Completed
Push — master ( a801e4...e1e7bc )
by Jonas
05:25 queued 02:50
created

CategoryExtractor::extractNode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 12
rs 9.4285
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 Shopware\Connect\Gateway;
11
use Shopware\Models\Category\Category;
12
use Shopware\CustomModels\Connect\AttributeRepository;
13
use Enlight_Components_Db_Adapter_Pdo_Mysql as Pdo;
14
15
/**
16
 * Class CategoryExtractor
17
 * @package Shopware\CustomModels\Connect
18
 */
19
class CategoryExtractor
20
{
21
    /**
22
     * @var \Shopware\CustomModels\Connect\AttributeRepository
23
     */
24
    private $attributeRepository;
25
26
    /**
27
     * @var \ShopwarePlugins\Connect\Components\CategoryResolver
28
     */
29
    private $categoryResolver;
30
31
    /**
32
     * @var \Shopware\Connect\Gateway
33
     */
34
    private $configurationGateway;
35
36
    /**
37
     * @var \ShopwarePlugins\Connect\Components\RandomStringGenerator;
38
     */
39
    private $randomStringGenerator;
40
41
    /**
42
     * @var \Enlight_Components_Db_Adapter_Pdo_Mysql
43
     */
44
    private $db;
45
46
    /**
47
     * @param AttributeRepository $attributeRepository
48
     * @param CategoryResolver $categoryResolver
49
     * @param Gateway $configurationGateway
50
     * @param RandomStringGenerator $randomStringGenerator
51
     */
52
    public function __construct(
53
        AttributeRepository $attributeRepository,
54
        CategoryResolver $categoryResolver,
55
        Gateway $configurationGateway,
56
        RandomStringGenerator $randomStringGenerator,
57
        Pdo $db
58
    ) {
59
        $this->attributeRepository = $attributeRepository;
60
        $this->categoryResolver = $categoryResolver;
61
        $this->configurationGateway = $configurationGateway;
62
        $this->randomStringGenerator = $randomStringGenerator;
63
        $this->db = $db;
64
    }
65
66
    /**
67
     * Collects categories
68
     * from imported Shopware Connect products
69
     */
70
    public function extractImportedCategories()
71
    {
72
        $categories = [];
73
        /** @var \Shopware\CustomModels\Connect\Attribute $attribute */
74
        foreach ($this->attributeRepository->findRemoteArticleAttributes() as $attribute) {
75
            $categories = array_merge($categories, $attribute->getCategory());
76
        }
77
78
        return $this->convertTree($this->categoryResolver->generateTree($categories));
79
    }
80
81
    /**
82
     * @param Category $category
83
     * @return array
84
     */
85
    public function getCategoryIdsCollection(Category $category)
86
    {
87
        return $this->collectCategoryIds($category);
88
    }
89
90
    /**
91
     * Collects connect category ids
92
     *
93
     * @param Category $parentCategory
94
     * @param array|null $categoryIds
95
     * @return array
96
     */
97
    private function collectCategoryIds(Category $parentCategory, array $categoryIds = [])
98
    {
99
        //is connect category
100
        if ($parentCategory->getAttribute()->getConnectImportedCategory()) {
101
            $categoryIds[] = $parentCategory->getId();
102
        }
103
104
        foreach ($parentCategory->getChildren() as $category) {
105
            $categoryIds = $this->collectCategoryIds($category, $categoryIds);
106
        }
107
108
        return $categoryIds;
109
    }
110
111
    /**
112
     * Loads remote categories
113
     *
114
     * @param string|null $parent
115
     * @param bool|null $includeChildren
116
     * @param bool|null $excludeMapped
117
     * @param int|null $shopId
118
     * @param string|null $stream
119
     * @return array
120
     */
121
    public function getRemoteCategoriesTree($parent = null, $includeChildren = false, $excludeMapped = false, $shopId = null, $stream = null)
122
    {
123
        $sql = '
124
            SELECT pcc.category_key, pcc.label
125
            FROM s_plugin_connect_items pci
126
            INNER JOIN `s_plugin_connect_product_to_categories` pcptc ON pci.article_id = pcptc.articleID
127
            INNER JOIN `s_plugin_connect_categories` pcc ON pcptc.connect_category_id = pcc.id
128
        ';
129
130
        $whereParams = [];
131
        $whereSql = [];
132
133
        if ($shopId > 0) {
134
            $whereSql[] = 'pci.shop_id = ?';
135
            $whereParams[] = (string) $shopId;
136
        }
137
138
        if ($stream) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stream of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
139
            $whereSql[] = 'pci.stream = ?';
140
            $whereParams[] = $stream;
141
        }
142
143
        if ($parent !== null) {
144
            $whereSql[] = 'pcc.category_key LIKE ?';
145
            $whereParams[] = $parent . '/%';
146
        }
147
148
        if ($excludeMapped === true) {
149
            $sql .= ' INNER JOIN `s_articles_attributes` ar ON ar.articleDetailsID = pci.article_detail_id';
150
            $whereSql[] = 'ar.connect_mapped_category IS NULL';
151
        }
152
153
        if (count($whereSql) > 0) {
154
            $sql .= sprintf(' WHERE %s', implode(' AND ', $whereSql));
155
        }
156
157
        $rows = $this->db->fetchPairs($sql, $whereParams);
158
159
        $parent = $parent ?: '';
160
        // if parent is an empty string, filter only main categories, otherwise
161
        // filter only first child categories
162
        $rows = $this->convertTree(
163
            $this->categoryResolver->generateTree($rows, $parent),
164
            $includeChildren,
0 ignored issues
show
Bug introduced by
It seems like $includeChildren defined by parameter $includeChildren on line 121 can also be of type null; however, ShopwarePlugins\Connect\...xtractor::convertTree() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
165
            false,
166
            false,
167
            $shopId,
168
            $stream
169
        );
170
171
        return $rows;
172
    }
