Completed
Pull Request — master (#377)
by Jonas
03:33
created

ImportService::assignCategoryToArticles()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 5
nop 2
dl 0
loc 28
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * (c) shopware AG <[email protected]>
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace ShopwarePlugins\Connect\Components;
9
10
use Doctrine\DBAL\Connection;
11
use Shopware\Bundle\AttributeBundle\Service\DataPersister;
12
use Shopware\Components\Model\CategoryDenormalization;
13
use ShopwarePlugins\Connect\Components\CategoryResolver\AutoCategoryResolver;
14
use Shopware\Components\Model\ModelManager;
15
use Shopware\Components\MultiEdit\Resource\ResourceInterface;
16
use Shopware\CustomModels\Connect\ProductToRemoteCategoryRepository;
17
use Shopware\CustomModels\Connect\RemoteCategoryRepository;
18
use Shopware\Models\Article\Repository as ArticleRepository;
19
use Shopware\Models\Category\Repository as CategoryRepository;
20
21
class ImportService
22
{
23
    /**
24
     * @var ModelManager
25
     */
26
    private $manager;
27
28
    /**
29
     * @var \Shopware\Components\MultiEdit\Resource\ResourceInterface
30
     */
31
    private $productResource;
32
33
    /**
34
     * @var \Shopware\Models\Category\Repository
35
     */
36
    private $categoryRepository;
37
38
    /**
39
     * @var ArticleRepository
40
     */
41
    private $articleRepository;
42
43
    /**
44
     * @var RemoteCategoryRepository
45
     */
46
    private $remoteCategoryRepository;
47
48
    /**
49
     * @var ProductToRemoteCategoryRepository
50
     */
51
    private $productToRemoteCategoryRepository;
52
53
    /**
54
     * @var CategoryResolver
55
     */
56
    private $autoCategoryResolver;
57
58
    /**
59
     * @var CategoryExtractor
60
     */
61
    private $categoryExtractor;
62
63
    /**
64
     * @var CategoryDenormalization
65
     */
66
    private $categoryDenormalization;
67
68
    /**
69
     * @var DataPersister
70
     */
71
    private $dataPersister;
72
73
    public function __construct(
74
        ModelManager $manager,
75
        ResourceInterface $productResource,
76
        CategoryRepository $categoryRepository,
77
        ArticleRepository$articleRepository,
78
        RemoteCategoryRepository $remoteCategoryRepository,
79
        ProductToRemoteCategoryRepository $productToRemoteCategoryRepository,
80
        AutoCategoryResolver $categoryResolver,
81
        CategoryExtractor $categoryExtractor,
82
        CategoryDenormalization $categoryDenormalization,
83
        DataPersister $dataPersister
84
    ) {
85
        $this->manager = $manager;
86
        $this->productResource = $productResource;
87
        $this->categoryRepository = $categoryRepository;
88
        $this->articleRepository = $articleRepository;
89
        $this->remoteCategoryRepository = $remoteCategoryRepository;
90
        $this->productToRemoteCategoryRepository = $productToRemoteCategoryRepository;
91
        $this->autoCategoryResolver = $categoryResolver;
92
        $this->categoryExtractor = $categoryExtractor;
93
        $this->categoryDenormalization = $categoryDenormalization;
94
        $this->dataPersister = $dataPersister;
95
    }
96
97
    public function findBothArticlesType($categoryId, $query = '', $showOnlyConnectArticles = true, $limit = 10, $offset = 0)
98
    {
99
        if ($categoryId == 0) {
100
            return [];
101
        }
102
103
        return $this->productResource->filter($this->getAst($categoryId, $query, $showOnlyConnectArticles), $offset, $limit);
104
    }
105
106
    /**
107
     * @param $categoryId
108
     * @return bool
109
     */
110
    public function hasCategoryChildren($categoryId)
111
    {
112
        return (bool) $this->categoryRepository->getChildrenCountList($categoryId);
113
    }
114
115
    public function assignCategoryToArticles($categoryId, array $articleIds)
116
    {
117
        $articles = $this->articleRepository->findBy(['id' => $articleIds]);
118
119
        if (empty($articles)) {
120
            throw new \RuntimeException('Invalid article ids');
121
        }
122
123
        /** @var \Shopware\Models\Category\Category $category */
124
        $category = $this->categoryRepository->find($categoryId);
125
        if (!$category) {
126
            throw new \RuntimeException('Invalid category id');
127
        }
128
129
        /** @var \Shopware\Models\Article\Article $article */
130
        foreach ($articles as $article) {
131
            $article->addCategory($category);
132
            $this->manager->persist($article);
133
            /** @var \Shopware\Models\Article\Detail $detail */
134
            foreach ($article->getDetails() as $detail) {
135
                $attribute = $detail->getAttribute();
136
                $attribute->setConnectMappedCategory(true);
137
                $this->manager->persist($attribute);
138
            }
139
        }
140
141
        $this->manager->flush();
142
    }
143
144
    /**
145
     * Unassign all categories from given article ids
146
     * Set connect_mapped_category flag in article
147
     * attributes to NULL
148
     *
149
     * @param array $articleIds
150
     * @throws \Doctrine\DBAL\ConnectionException
151
     * @throws \Exception
152
     */
153
    public function unAssignArticleCategories(array $articleIds)
154
    {
155
        if (!empty($articleIds)) {
156
            // cast all items in $articleIds to int
157
            // before use them in WHERE IN clause
158
            foreach ($articleIds as $key => $articleId) {
159
                $articleIds[$key] = (int) $articleId;
160
            }
161
162
            $connection = $this->manager->getConnection();
163
            $connection->beginTransaction();
164
165
            try {
166
                $attributeStatement = $connection->prepare(
167
                    'UPDATE s_articles_attributes SET connect_mapped_category = NULL WHERE articleID IN (' . implode(', ', $articleIds) . ')'
168
                );
169
                $attributeStatement->execute();
170
171
                $categoriesStatement = $this->manager->getConnection()->prepare('DELETE FROM s_articles_categories WHERE articleID IN (' . implode(', ', $articleIds) . ')');
172
                $categoriesStatement->execute();
173
174
                $categoryLogStatement = $this->manager->getConnection()->prepare('DELETE FROM s_articles_categories_ro WHERE articleID IN (' . implode(', ', $articleIds) . ')');
175
                $categoryLogStatement->execute();
176
                $connection->commit();
177
            } catch (\Exception $e) {
178
                $connection->rollBack();
179
                throw new \Exception($e->getMessage());
180
            }
181
        }
182
    }
183
184
    /**
185
     * Collect remote article ids by given category id
186
     *
187
     * @param int $localCategoryId
188
     * @return array
189
     */
190
    public function findRemoteArticleIdsByCategoryId($localCategoryId)
191
    {
192
        $connection = $this->manager->getConnection();
193
        $sql = 'SELECT sac.articleID
194
            FROM s_articles_categories sac
195
            LEFT JOIN s_articles_attributes saa ON sac.articleID = saa.articleID
196
            WHERE sac.categoryID = :categoryId AND saa.connect_mapped_category = 1';
197
        $rows = $connection->fetchAll($sql, [':categoryId' => $localCategoryId]);
198
199
        return array_map(function ($row) {
200
            return $row['articleID'];
201
        }, $rows);
202
    }
203
204
    /**
205
     * @param array $categoryIds
206
     * @return int
207
     */
208
    public function deactivateLocalCategoriesByIds(array $categoryIds)
209
    {
210
        $builder = $this->manager->getConnection()->createQueryBuilder();
211
        $rowCount = $builder->update('s_categories', 'c')
212
            ->set('c.active', 0)
213
            ->where('c.id IN (:categoryIds)')
214
            ->setParameter('categoryIds', $categoryIds, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
215
            ->execute();
216
217
        return $rowCount;
218
    }
219
220
    /**
221
     * Collect all child categories by given
222
     * remote category key and create same
223
     * categories structure as Shopware Connect structure.
224
     * Find all remote products which belong to these categories
225
     * and assign them.
226
     *
227
     * @param int $localCategoryId
228
     * @param string $remoteCategoryKey
229
     * @param string $remoteCategoryLabel
230
     * @return void
231
     */
232
    public function importRemoteCategory($localCategoryId, $remoteCategoryKey, $remoteCategoryLabel)
233
    {
234
        /** @var \Shopware\Models\Category\Category $localCategory */
235
        $localCategory = $this->categoryRepository->find((int) $localCategoryId);
236
        if (!$localCategory) {
237
            throw new \RuntimeException('Local category not found!');
238
        }
239
240
        /** @var \Shopware\CustomModels\Connect\RemoteCategory $remoteCategory */
241
        $remoteCategory = $this->remoteCategoryRepository->findOneBy(['categoryKey' => $remoteCategoryKey]);
242
        if (!$remoteCategory) {
243
            throw new \RuntimeException('Remote category not found!');
244
        }
245
246
        // collect his child categories and
247
        // generate remote category tree by given remote category
248
        $remoteCategoryChildren = $this->categoryExtractor->getRemoteCategoriesTree($remoteCategoryKey, true);
249
        $remoteCategoryNodes = [
250
            [
251
                'name' => $remoteCategoryLabel,
252
                'categoryId' => $remoteCategoryKey,
253
                'leaf' => empty($remoteCategoryChildren) ? true : false,
254
                'children' => $remoteCategoryChildren,
255
            ]
256
        ];
257
258
        // create same category structure as Shopware Connect structure
259
        $categories = $this->autoCategoryResolver->convertTreeToKeys($remoteCategoryNodes, $localCategory->getId(), false);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ShopwarePlugins\Connect\...onents\CategoryResolver as the method convertTreeToKeys() does only exist in the following sub-classes of ShopwarePlugins\Connect\...onents\CategoryResolver: ShopwarePlugins\Connect\...er\AutoCategoryResolver. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
260
261
        foreach ($categories as $category) {
262
            $articleIds = $this->productToRemoteCategoryRepository->findArticleIdsByRemoteCategory($category['remoteCategory']);
263
            foreach ($articleIds as $articleId) {
264
                $this->categoryDenormalization->addAssignment($articleId, $category['categoryKey']);
265
                $this->categoryDenormalization->removeAssignment($articleId, $category['parentId']);
266
                $this->manager->getConnection()->executeQuery(
267
                    'INSERT IGNORE INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES (?, ?)',
268
                    [$articleId,  $category['categoryKey']]
269
                );
270
                $this->manager->getConnection()->executeQuery(
271
                    'DELETE FROM `s_articles_categories` WHERE `articleID` = :articleID AND `categoryID` = :categoryID',
272
                    [
273
                        ':articleID' => $articleId,
274
                        ':categoryID' => $category['parentId']
275
                    ]
276
                );
277
                $detailId = $this->manager->getConnection()->fetchColumn(
278
                    'SELECT id FROM `s_articles_details` WHERE `articleID` = :articleID',
279
                    ['articleID' => $articleId]
280
                );
281
                $this->manager->getConnection()->executeQuery(
282
                    'INSERT  INTO `s_articles_attributes` (`articleID`, `articledetailsID`, `connect_mapped_category`) 
283
                        VALUES (?, ?, 1)
284
                        ON DUPLICATE KEY UPDATE `connect_mapped_category` = 1
285
                    ',
286
                    [$articleId,  $detailId]
287
                );
288
            }
289
        }
290
    }
291
292
    /**
293
     * @param array $articleIds
294
     */
295
    public function activateArticles(array $articleIds)
296
    {
297
        $articleBuilder = $this->manager->createQueryBuilder();
298
        $articleBuilder->update('\Shopware\Models\Article\Article', 'a')
299
            ->set('a.active', 1)
300
            ->where('a.id IN (:articleIds)')
301
            ->setParameter(':articleIds', $articleIds, Connection::PARAM_STR_ARRAY);
302
303
        $articleBuilder->getQuery()->execute();
304
305
        $detailBuilder = $this->manager->createQueryBuilder();
306
        $detailBuilder->update('\Shopware\Models\Article\Detail', 'd')
307
            ->set('d.active', 1)
308
            ->where('d.articleId IN (:articleIds)')
309
            ->setParameter(':articleIds', $articleIds, Connection::PARAM_STR_ARRAY);
310
311
        $detailBuilder->getQuery()->execute();
312
    }
313
314
    /**
315
     * Store remote categories in Connect tables
316
     * and add relations between categories and products.
317
     *
318
     * @param array $remoteItems
319
     *
320
     * @throws \Exception
321
     */
322
    public function storeRemoteCategories(array $remoteItems)
323
    {
324
        $connection = $this->manager->getConnection();
325
326
        $connection->beginTransaction();
327
        try {
328
            foreach ($remoteItems as $articleId => $categories) {
329
                foreach ($categories as $categoryKey => $category) {
330
                    $connection->executeQuery(
331
                        'INSERT IGNORE INTO `s_plugin_connect_categories` (`category_key`, `label`) VALUES (?, ?)',
332
                        [$categoryKey, $category]
333
                    );
334
335
                    $connection->executeQuery(
336
                        'INSERT IGNORE INTO `s_plugin_connect_product_to_categories` (`connect_category_id`, `articleID`) VALUES ((SELECT c.id FROM s_plugin_connect_categories c WHERE c.category_key = ?), ?)',
337
                        [$categoryKey, $articleId]
338
                    );
339
                }
340
            }
341
            $connection->commit();
342
        } catch (\Exception $e) {
343
            $connection->rollBack();
344
345
            throw $e;
346
        }
347
    }
348
349
    /**
350
     * Fetch remote (Connect) categories by given article ids
351
     * @param array $articleIds
352
     * @return array
353
     */
354
    public function fetchRemoteCategoriesByArticleIds(array $articleIds)
355
    {
356
        $remoteCategoryIds = [];
357
        while ($currentIdBatch = array_splice($articleIds, 0, 500)) {
358
            $sql = 'SELECT sac.categoryID
359
            FROM s_articles_categories sac
360
            LEFT JOIN s_categories_attributes attr ON sac.categoryID = attr.categoryID
361
            WHERE attr.connect_imported_category = 1 AND sac.articleID IN (' . implode(', ', $currentIdBatch) . ') GROUP BY sac.categoryID';
362
            $rows = $this->manager->getConnection()->fetchAll($sql);
363
364
            $remoteCategoryIds = array_merge($remoteCategoryIds, array_map(function ($row) {
365
                return $row['categoryID'];
366
            }, $rows));
367
        }
368
369
        return array_unique($remoteCategoryIds);
370
    }
371
372
    /**
373
     * Fetch all articles where categories are auto imported
374
     * and there isn't record in s_plugin_connect_product_to_categories for them.
375
     * Returned array contains key = articleId and value = array of categories
376
     *
377
     * @return array
378
     */
379
    public function getArticlesWithAutoImportedCategories()
380
    {
381
        $statement = $this->manager->getConnection()->prepare(
382
            'SELECT b.article_id, b.category
383
            FROM s_plugin_connect_items b
384
            LEFT JOIN s_plugin_connect_product_to_categories a ON b.article_id = a.articleID
385
            WHERE b.shop_id > 0 AND a.connect_category_id IS NULL GROUP BY b.article_id'
386
        );
387
        $statement->execute();
388
389
        $remoteItems = [];
390
        foreach ($statement->fetchAll(\PDO::FETCH_ASSOC) as $item) {
391
            $categories = json_decode($item['category'], true);
392
            if (is_array($categories) && count($categories) > 0) {
393
                $articleId = $item['article_id'];
394
                $remoteItems[$articleId] = $categories;
395
            }
396
        }
397
398
        return $remoteItems;
399
    }
400
401
    /**
402
     * Helper function to create filter values
403
     * @param int $categoryId
404
     * @param bool $showOnlyConnectArticles
405
     * @param string $query
406
     * @return array
407
     */
408
    private function getAst($categoryId, $query = '', $showOnlyConnectArticles = true)
409
    {
410
        $ast = [
411
            [
412
                'type' => 'nullaryOperators',
413
                'token' => 'ISMAIN',
414
            ]
415
        ];
416
417
        if (trim($query) !== '') {
418
            $queryArray = [
419
                [
420
                    'type' => 'boolOperators',
421
                    'token' => 'AND',
422
                ],
423
                [
424
                    'type' => 'subOperators',
425
                    'token' => '(',
426
                ],
427
                [
428
                    'type' => 'attribute',
429
                    'token' => 'ARTICLE.NAME'
430
                ],
431
                [
432
                    'type' => 'binaryOperator',
433
                    'token' => '~'
434
                ],
435
                [
436
                    'type' => 'values',
437
                    'token' => '"' . $query . '"'
438
                ],
439
                [
440
                    'type' => 'boolOperators',
441
                    'token' => 'OR',
442
                ],
443
                [
444
                    'type' => 'attribute',
445
                    'token' => 'SUPPLIER.NAME'
446
                ],
447
                [
448
                    'type' => 'binaryOperator',
449
                    'token' => '~'
450
                ],
451
                [
452
                    'type' => 'values',
453
                    'token' => '"' . $query . '"'
454
                ],
455
                [
456
                    'type' => 'boolOperators',
457
                    'token' => 'OR',
458
                ],
459
                [
460
                    'type' => 'attribute',
461
                    'token' => 'DETAIL.NUMBER'
462
                ],
463
                [
464
                    'type' => 'binaryOperator',
465
                    'token' => '~'
466
                ],
467
                [
468
                    'type' => 'values',
469
                    'token' => '"' . $query . '"'
470
                ],
471
                [
472
                    'type' => 'subOperators',
473
                    'token' => ')',
474
                ]
475
            ];
476
            $ast = array_merge($ast, $queryArray);
477
        }
478
479
        $categoryArray = [
480
            [
481
            'type' => 'boolOperators',
482
            'token' => 'AND',
483
            ],
484
            [
485
                'type' => 'subOperators',
486
                'token' => '(',
487
            ],
488
            [
489
                'type' => 'attribute',
490
                'token' => 'CATEGORY.PATH',
491
            ],
492
            [
493
                'type' => 'binaryOperators',
494
                'token' => '=',
495
            ],
496
            [
497
                'type' => 'values',
498
                'token' => '"%|' . $categoryId . '|%"',
499
            ],
500
            [
501
                'type' => 'boolOperators',
502
                'token' => 'OR',
503
            ],
504
            [
505
                'type' => 'attribute',
506
                'token' => 'CATEGORY.ID',
507
            ],
508
            [
509
                'type' => 'binaryOperators',
510
                'token' => '=',
511
            ],
512
            [
513
                'type' => 'values',
514
                'token' => $categoryId,
515
            ],
516
            [
517
                'type' => 'subOperators',
518
                'token' => ')',
519
            ]
520
        ];
521
522
        $ast = array_merge($ast, $categoryArray);
523
524
        if ($showOnlyConnectArticles === true) {
525
            $ast = array_merge($ast, [
526
                [
527
                    'type' => 'boolOperators',
528
                    'token' => 'AND',
529
                ],
530
                [
531
                    'type' => 'attribute',
532
                    'token' => 'ATTRIBUTE.CONNECTMAPPEDCATEGORY',
533
                ],
534
                [
535
                    'type' => 'binaryOperators',
536
                    'token' => '!=',
537
                ],
538
                [
539
                    'type' => 'values',
540
                    'token' => 'NULL',
541
                ],
542
            ]);
543
        }
544
545
        return $ast;
546
    }
547
}
548