Completed
Pull Request — master (#330)
by Stefan
04:25
created

ConnectExport::recordDelete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace ShopwarePlugins\Connect\Components;
4
5
use Doctrine\DBAL\DBALException;
6
use Shopware\Components\ContainerAwareEventManager;
7
use Shopware\Connect\SDK;
8
use Shopware\CustomModels\Connect\Attribute;
9
use ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway;
10
use ShopwarePlugins\Connect\Components\Validator\ProductAttributesValidator;
11
use Shopware\Components\Model\ModelManager;
12
use Shopware\Models\Article\Article;
13
use Shopware\Models\Article\Detail;
14
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamsAssignments;
15
use ShopwarePlugins\Connect\Components\ErrorHandler;
16
use ShopwarePlugins\Connect\Struct\ExportList;
17
use ShopwarePlugins\Connect\Struct\SearchCriteria;
18
use Enlight_Event_EventManager;
19
20
class ConnectExport
21
{
22
    const BATCH_SIZE = 200;
23
24
    /** @var
25
     * Helper
26
     */
27
    protected $helper;
28
29
    /** @var
30
     * SDK
31
     */
32
    protected $sdk;
33
34
    /** @var
35
     * ModelManager
36
     */
37
    protected $manager;
38
39
    /**
40
     * @var ProductAttributesValidator
41
     */
42
    protected $productAttributesValidator;
43
44
    /** @var
45
     * MarketplaceGateway
46
     */
47
    protected $marketplaceGateway;
48
49
    /**
50
     * @var ErrorHandler
51
     */
52
    protected $errorHandler;
53
54
    /**
55
     * @var Config
56
     */
57
    protected $configComponent;
58
59
    /**
60
     * @var Enlight_Event_EventManager
61
     */
62
    private $eventManager;
63
64
    /**
65
     * ConnectExport constructor.
66
     * @param Helper $helper
67
     * @param SDK $sdk
68
     * @param ModelManager $manager
69
     * @param ProductAttributesValidator $productAttributesValidator
70
     * @param Config $configComponent
71
     * @param \ShopwarePlugins\Connect\Components\ErrorHandler $errorHandler
72
     * @param Enlight_Event_EventManager $eventManager
73
     */
74
    public function __construct(
75
        Helper $helper,
76
        SDK $sdk,
77
        ModelManager $manager,
78
        ProductAttributesValidator $productAttributesValidator,
79
        Config $configComponent,
80
        ErrorHandler $errorHandler,
81
        Enlight_Event_EventManager $eventManager
82
    )
83
    {
84
        $this->helper = $helper;
85
        $this->sdk = $sdk;
86
        $this->manager = $manager;
87
        $this->productAttributesValidator = $productAttributesValidator;
88
        $this->configComponent = $configComponent;
89
        $this->errorHandler = $errorHandler;
90
        $this->eventManager = $eventManager;
91
    }
92
93
    /**
94
     * Load article entity
95
     *
96
     * @param $id
97
     * @return null|\Shopware\Models\Article\Article
98
     */
99
    public function getArticleModelById($id)
100
    {
101
        return $this->manager->getRepository('Shopware\Models\Article\Article')->find($id);
102
    }
103
104
    /**
105
     * Load article detail entity
106
     *
107
     * @param $id
108
     * @return null|\Shopware\Models\Article\Detail
109
     */
110
    public function getArticleDetailById($id)
111
    {
112
        return $this->manager->getRepository('Shopware\Models\Article\Detail')->find($id);
113
    }
114
115
    /**
116
     * Helper function to mark a given array of source ids for connect update
117
     *
118
     * There is a problem with flush when is called from life cycle event in php7,
119
     * this flag '$isEvent' is preventing the flush
120
     *
121
     * @param array $ids
122
     * @param ProductStreamsAssignments|null $streamsAssignments
123
     * @return array
124
     */
125
    public function export(array $ids, ProductStreamsAssignments $streamsAssignments = null)
126
    {
127
        $ids = $this->eventManager->filter(
128
            'Connect_Supplier_Get_Products_Filter_Source_IDS',
129
            $ids,
130
            [
131
                'subject' => $this
132
            ]
133
        );
134
135
        $connectItems = $this->fetchConnectItems($ids);
136
137
        $this->eventManager->notify(
138
            'Connect_Supplier_Get_All_Products_Before',
139
            [
140
                'subject' => $this,
141
                'products' => $connectItems
142
            ]
143
        );
144
145
        $this->manager->beginTransaction();
146
147
        foreach ($connectItems as &$item) {
148
            $model = $this->getArticleDetailById($item['articleDetailId']);
149
            if($model === null) {
150
                continue;
151
            }
152
153
            $connectAttribute = $this->helper->getOrCreateConnectAttributeByModel($model);
154
155
            $excludeInactiveProducts = $this->configComponent->getConfig('excludeInactiveProducts');
156
            if ($excludeInactiveProducts && !$model->getActive()) {
157
                $this->updateLocalConnectItem(
158
                    $connectAttribute->getSourceId(),
159
                    array(
160
                        'export_status' => Attribute::STATUS_INACTIVE,
161
                        'exported' => false,
162
                        'export_message' =>  Shopware()->Snippets()->getNamespace('backend/connect/view/main')->get(
163
                            'export/message/error_product_is_not_active',
164
                            'Produkt ist inaktiv',
165
                            true
166
                        ),
167
                    )
168
                );
169
                $this->manager->refresh($connectAttribute);
170
                continue;
171
            }
172
173
            if (!$this->helper->isProductExported($connectAttribute)) {
174
                $status = Attribute::STATUS_INSERT;
175
            } else {
176
                $status = Attribute::STATUS_UPDATE;
177
            }
178
179
            $categories = $this->helper->getConnectCategoryForProduct($item['articleId']);
180
            if (is_string($categories)) {
181
                $categories = array($categories);
182
            }
183
            $categories = json_encode($categories);
184
185
            $this->updateLocalConnectItem(
186
                $connectAttribute->getSourceId(),
187
                array(
188
                    'export_status' => $status,
189
                    'export_message' => null,
190
                    'exported' => true,
191
                    'category' => $categories,
192
                )
193
            );
194
195
            try {
196
                $this->productAttributesValidator->validate($this->extractProductAttributes($model));
197
                if ($status == Attribute::STATUS_INSERT) {
198
                    $this->sdk->recordInsert($item['sourceId']);
199
                } else {
200
                    $this->sdk->recordUpdate($item['sourceId']);
201
                }
202
203
                if ($this->helper->isMainVariant($item['sourceId']) &&
204
                    $streamsAssignments !== null &&
205
                    $streamsAssignments->getStreamsByArticleId($item['articleId']) !== null
206
                ) {
207
                    $this->sdk->recordStreamAssignment(
208
                        $item['sourceId'],
209
                        $streamsAssignments->getStreamsByArticleId($item['articleId']),
210
                        $item['groupId']
211
                    );
212
                }
213
            } catch (\Exception $e) {
214
                if ($this->errorHandler->isPriceError($e)) {
215
                    $this->updateLocalConnectItem(
216
                        $connectAttribute->getSourceId(),
217
                        array(
218
                            'export_status' => Attribute::STATUS_ERROR_PRICE,
219
                            'export_message' => Shopware()->Snippets()->getNamespace('backend/connect/view/main')->get(
220
                                'export/message/error_price_status',
221
                                'There is an empty price field',
222
                                true
223
                            ),
224
                        )
225
                    );
226
                } else {
227
                    $this->updateLocalConnectItem(
228
                        $connectAttribute->getSourceId(),
229
                        array(
230
                            'export_status' => Attribute::STATUS_ERROR,
231
                            'export_message' => $e->getMessage() . "\n" . $e->getTraceAsString(),
232
                        )
233
                    );
234
                }
235
236
                $this->errorHandler->handle($e);
237
            }
238
            $this->manager->refresh($connectAttribute);
239
        }
240
241
        try {
242
            $this->manager->commit();
243
        } catch (\Exception $e) {
244
            $this->manager->rollback();
245
            $this->errorHandler->handle($e);
246
        }
247
248
        return $this->errorHandler->getMessages();
249
    }
250
251
    /**
252
     * Update connect attribute data
253
     *
254
     * @param string $sourceId
255
     * @param array $params
256
     */
257
    private function updateLocalConnectItem($sourceId, $params = array())
258
    {
259
        if (empty($params)) {
260
            return;
261
        }
262
        $possibleValues = array(
263
            Attribute::STATUS_DELETE,
264
            Attribute::STATUS_INSERT,
265
            Attribute::STATUS_UPDATE,
266
            Attribute::STATUS_ERROR,
267
            Attribute::STATUS_ERROR_PRICE,
268
            Attribute::STATUS_INACTIVE,
269
            Attribute::STATUS_SYNCED,
270
            null,
271
        );
272
273
        if (isset($params['export_status']) && !in_array($params['export_status'], $possibleValues)) {
274
            throw new \InvalidArgumentException('Invalid export status');
275
        }
276
277
        if (isset($params['exported']) && !is_bool($params['exported'])) {
278
            throw new \InvalidArgumentException('Parameter $exported must be boolean.');
279
        }
280
281
        $builder = $this->manager->getConnection()->createQueryBuilder();
282
        $builder->update('s_plugin_connect_items', 'ci');
283
        array_walk($params, function($param, $name) use ($builder) {
284
            $builder->set('ci.' . $name, ':' . $name)
285
                    ->setParameter($name, $param);
286
        });
287
288
        $builder->where('source_id = :sourceId')
289
                ->setParameter('sourceId', $sourceId)
290
                ->andWhere('shop_id IS NULL')
291
                ->execute();
292
    }
293
294
    /**
295
     * Fetch connect items
296
     * Default order is main variant first, after that regular variants.
297
     * This is needed, because first received variant with an unknown groupId in Connect
298
     * will be selected as main variant.
299
     *
300
     * @param array $sourceIds
301
     * @param boolean $orderByMainVariants
302
     * @return array
303
     */
304
    public function fetchConnectItems(array $sourceIds, $orderByMainVariants = true)
305
    {
306
        if (count($sourceIds) == 0) {
307
            return array();
308
        }
309
310
        $implodedIds = '"' . implode('","', $sourceIds) . '"';
311
        $query = "SELECT bi.article_id as articleId,
312
                    bi.article_detail_id as articleDetailId,
313
                    bi.export_status as exportStatus,
314
                    bi.export_message as exportMessage,
315
                    bi.source_id as sourceId,
316
                    a.name as title,
317
                    IF (a.configurator_set_id IS NOT NULL, a.id, NULL) as groupId,
318
                    d.ordernumber as number
319
            FROM s_plugin_connect_items bi
320
            LEFT JOIN s_articles a ON bi.article_id = a.id
321
            LEFT JOIN s_articles_details d ON bi.article_detail_id = d.id
322
            WHERE bi.source_id IN ($implodedIds)";
323
324
        if ($orderByMainVariants === false) {
325
            $query .= ';';
326
            return Shopware()->Db()->fetchAll($query);
327
        }
328
329
        $query .= 'AND d.kind = ?;';
330
        $mainVariants = Shopware()->Db()->fetchAll($query, array(1));
331
        $regularVariants = Shopware()->Db()->fetchAll($query, array(2));
332
333
        return array_merge($mainVariants, $regularVariants);
334
    }
335
336
    /**
337
     * Helper function to return export product ids
338
     * @return array
339
     */
340
    public function getExportArticlesIds()
341
    {
342
        $builder = $this->manager->createQueryBuilder();
343
        $builder->from('Shopware\CustomModels\Connect\Attribute', 'at');
344
        $builder->join('at.article', 'a');
345
        $builder->join('a.mainDetail', 'd');
346
        $builder->leftJoin('d.prices', 'p', 'with', "p.from = 1 AND p.customerGroupKey = 'EK'");
347
        $builder->leftJoin('a.supplier', 's');
348
        $builder->leftJoin('a.tax', 't');
349
350
        $builder->select(array('a.id'));
351
352
        $builder->where("at.exportStatus = 'update' OR at.exportStatus = 'insert' OR at.exportStatus = 'error'");
353
        $builder->andWhere('at.shopId IS NULL');
354
355
        $query = $builder->getQuery();
356
        $articles = $query->getArrayResult();
357
358
        $ids = array();
359
        foreach ($articles as $article) {
360
            $ids[] = $article['id'];
361
        }
362
363
        return $ids;
364
    }
365
366
    /**
367
     * Helper function to count how many changes
368
     * are waiting to be synchronized
369
     *
370
     * @return int
371
     */
372
    public function getChangesCount()
373
    {
374
        $sql = 'SELECT COUNT(*) FROM `sw_connect_change`';
375
376
        return (int)Shopware()->Db()->fetchOne($sql);
377
    }
378
379
    /**
380
     * Mark single connect product detail for delete
381
     *
382
     * @param \Shopware\Models\Article\Detail $detail
383
     */
384
    public function syncDeleteDetail(Detail $detail)
385
    {
386
        $attribute = $this->helper->getConnectAttributeByModel($detail);
387
        if (!$this->helper->isProductExported($attribute)) {
388
            return;
389
        }
390
        $this->sdk->recordDelete($attribute->getSourceId());
391
        $attribute->setExportStatus(Attribute::STATUS_DELETE);
392
        $attribute->setExported(false);
393
        $this->manager->persist($attribute);
394
        $this->manager->flush($attribute);
395
    }
396
397
    /**
398
     * Mark all product variants for delete
399
     *
400
     * @param Article $article
401
     */
402
    public function setDeleteStatusForVariants(Article $article)
403
    {
404
        $builder = $this->manager->createQueryBuilder();
405
        $builder->select(array('at.sourceId'))
406
            ->from('Shopware\CustomModels\Connect\Attribute', 'at')
407
            ->where('at.articleId = :articleId')
408
            ->andWhere('at.exported = 1')
409
            ->setParameter(':articleId', $article->getId());
410
        $connectItems = $builder->getQuery()->getArrayResult();
411
412
        foreach($connectItems as $item) {
413
            $this->sdk->recordDelete($item['sourceId']);
414
        }
415
416
        $builder = $this->manager->createQueryBuilder();
417
        $builder->update('Shopware\CustomModels\Connect\Attribute', 'at')
418
            ->set('at.exportStatus', $builder->expr()->literal(Attribute::STATUS_DELETE))
419
            ->set('at.exported', 0)
420
            ->where('at.articleId = :articleId')
421
            ->setParameter(':articleId', $article->getId());
422
423
        $builder->getQuery()->execute();
424
    }
425
426
    /**
427
     * @param array $sourceIds
428
     * @param $status
429
     */
430
    public function updateConnectItemsStatus(array $sourceIds, $status)
431
    {
432
        if (empty($sourceIds)) {
433
            return;
434
        }
435
436
        $chunks = array_chunk($sourceIds, self::BATCH_SIZE);
437
438
        foreach ($chunks as $chunk) {
439
            $builder = $this->manager->getConnection()->createQueryBuilder();
440
            $builder->update('s_plugin_connect_items', 'ci')
441
                ->set('ci.export_status', ':status')
442
                ->where('source_id IN (:sourceIds)')
443
                ->setParameter('sourceIds', $chunk, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
444
                ->setParameter('status', $status)
445
                ->execute();
446
        }
447
    }
448
449
    /**
450
     * @param SearchCriteria $criteria
451
     * @return ExportList
452
     */
453
    public function getExportList(SearchCriteria $criteria)
454
    {
455
        $customProductsTableExists = false;
456
        try {
457
            $builder = $this->manager->getConnection()->createQueryBuilder();
458
            $builder->select('id');
459
            $builder->from('s_plugin_custom_products_template');
460
            $builder->setMaxResults(1);
461
            $builder->execute()->fetch();
462
463
            $customProductsTableExists = true;
464
        } catch (DBALException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\DBALException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
465
            // ignore it
466
            // custom products is not installed
467
        }
468
469
        $builder = $this->manager->getConnection()->createQueryBuilder();
470
        $builder->select(array(
471
            'a.id',
472
            'd.ordernumber as number',
473
            'd.inStock as inStock',
474
            'a.name as name',
475
            's.name as supplier',
476
            'a.active as active',
477
            't.tax as tax',
478
            'p.price * (100 + t.tax) / 100 as price',
479
            'i.category',
480
            'i.export_status as exportStatus',
481
            'i.export_message as exportMessage',
482
            'i.cron_update as cronUpdate'
483
        ))
484
            ->from('s_plugin_connect_items', 'i')
485
            ->innerJoin('i', 's_articles', 'a', 'a.id = i.article_id')
486
            ->innerJoin('a', 's_articles_details', 'd', 'a.main_detail_id = d.id')
487
            ->leftJoin('d', 's_articles_prices', 'p', 'd.id = p.articledetailsID')
488
            ->leftJoin('a', 's_core_tax', 't', 'a.taxID = t.id')
489
            ->leftJoin('a', 's_articles_supplier', 's', 'a.supplierID = s.id')
490
            ->groupBy('i.article_id')
491
            ->where('i.shop_id IS NULL');
492
493
        if ($customProductsTableExists) {
494
            $builder->addSelect("IF(spcptpr.template_id > 0, 1, 0) as customProduct")
495
                    ->leftJoin('a', 's_plugin_custom_products_template_product_relation', 'spcptpr', 'a.id = spcptpr.article_id');
496
        }
497
498
        if ($criteria->search) {
499
            $builder->andWhere('d.ordernumber LIKE :search OR a.name LIKE :search OR s.name LIKE :search')
500
                ->setParameter('search', $criteria->search);
501
        }
502
503
        if ($criteria->categoryId) {
504
505
            // Get all children categories
506
            $qBuilder = $this->manager->getConnection()->createQueryBuilder();
507
            $qBuilder->select('c.id');
508
            $qBuilder->from('s_categories', 'c');
509
            $qBuilder->where('c.path LIKE :categoryIdSearch');
510
            $qBuilder->orWhere('c.id = :categoryId');
511
            $qBuilder->setParameter(':categoryId', $criteria->categoryId);
512
            $qBuilder->setParameter(':categoryIdSearch', "%|$criteria->categoryId|%");
513
514
            $categoryIds = $qBuilder->execute()->fetchAll(\PDO::FETCH_COLUMN);
515
516
            if (count($categoryIds) === 0) {
517
                $categoryIds = array($criteria->categoryId);
518
            }
519
520
            $builder->innerJoin('a', 's_articles_categories', 'sac', 'a.id = sac.articleID')
521
                ->andWhere('sac.categoryID IN (:categoryIds)')
522
                ->setParameter('categoryIds', $categoryIds, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
523
        }
524
525
        if ($criteria->supplierId) {
526
            $builder->andWhere('a.supplierID = :supplierId')
527
                ->setParameter('supplierId', $criteria->supplierId);
528
        }
529
530
        if ($criteria->exportStatus) {
531
            $errorStatuses = [Attribute::STATUS_ERROR, Attribute::STATUS_ERROR_PRICE];
532
533
            if (in_array($criteria->exportStatus, $errorStatuses)) {
534
                $builder->andWhere('i.export_status IN (:status)')
535
                    ->setParameter('status', $errorStatuses, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
536
            } elseif ($criteria->exportStatus == Attribute::STATUS_INACTIVE) {
537
                $builder->andWhere('a.active = :status')
538
                    ->setParameter('status', false);
539
            } else {
540
                $builder->andWhere('i.export_status LIKE :status')
541
                    ->setParameter('status', $criteria->exportStatus);
542
            }
543
        }
544
545
        if ($criteria->active) {
546
            $builder->andWhere('a.active = :active')
547
                ->setParameter('active', $criteria->active);
548
        }
549
550
        if ($criteria->orderBy) {
551
            $builder->orderBy($criteria->orderBy, $criteria->orderByDirection);
552
        }
553
554
        $total = $builder->execute()->rowCount();
555
556
        $builder->setFirstResult($criteria->offset);
557
        $builder->setMaxResults($criteria->limit);
558
559
        $data = $builder->execute()->fetchAll();
560
561
        return new ExportList(array(
562
            'articles' => $data,
563
            'count' => $total,
564
        ));
565
    }
566
567
    public function clearConnectItems()
568
    {
569
        $this->deleteAllConnectProducts();
570
        $this->resetConnectItemsStatus();
571
    }
572
573
    /**
574
     * @param $articleId
575
     */
576
    public function markArticleForCronUpdate($articleId)
577
    {
578
        $this->manager->getConnection()->update(
579
            's_plugin_connect_items',
580
            ['cron_update' => 1],
581
            ['article_id' => (int) $articleId]
582
        );
583
    }
584
585
    /**
586
     * Wrapper method
587
     *
588
     * @param string $sourceId
589
     */
590
    public function recordDelete($sourceId)
591
    {
592
        $this->sdk->recordDelete($sourceId);
593
    }
594
595
    /**
596
     * Deletes products hash
597
     */
598
    private function deleteAllConnectProducts()
599
    {
600
        $builder = $this->manager->getConnection()->createQueryBuilder();
601
        $builder->delete('sw_connect_product');
602
        $builder->execute();
603
    }
604
605
    /**
606
     * Resets all item status
607
     */
608
    private function resetConnectItemsStatus()
609
    {
610
        $builder = $this->manager->getConnection()->createQueryBuilder();
611
        $builder->update('s_plugin_connect_items', 'ci')
612
            ->set('export_status', ':exportStatus')
613
            ->set('revision', ':revision')
614
            ->set('exported', 0)
615
            ->setParameter('exportStatus', null)
616
            ->setParameter('revision', null);
617
618
        $builder->execute();
619
    }
620
621
    private function getMarketplaceGateway()
622
    {
623
        //todo@fixme: Implement better way to get MarketplaceGateway
624
        if (!$this->marketplaceGateway) {
625
            $this->marketplaceGateway = new MarketplaceGateway($this->manager);
626
        }
627
628
        return $this->marketplaceGateway;
629
    }
630
631
    /**
632
     * Extracts all marketplaces attributes from product
633
     *
634
     * @param Detail $detail
635
     * @return array
636
     */
637
    private function extractProductAttributes(Detail $detail)
638
    {
639
        $marketplaceAttributes = array();
640
        $marketplaceAttributes['purchaseUnit'] = $detail->getPurchaseUnit();
641
        $marketplaceAttributes['referenceUnit'] = $detail->getReferenceUnit();
642
643
        // marketplace attributes are available only for SEM shops
644
        if ($this->configComponent->getConfig('isDefault', true)) {
645
            return $marketplaceAttributes;
646
        }
647
648
        foreach ($this->getMarketplaceGateway()->getMappings() as $mapping) {
649
            $shopwareAttribute = $mapping['shopwareAttributeKey'];
650
            $getter = 'get' . ucfirst($shopwareAttribute);
651
652
            if (method_exists($detail->getAttribute(), $getter)) {
653
                $marketplaceAttributes[$shopwareAttribute] = $detail->getAttribute()->{$getter}();
654
            }
655
        }
656
657
        return $marketplaceAttributes;
658
    }
659
}