173
174
    /**
175
     * Collects remote categories by given stream and shopId
176
     *
177
     * @param string $stream
178
     * @param int $shopId
179
     * @param bool $hideMapped
180
     * @return array
181
     */
182
    public function getRemoteCategoriesTreeByStream($stream, $shopId, $hideMapped = false)
183
    {
184
        $sql = 'SELECT cat.category_key, cat.label
185
                FROM s_plugin_connect_items attributes
186
                INNER JOIN `s_plugin_connect_product_to_categories` prod_to_cat ON attributes.article_id = prod_to_cat.articleID
187
                INNER JOIN `s_plugin_connect_categories` cat ON prod_to_cat.connect_category_id = cat.id';
188
        $whereClause = ' WHERE attributes.shop_id = ? AND attributes.stream = ?';
189
        if ($hideMapped) {
190
            $sql .= ' INNER JOIN `s_articles_attributes` ar ON ar.articleDetailsID = attributes.article_detail_id';
191
            $whereClause .= ' AND ar.connect_mapped_category IS NULL';
192
        }
193
194
        $sql .= $whereClause;
195
        $rows = $this->db->fetchPairs($sql, [(int) $shopId, $stream]);
196
197
        return $this->convertTree($this->categoryResolver->generateTree($rows), false, false, false, $shopId, $stream);
198
    }
199
200
    /**
201
     * Collects supplier names as categories tree
202
     * @param null $excludeMapped
203
     * @param bool $expanded
204
     * @return array
205
     */
206
    public function getMainNodes($excludeMapped = null, $expanded = false)
207
    {
208
        // if parent is null collect shop names
209
        $shops = [];
210
        foreach ($this->configurationGateway->getConnectedShopIds() as $shopId) {
211
            if (!$this->hasShopItems($shopId, $excludeMapped)) {
0 ignored issues
show
Documentation introduced by
$excludeMapped is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
212
                continue;
213
            }
214
            $configuration = $this->configurationGateway->getShopConfiguration($shopId);
215
            $shops[$shopId] = [
216
                'name' => $configuration->displayName,
217
                'iconCls' => 'sc-tree-node-icon',
218
                'icon' => $configuration->logoUrl,
219
            ];
220
        }
221
222
        $tree = $this->convertTree($shops, false, $expanded);
223
        array_walk($tree, function (&$node) {
224
            $node['leaf'] = false;
225
        });
226
227
        return $tree;
228
    }
229
230
    /**
231
     * @param $shopId
232
     * @param bool $excludeMapped
233
     * @return bool
234
     */
235
    public function hasShopItems($shopId, $excludeMapped = false)
