Magento1CustomerReader.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. namespace App\Services\Asteria;
  3. use Illuminate\Support\Collection;
  4. use Illuminate\Support\Facades\DB;
  5. class Magento1CustomerReader
  6. {
  7. private const CUSTOMER_CODES = [
  8. 'firstname',
  9. 'lastname',
  10. 'dob',
  11. 'gender',
  12. 'telephone',
  13. 'password_hash',
  14. 'default_billing',
  15. 'default_shipping',
  16. 'source',
  17. 'customer_source',
  18. ];
  19. private const ADDRESS_CODES = [
  20. 'firstname',
  21. 'lastname',
  22. 'company',
  23. 'street',
  24. 'city',
  25. 'region',
  26. 'postcode',
  27. 'country_id',
  28. 'telephone',
  29. ];
  30. private Magento1Schema $schema;
  31. /** @var array<string, array<string, object>> */
  32. private array $attributeMaps = [];
  33. public function __construct(private string $connection = 'asteria')
  34. {
  35. $this->schema = new Magento1Schema($connection);
  36. }
  37. /**
  38. * @return Collection<int, array<string, mixed>>
  39. */
  40. public function fetchCustomers(int $afterEntityId, int $limit): Collection
  41. {
  42. $query = DB::connection($this->connection)
  43. ->table('customer_entity')
  44. ->where('entity_id', '>', $afterEntityId)
  45. ->orderBy('entity_id')
  46. ->limit($limit);
  47. $staticColumns = $this->existingColumns('customer_entity', array_merge(
  48. ['entity_id', 'email', 'is_active', 'created_at', 'updated_at', 'group_id'],
  49. self::CUSTOMER_CODES
  50. ));
  51. $rows = $query->get($staticColumns);
  52. if ($rows->isEmpty()) {
  53. return collect();
  54. }
  55. $eav = $this->loadEavValues(
  56. 'customer',
  57. 'customer_entity',
  58. $rows->pluck('entity_id')->all(),
  59. self::CUSTOMER_CODES
  60. );
  61. return $rows->map(function ($row) use ($eav) {
  62. $id = (int) $row->entity_id;
  63. $merged = array_merge($eav[$id] ?? [], (array) $row);
  64. $merged['entity_id'] = $id;
  65. return $merged;
  66. })->values();
  67. }
  68. /**
  69. * @param array<int, int> $customerEntityIds
  70. * @return Collection<int, array<string, mixed>>
  71. */
  72. public function fetchAddresses(array $customerEntityIds): Collection
  73. {
  74. if ($customerEntityIds === []) {
  75. return collect();
  76. }
  77. $staticColumns = $this->existingColumns('customer_address_entity', array_merge(
  78. ['entity_id', 'parent_id', 'created_at', 'is_active'],
  79. self::ADDRESS_CODES
  80. ));
  81. $rows = DB::connection($this->connection)
  82. ->table('customer_address_entity')
  83. ->whereIn('parent_id', $customerEntityIds)
  84. ->orderBy('entity_id')
  85. ->get($staticColumns);
  86. if ($rows->isEmpty()) {
  87. return collect();
  88. }
  89. $eav = $this->loadEavValues(
  90. 'customer_address',
  91. 'customer_address_entity',
  92. $rows->pluck('entity_id')->all(),
  93. self::ADDRESS_CODES
  94. );
  95. return $rows->map(function ($row) use ($eav) {
  96. $id = (int) $row->entity_id;
  97. $merged = array_merge($eav[$id] ?? [], (array) $row);
  98. $merged['entity_id'] = $id;
  99. $merged['parent_id'] = (int) $row->parent_id;
  100. return $merged;
  101. })->values();
  102. }
  103. /**
  104. * Magento 1 newsletter_subscriber rows for the given customers (by entity_id or email).
  105. *
  106. * @param array<int, int> $customerEntityIds
  107. * @param array<int, string> $emails
  108. * @return Collection<int, array<string, mixed>>
  109. */
  110. public function fetchSubscribers(array $customerEntityIds, array $emails): Collection
  111. {
  112. $columns = $this->subscriberColumns();
  113. if ($columns === []) {
  114. return collect();
  115. }
  116. $customerEntityIds = array_values(array_unique(array_filter(array_map('intval', $customerEntityIds))));
  117. $emails = array_values(array_unique(array_filter(array_map(
  118. fn ($email) => strtolower(trim((string) $email)),
  119. $emails
  120. ))));
  121. if ($customerEntityIds === [] && $emails === []) {
  122. return collect();
  123. }
  124. $query = DB::connection($this->connection)->table('newsletter_subscriber');
  125. $query->where(function ($inner) use ($customerEntityIds, $emails, $columns) {
  126. if ($customerEntityIds !== [] && in_array('customer_id', $columns, true)) {
  127. $inner->whereIn('customer_id', $customerEntityIds);
  128. }
  129. if ($emails !== []) {
  130. $method = $customerEntityIds !== [] && in_array('customer_id', $columns, true)
  131. ? 'orWhereIn'
  132. : 'whereIn';
  133. $inner->{$method}(DB::raw('LOWER(subscriber_email)'), $emails);
  134. }
  135. });
  136. return $this->mapSubscriberRows($query->get($columns));
  137. }
  138. /**
  139. * @return Collection<int, array<string, mixed>>
  140. */
  141. public function fetchSubscribersAfter(int $afterSubscriberId, int $limit): Collection
  142. {
  143. $columns = $this->subscriberColumns();
  144. if ($columns === [] || ! in_array('subscriber_id', $columns, true)) {
  145. return collect();
  146. }
  147. $rows = DB::connection($this->connection)
  148. ->table('newsletter_subscriber')
  149. ->where('subscriber_id', '>', $afterSubscriberId)
  150. ->orderBy('subscriber_id')
  151. ->limit($limit)
  152. ->get($columns);
  153. return $this->mapSubscriberRows($rows);
  154. }
  155. /**
  156. * @return array<int, string>
  157. */
  158. private function subscriberColumns(): array
  159. {
  160. if (! $this->schema->hasTable('newsletter_subscriber')) {
  161. return [];
  162. }
  163. $columns = $this->schema->existingColumns('newsletter_subscriber', [
  164. 'subscriber_id',
  165. 'customer_id',
  166. 'subscriber_email',
  167. 'subscriber_status',
  168. 'subscriber_confirm_code',
  169. 'change_status_at',
  170. ]);
  171. if (! in_array('subscriber_email', $columns, true) || ! in_array('subscriber_status', $columns, true)) {
  172. return [];
  173. }
  174. return $columns;
  175. }
  176. /**
  177. * @param \Illuminate\Support\Collection<int, object>|iterable<int, object> $rows
  178. * @return Collection<int, array<string, mixed>>
  179. */
  180. private function mapSubscriberRows($rows): Collection
  181. {
  182. return collect($rows)->map(function ($row) {
  183. $data = (array) $row;
  184. $data['subscriber_id'] = (int) ($row->subscriber_id ?? 0);
  185. $data['customer_id'] = (int) ($row->customer_id ?? 0);
  186. $data['subscriber_status'] = (int) ($row->subscriber_status ?? 0);
  187. return $data;
  188. })->values();
  189. }
  190. /**
  191. * @param array<int, string> $codes
  192. * @return array<int, array<string, mixed>>
  193. */
  194. private function loadEavValues(
  195. string $entityTypeCode,
  196. string $valueTablePrefix,
  197. array $entityIds,
  198. array $codes
  199. ): array {
  200. if ($entityIds === []) {
  201. return [];
  202. }
  203. $attributes = $this->attributeMap($entityTypeCode, $codes);
  204. if ($attributes === []) {
  205. return [];
  206. }
  207. $byType = [];
  208. foreach ($attributes as $code => $attribute) {
  209. $type = $attribute->backend_type ?? 'varchar';
  210. if ($type === 'static' || $type === '') {
  211. continue;
  212. }
  213. $byType[$type][(int) $attribute->attribute_id] = $code;
  214. }
  215. $values = [];
  216. foreach ($byType as $type => $idToCode) {
  217. $table = $valueTablePrefix.'_'.$type;
  218. if (! $this->schema->hasTable($table)) {
  219. continue;
  220. }
  221. $rows = DB::connection($this->connection)
  222. ->table($table)
  223. ->select('entity_id', 'attribute_id', 'value')
  224. ->whereIn('entity_id', $entityIds)
  225. ->whereIn('attribute_id', array_keys($idToCode))
  226. ->get();
  227. foreach ($rows as $row) {
  228. $code = $idToCode[(int) $row->attribute_id] ?? null;
  229. if ($code === null || $row->value === null || $row->value === '') {
  230. continue;
  231. }
  232. $values[(int) $row->entity_id][$code] = $row->value;
  233. }
  234. }
  235. return $values;
  236. }
  237. /**
  238. * @param array<int, string> $codes
  239. * @return array<string, object>
  240. */
  241. private function attributeMap(string $entityTypeCode, array $codes): array
  242. {
  243. $cacheKey = $entityTypeCode.':'.implode(',', $codes);
  244. if (isset($this->attributeMaps[$cacheKey])) {
  245. return $this->attributeMaps[$cacheKey];
  246. }
  247. $query = DB::connection($this->connection)
  248. ->table('eav_attribute')
  249. ->select('attribute_id', 'attribute_code', 'backend_type')
  250. ->whereIn('attribute_code', $codes);
  251. if ($this->schema->hasTable('eav_entity_type')) {
  252. $typeId = DB::connection($this->connection)
  253. ->table('eav_entity_type')
  254. ->where('entity_type_code', $entityTypeCode)
  255. ->value('entity_type_id');
  256. if ($typeId) {
  257. $query->where('entity_type_id', $typeId);
  258. }
  259. }
  260. return $this->attributeMaps[$cacheKey] = $query->get()->keyBy('attribute_code')->all();
  261. }
  262. /**
  263. * @param array<int, string> $candidates
  264. * @return array<int, string>
  265. */
  266. private function existingColumns(string $table, array $candidates): array
  267. {
  268. return $this->schema->existingColumns($table, $candidates);
  269. }
  270. }