236
    {
237
        $sql = 'SELECT COUNT(pci.id)
238
                FROM `s_plugin_connect_items` pci
239
        ';
240
241
        $whereClause = ' WHERE pci.shop_id = ?';
242
243
        if ($excludeMapped === true) {
244
            $sql .= ' INNER JOIN `s_articles_attributes` aa ON aa.articleDetailsID = pci.article_detail_id';
245
            $whereClause .= ' AND aa.connect_mapped_category IS NULL';
246
        }
247
248
        $sql .= $whereClause;
249
250
        $count = $this->db->fetchOne($sql, [(string) $shopId]);
251
252
        return $count > 0;
253
    }
254
255
    /**
256
     * Collects categories from products
257
     * by given shopId
258
     *
259
     * @param int $shopId
260
     * @param bool $includeChildren
261
     * @return array
262
     */
263
    public function extractByShopId($shopId, $includeChildren = false)
264
    {
265
        $sql = 'SELECT category_key, label
266
                FROM `s_plugin_connect_categories` cat
267
                INNER JOIN `s_plugin_connect_product_to_categories` prod_to_cat ON cat.id = prod_to_cat.connect_category_id
268
                INNER JOIN `s_plugin_connect_items` attributes ON prod_to_cat.articleID = attributes.article_id
269
                WHERE attributes.shop_id = ?';
270
        $rows = $this->db->fetchPairs($sql, [$shopId]);
271
272
        return $this->convertTree($this->categoryResolver->generateTree($rows), $includeChildren);
273
    }
274
275
    public function getStreamsByShopId($shopId)
276
    {
277
        $sql = 'SELECT DISTINCT(stream)
278
                FROM `s_plugin_connect_items` attributes
279
                WHERE attributes.shop_id = ?';
280
        $rows = $this->db->fetchCol($sql, [(string) $shopId]);
281
282
        $streams = [];
283 View Code Duplication
        foreach ($rows as $streamName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
284
            $id = sprintf('%s_stream_%s', $shopId, $streamName);
285
            $streams[$id] = [
286
                'name' => $streamName,
287
                'iconCls' => 'sprite-product-streams',
288
            ];
289
        }
290
291
        $tree = $this->convertTree($streams, false);
292
        array_walk($tree, function (&$node) {
293
            $node['leaf'] = false;
294
        });
295
296
        return $tree;
297
    }
298
299
    public function getNodesByQuery($hideMapped, $query, $parent, $node)
300
    {
301
        switch ($parent) {
302
            case 'root':
303
                $categories = $this->getMainNodes($hideMapped, true);
304
                break;
305
            case is_numeric($parent):
306
                $categories = $this->getQueryStreams($parent, $query, $hideMapped);
307
                break;
308
            case strpos($parent, '_stream_') > 0:
309
                list($shopId, $stream) = explode('_stream_', $parent);
310
                $categories = $this->getMainCategoriesByQuery($shopId, $stream, $query, $hideMapped);
311
                break;
312
            default:
313
                // given id must have following structure:
314
                // shopId5~stream~AwesomeProducts~/english/boots/nike
315
                // shopId is required parameter to fetch all child categories of this parent
316
                list($shopId, $stream) = $this->extractNode($node);
317
                $categories = $this->getChildrenCategoriesByQuery($parent, $query, $hideMapped, $shopId, $stream);
318
        }
319
320
        return $categories;
321
    }
322
323
    /**
324
     * Returns shopId and stream from given node,
325
     * it must have specific format.
326
     *
327
     * shopId5~stream~AwesomeProducts~/english/boots/nike
328
     *
329
     * Used in remote category tree to identify which category
330
     * to which shopId and stream belongs.
331
     *
332
     * Returned array has following structure
333
     * [ shopId, stream]
334
     *
335
     * @param string $node
336
     * @throws \InvalidArgumentException
337
     * @return array
338
     */
339
    public function extractNode($node)
340
    {
341
        preg_match('/^(shopId(\d+)~)(stream~(.*)~)(.*)$/', $node, $matches);
342
        if (empty($matches)) {
343
            throw new \InvalidArgumentException('Node must contain shopId and stream');
344
        }
345
346
        return [
347
            $matches[2],
348
            $matches[4]
349
        ];
350
    }
351
352
    /**
353
     * @param $shopId
354
     * @param $query
355
     * @param $hideMapped
356
     * @param int $shopId
357
     * @return array
358
     */
359
    public function getQueryStreams($shopId, $query, $hideMapped)
360
    {
361
        $rows = $this->getQueryCategories($query, $shopId, null, $hideMapped);
362
363
        if (count($rows) === 0) {
364
            return [];
365
        }
366
367
        $sql = 'SELECT DISTINCT(attributes.stream)
368
                FROM `s_plugin_connect_categories` cat
369
                INNER JOIN `s_plugin_connect_product_to_categories` prod_to_cat ON cat.id = prod_to_cat.connect_category_id
370
                INNER JOIN `s_plugin_connect_items` attributes ON prod_to_cat.articleID = attributes.article_id
371
                WHERE attributes.shop_id = ?  AND (';
372
373
        $params = [$shopId];
374
        foreach ($rows as $categoryKey => $label) {
375
            if ($categoryKey !== reset(array_keys($rows))) {
0 ignored issues
show
Bug introduced by
array_keys($rows) cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
376
                $sql .= ' OR ';
377
            }
378
379
            $sql .= ' cat.category_key = ?';
380
            $params[] = $categoryKey;
381
        }
382
383
        $sql .= ' )';
384
        $rows = $this->db->fetchCol($sql, $params);
385
        $streams = [];
386
387 View Code Duplication
        foreach ($rows as $streamName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
388
            $id = sprintf('%s_stream_%s', $shopId, $streamName);
389
            $streams[$id] = [
390
                'name' => $streamName,
391
                'iconCls' => 'sprite-product-streams',
392
            ];
393
        }
394
395
        $tree = $this->convertTree($streams, false, true);
396
        array_walk($tree, function (&$node) {
397
            $node['leaf'] = false;
398
        });
399
400
        return $tree;
401
    }
402
403
    /**
404
     * @param $shopId
405
     * @param $stream
406
     * @param $query
407
     * @param $hideMapped
408
     * @return array
409
     */
410
    public function getMainCategoriesByQuery($shopId, $stream, $query, $hideMapped)
411
    {
412
        $rows = $this->getQueryCategories($query, $shopId, $stream, $hideMapped);
413
414
        $rootCategories = [];
415
416
        foreach ($rows as $key => $name) {
417
            $position = strpos($key, '/', 1);
418
419
            if ($position === false) {
420
                $rootCategory = $key;
421
            } else {
422
                $rootCategory = substr($key, 0, $position);
423
            }
424
425
            if (!in_array($rootCategory, $rootCategories)) {
426
                $rootCategories[] = $rootCategory;
427
            }
428
        }
429
430
        if (count($rootCategories) === 0) {
431
            return [];
432
        }
433
434
        $sql = 'SELECT DISTINCT(category_key), label
435
                FROM `s_plugin_connect_categories` cat
436
                INNER JOIN `s_plugin_connect_product_to_categories` prod_to_cat ON cat.id = prod_to_cat.connect_category_id
437
                INNER JOIN `s_plugin_connect_items` attributes ON prod_to_cat.articleID = attributes.article_id
438
                WHERE attributes.shop_id = ? AND attributes.stream = ?  AND (';
439
440
        $params = [$shopId, $stream];
441
442
        foreach ($rootCategories as $item) {
443
            if ($item !== $rootCategories[0]) {
444
                $sql .= ' OR ';
445
            }
446
447
            $sql .= ' cat.category_key LIKE ?';
448
            $params[] = $item . '%';
449
        }
450
451
        $sql .= ' )';
452
453
        $rows = $this->db->fetchPairs($sql, $params);
454
455
        return $this->convertTree($this->categoryResolver->generateTree($rows), false, true, false, $shopId, $stream);
456
    }
457
458
    public function getChildrenCategoriesByQuery($parent, $query, $hideMapped, $shopId, $stream)
459
    {
460
        $rows = $this->getQueryCategories($query, $shopId, $stream, $hideMapped, $parent);
461
462
        $parents = $this->getUniqueParents($rows, $parent);
463
464
        $categoryKeys = array_unique(array_merge(array_keys($rows), $parents));
465
466
        $result = $this->getCategoryNames($categoryKeys);
467
468
        return $this->convertTree($this->categoryResolver->generateTree($result, $parent), false, true, true, $shopId, $stream);
469
    }
470
471
    public function getUniqueParents($rows, $parent)
472
    {
473
        $parents = [];
474
475
        foreach ($rows as $key => $name) {
476
            $position = strrpos($key, '/', 1);
477
478
            if ($position === false) {
479
                continue;
480
            }
481
482
            while ($position !== strlen($parent)) {
483
                $newParent = substr($key, 0, $position);
484
                $position = strrpos($newParent, '/', 1);
485
486
                if ($position === false) {
487
                    break;
488
                }
489
490
                if (!in_array($newParent, $parents)) {
491
                    $parents[] = $newParent;
492
                }
493
            }
494
        }
495
496
        return $parents;
497
    }
498
499
    public function getCategoryNames($categoryKeys)
500
    {
501
        if (count($categoryKeys) === 0) {
502
            return [];
503
        }
504
505
        $params = [];
506
507
        $sql = 'SELECT category_key, label
508
                FROM `s_plugin_connect_categories` cat';
509
510
        foreach ($categoryKeys as $categoryKey) {
511
            if ($categoryKey === $categoryKeys[0]) {
512
                $sql .= ' WHERE cat.category_key = ?';
513
            } else {
514
                $sql .= ' OR cat.category_key = ?';
515
            }
516
            $params[] = $categoryKey;
517
        }
518
519
        $rows = $this->db->fetchPairs($sql, $params);
520
521
        return $rows;
522
    }
523
524
    /**
525
     * Converts categories tree structure
526
     * to be usable in ExtJS tree
527
     *
528
     * @param array $tree
529
     * @return array
530
     */
531
    private function convertTree(array $tree, $includeChildren = true, $expanded = false, $checkLeaf = false, $shopId = null, $stream = null)
532
    {
533
        $categories = [];
534
        foreach ($tree as $id => $node) {
535
            $children = [];
536
            if ($includeChildren === true && !empty($node['children'])) {
537
                $children = $this->convertTree($node['children'], $includeChildren);
538
            }
539
540
            if (strlen($node['name']) === 0) {
541
                continue;
542
            }
543
544
            $prefix = '';
545
            if ($shopId > 0) {
546
                $prefix .= sprintf('shopId%s~', $shopId);
547
            }
548
549
            if ($stream) {
550
                $prefix .= sprintf('stream~%s~', $stream);
551
            }
552
553
            $category = [
554
                'name' => $node['name'],
555
                'id' => $this->randomStringGenerator->generate($prefix . $id),
556
                'categoryId' => $id,
557
                'leaf' => empty($node['children']) ? true : false,
558
                'children' => $children,
559
                'cls' => 'sc-tree-node',
560
                'expanded' => $expanded
561
            ];
562
563
            if ($checkLeaf && $category['leaf'] == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
564
                $category['leaf'] = $this->isLeaf($id);
565
            }
566
567
            if (isset($node['iconCls'])) {
568
                $category['iconCls'] = $node['iconCls'];
569
            }
570
571
            if (isset($node['icon'])) {
572
                $category['icon'] = $node['icon'];
573
            }
574
575
            $categories[] = $category;
576
        }
577
578
        return $categories;
579
    }
580
581
    public function getQueryCategories($query, $shopId, $stream = null, $excludeMapped = false, $parent = '')
582
    {
583
        $sql = 'SELECT category_key, label
584
                FROM `s_plugin_connect_categories` cat
585
                INNER JOIN `s_plugin_connect_product_to_categories` prod_to_cat ON cat.id = prod_to_cat.connect_category_id
586
                INNER JOIN `s_plugin_connect_items` attributes ON prod_to_cat.articleID = attributes.article_id
587
                INNER JOIN `s_articles_attributes` ar ON ar.articleID = attributes.article_id
588
                WHERE cat.label LIKE ? AND cat.category_key LIKE ? AND attributes.shop_id = ?';
589
        $whereParams = [
590
            '%' . $query . '%',
591
            $parent . '%',
592
            $shopId,
593
        ];
594
595
        if ($excludeMapped === true) {
596
            $sql .= ' AND ar.connect_mapped_category IS NULL';
597
        }
598
599
        if ($stream) {
600
            $sql .= '  AND attributes.stream = ?';
601
            $whereParams[] = $stream;
602
        }
603
604
        $rows = $this->db->fetchPairs($sql, $whereParams);
605
606
        return $rows;
607
    }
608
609
    public function isLeaf($categoryId)
610
    {
611
        $sql = 'SELECT COUNT(id)
612
                FROM `s_plugin_connect_categories` cat
613
                WHERE cat.category_key LIKE ?';
614
615
        $count = $this->db->fetchOne($sql, [$categoryId . '/%']);
616
617
        return $count == 0;
618
    }
619
}
620