Completed
Pull Request — master (#415)
by Jonas
06:56 queued 03:47
created

ProductToShop::cleanUpConfiguratorSet()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 2
dl 0
loc 9
rs 9.6666
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\Bundle\SearchBundle\Sorting\ReleaseDateSorting;
11
use Shopware\Connect\Gateway;
12
use Shopware\Components\Model\CategoryDenormalization;
13
use Shopware\Connect\ProductToShop as ProductToShopBase;
14
use Shopware\Connect\Struct\OrderStatus;
15
use Shopware\Connect\Struct\Product;
16
use Shopware\Models\Article\Article as ProductModel;
17
use Shopware\Models\Order\Order;
18
use Shopware\Models\Category\Category;
19
use Shopware\Models\Order\Status;
20
use Shopware\Models\Article\Detail as DetailModel;
21
use Shopware\Models\Attribute\Article as AttributeModel;
22
use Shopware\Components\Model\ModelManager;
23
use Shopware\Connect\Struct\PriceRange;
24
use Shopware\Connect\Struct\ProductUpdate;
25
use Shopware\CustomModels\Connect\ProductStreamAttribute;
26
use Shopware\Models\Customer\Group;
27
use Shopware\Connect\Struct\Property;
28
use Shopware\Models\ProductStream\ProductStream;
29
use Shopware\Models\Property\Group as PropertyGroup;
30
use Shopware\Models\Property\Option as PropertyOption;
31
use Shopware\Models\Property\Value as PropertyValue;
32
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamRepository;
33
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamService;
34
use ShopwarePlugins\Connect\Components\Translations\LocaleMapper;
35
use ShopwarePlugins\Connect\Components\Gateway\ProductTranslationsGateway;
36
use ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway;
37
use ShopwarePlugins\Connect\Components\Utils\UnitMapper;
38
use Shopware\CustomModels\Connect\Attribute as ConnectAttribute;
39
use Shopware\Models\Article\Image;
40
use Shopware\Models\Article\Supplier;
41
use Shopware\Models\Tax\Tax;
42
43
/**
44
 * The interface for products imported *from* connect *to* the local shop
45
 *
46
 * @category  Shopware
47
 * @package   Shopware\Plugins\SwagConnect
48
 */
49
class ProductToShop implements ProductToShopBase
50
{
51
    const RELATION_TYPE_RELATED = 'relationships';
52
    const RELATION_TYPE_SIMILAR = 'similar';
53
54
    /**
55
     * @var Helper
56
     */
57
    private $helper;
58
59
    /**
60
     * @var ModelManager
61
     */
62
    private $manager;
63
64
    /**
65
     * @var \ShopwarePlugins\Connect\Components\Config
66
     */
67
    private $config;
68
69
    /**
70
     * @var ImageImport
71
     */
72
    private $imageImport;
73
74
    /**
75
     * @var \ShopwarePlugins\Connect\Components\VariantConfigurator
76
     */
77
    private $variantConfigurator;
78
79
    /**
80
     * @var MarketplaceGateway
81
     */
82
    private $marketplaceGateway;
83
84
    /**
85
     * @var ProductTranslationsGateway
86
     */
87
    private $productTranslationsGateway;
88
89
    /**
90
     * @var \Shopware\Models\Shop\Repository
91
     */
92
    private $shopRepository;
93
94
    private $localeRepository;
95
96
    /**
97
     * @var CategoryResolver
98
     */
99
    private $categoryResolver;
100
101
    /**
102
     * @var \Shopware\Connect\Gateway
103
     */
104
    private $connectGateway;
105
106
    /**
107
     * @var \Enlight_Event_EventManager
108
     */
109
    private $eventManager;
110
111
    /**
112
     * @var CategoryDenormalization
113
     */
114
    private $categoryDenormalization;
115
116
    /**
117
     * @param Helper $helper
118
     * @param ModelManager $manager
119
     * @param ImageImport $imageImport
120
     * @param \ShopwarePlugins\Connect\Components\Config $config
121
     * @param VariantConfigurator $variantConfigurator
122
     * @param \ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway $marketplaceGateway
123
     * @param ProductTranslationsGateway $productTranslationsGateway
124
     * @param CategoryResolver $categoryResolver
125
     * @param Gateway $connectGateway
126
     * @param \Enlight_Event_EventManager $eventManager
127
     * @param CategoryDenormalization $categoryDenormalization
128
     */
129
    public function __construct(
130
        Helper $helper,
131
        ModelManager $manager,
132
        ImageImport $imageImport,
133
        Config $config,
134
        VariantConfigurator $variantConfigurator,
135
        MarketplaceGateway $marketplaceGateway,
136
        ProductTranslationsGateway $productTranslationsGateway,
137
        CategoryResolver $categoryResolver,
138
        Gateway $connectGateway,
139
        \Enlight_Event_EventManager $eventManager,
140
        CategoryDenormalization $categoryDenormalization
141
    ) {
142
        $this->helper = $helper;
143
        $this->manager = $manager;
144
        $this->config = $config;
145
        $this->imageImport = $imageImport;
146
        $this->variantConfigurator = $variantConfigurator;
147
        $this->marketplaceGateway = $marketplaceGateway;
148
        $this->productTranslationsGateway = $productTranslationsGateway;
149
        $this->categoryResolver = $categoryResolver;
150
        $this->connectGateway = $connectGateway;
151
        $this->eventManager = $eventManager;
152
        $this->categoryDenormalization = $categoryDenormalization;
153
    }
154
155
    /**
156
     * Start transaction
157
     *
158
     * Starts a transaction, which includes all insertOrUpdate and delete
159
     * operations, as well as the revision updates.
160
     *
161
     * @return void
162
     */
163
    public function startTransaction()
164
    {
165
        $this->manager->getConnection()->beginTransaction();
166
    }
167
168
    /**
169
     * Commit transaction
170
     *
171
     * Commits the transactions, once all operations are queued.
172
     *
173
     * @return void
174
     */
175
    public function commit()
176
    {
177
        $this->manager->getConnection()->commit();
178
    }
179
180
    /**
181
     * Import or update given product
182
     *
183
     * Store product in your shop database as an external product. The
184
     * associated sourceId
185
     *
186
     * @param Product $product
187
     */
188
    public function insertOrUpdate(Product $product)
189
    {
190
        /** @var Product $product */
191
        $product = $this->eventManager->filter(
192
            'Connect_ProductToShop_InsertOrUpdate_Before',
193
            $product
194
        );
195
196
        // todo@dn: Set dummy values and make product inactive
197
        if (empty($product->title) || empty($product->vendor)) {
198
            return;
199
        }
200
201
        $number = $this->generateSKU($product);
202
203
        $detail = $this->helper->getArticleDetailModelByProduct($product);
204
        $detail = $this->eventManager->filter(
205
            'Connect_Merchant_Get_Article_Detail_After',
206
            $detail,
207
            [
208
                'product' => $product,
209
                'subject' => $this
210
            ]
211
        );
212
213
        $isMainVariant = false;
214
        if ($detail === null) {
215
            $active = $this->config->getConfig('activateProductsAutomatically', false) ? true : false;
216
217
            $model = $this->getSWProductModel($product, $active, $isMainVariant);
218
219
            $detail = $this->generateNewDetail($product, $model);
220
        } else {
221
            /** @var ProductModel $model */
222
            $model = $detail->getArticle();
223
            // fix for isMainVariant flag
224
            // in connect attribute table
225
            $mainDetail = $model->getMainDetail();
226
            $isMainVariant = $this->checkIfMainVariant($detail, $mainDetail);
227
228
            $this->updateConfiguratorSetTypeFromProduct($model, $product);
229
230
            $this->cleanUpConfiguratorSet($model, $product);
231
        }
232
233
        $detail->setNumber($number);
234
235
        $detailAttribute = $this->getOrCreateAttributeModel($detail, $model);
236
237
        $connectAttribute = $this->helper->getConnectAttributeByModel($detail) ?: new ConnectAttribute;
238
        // configure main variant and groupId
239
        if ($isMainVariant === true) {
240
            $connectAttribute->setIsMainVariant(true);
241
        }
242
        $connectAttribute->setGroupId($product->groupId);
243
244
        list($updateFields, $flag) = $this->getUpdateFields($model, $detail, $connectAttribute, $product);
245
        $this->setPropertiesForNewProducts($updateFields, $model, $detailAttribute, $product);
246
247
        $this->saveVat($product, $model);
248
249
        $this->applyProductProperties($model, $product);
250
251
        $detailAttribute = $this->applyMarketplaceAttributes($detailAttribute, $product);
252
253
        $this->setConnectAttributesFromProduct($connectAttribute, $product);
254
255
        // store product categories to connect attribute
256
        $connectAttribute->setCategory($product->categories);
257
258
        $connectAttribute->setLastUpdateFlag($flag);
259
260
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
261
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
262
263
        $this->updateDetailFromProduct($detail, $product);
264
265
        // some shops have feature "sell not in stock",
266
        // then end customer should be able to by the product with stock = 0
267
        $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
268
        if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
269
            $model->setLastStock(false);
270
        } else {
271
            $model->setLastStock(true);
272
        }
273
274
        $this->detailSetUnit($detail, $product, $detailAttribute);
275
276
        $this->detailSetAttributes($detail, $product);
277
278
        $this->connectAttributeSetLastUpdate($connectAttribute, $product);
279
280
        if ($model->getMainDetail() === null) {
281
            $model->setMainDetail($detail);
282
        }
283
284
        if ($detail->getAttribute() === null) {
285
            $detail->setAttribute($detailAttribute);
286
            $detailAttribute->setArticle($model);
287
        }
288
289
        $connectAttribute->setArticle($model);
290
        $connectAttribute->setArticleDetail($detail);
291
292
        $this->eventManager->notify(
293
            'Connect_Merchant_Saving_ArticleAttribute_Before',
294
            [
295
                'subject' => $this,
296
                'connectAttribute' => $connectAttribute
297
            ]
298
        );
299
300
        //article has to be flushed
301
        $this->manager->persist($model);
302
        $this->manager->persist($connectAttribute);
303
        $this->manager->persist($detail);
304
        $this->manager->flush();
305
306
        $this->categoryResolver->storeRemoteCategories($product->categories, $model->getId(), $product->shopId);
307
        $categories = $this->categoryResolver->resolve($product->categories, $product->shopId);
308
        if (count($categories) > 0) {
309
            $detailAttribute->setConnectMappedCategory(true);
310
        }
311
312
        $this->manager->persist($detailAttribute);
313
        $this->manager->flush();
314
315
        $this->categoryDenormalization($model, $categories);
316
317
        $defaultCustomerGroup = $this->helper->getDefaultCustomerGroup();
318
        // Only set prices, if fixedPrice is active or price updates are configured
319
        if (count($detail->getPrices()) == 0 || $connectAttribute->getFixedPrice() || $updateFields['price']) {
320
            $this->setPrice($model, $detail, $product);
321
        }
322
        // If the price is not being update, update the purchasePrice anyway
323
        $this->setPurchasePrice($detail, $product->purchasePrice, $defaultCustomerGroup);
324
325
        $this->manager->clear();
326
327
        $this->addArticleTranslations($model, $product);
328
329
        if ($isMainVariant || $product->groupId === null) {
330
            $this->applyCrossSelling($model->getId(), $product);
331
        }
332
333
        //clear cache for that article
334
        $this->helper->clearArticleCache($model->getId());
335
336
        if ($updateFields['image']) {
337
            // Reload the model in order to not to work an the already flushed model
338
            $model = $this->helper->getArticleModelByProduct($product);
339
            // import only global images for article
340
            $this->imageImport->importImagesForArticle(array_diff($product->images, $product->variantImages), $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by $this->helper->getArticleModelByProduct($product) on line 338 can be null; however, ShopwarePlugins\Connect\...mportImagesForArticle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
341
            if ($updateFields['mainImage'] && isset($product->images[0])) {
342
                $this->imageImport->importMainImage($product->images[0], $model->getId());
343
            }
344
            // Reload the article detail model in order to not to work an the already flushed model
345
            $detail = $this->helper->getArticleDetailModelByProduct($product);
346
            // import only specific images for variant
347
            $this->imageImport->importImagesForDetail($product->variantImages, $detail);
0 ignored issues
show
Bug introduced by
It seems like $detail defined by $this->helper->getArticl...odelByProduct($product) on line 345 can be null; however, ShopwarePlugins\Connect\...importImagesForDetail() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
348
        }
349
350
        $this->eventManager->notify(
351
            'Connect_ProductToShop_InsertOrUpdate_After',
352
            [
353
                'connectProduct' => $product,
354
                'shopArticleDetail' => $detail
355
            ]
356
        );
357
358
        $stream = $this->getOrCreateStream($product);
359
        $this->addProductToStream($stream, $model);
0 ignored issues
show
Bug introduced by
It seems like $model can be null; however, addProductToStream() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
360
    }
361
362
    /**
363
     * @param Product $product
364
     * @return string
365
     */
366
    private function generateSKU(Product $product)
367
    {
368
        if (!empty($product->sku)) {
369
            $number = 'SC-' . $product->shopId . '-' . $product->sku;
370
            $duplicatedDetail = $this->helper->getDetailByNumber($number);
371
            if ($duplicatedDetail
372
                && $this->helper->getConnectAttributeByModel($duplicatedDetail)->getSourceId() != $product->sourceId
373
            ) {
374
                $this->deleteDetail($duplicatedDetail);
375
            }
376
        } else {
377
            $number = 'SC-' . $product->shopId . '-' . $product->sourceId;
378
        }
379
380
        return $number;
381
    }
382
383
    /**
384
     * @param DetailModel $detailModel
385
     */
386
    private function deleteDetail(DetailModel $detailModel)
387
    {
388
        $this->eventManager->notify(
389
            'Connect_Merchant_Delete_Product_Before',
390
            [
391
                'subject' => $this,
392
                'articleDetail' => $detailModel
393
            ]
394
        );
395
396
        $article = $detailModel->getArticle();
397
        // Not sure why, but the Attribute can be NULL
398
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
399
        $this->manager->remove($detailModel);
400
401
        if ($attribute) {
402
            $this->manager->remove($attribute);
403
        }
404
405
        // if removed variant is main variant
406
        // find first variant which is not main and mark it
407
        if ($detailModel->getKind() === 1) {
408
            /** @var \Shopware\Models\Article\Detail $variant */
409
            foreach ($article->getDetails() as $variant) {
410
                if ($variant->getId() != $detailModel->getId()) {
411
                    $variant->setKind(1);
412
                    $article->setMainDetail($variant);
413
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
414
                    if (!$connectAttribute) {
415
                        continue;
416
                    }
417
                    $connectAttribute->setIsMainVariant(true);
418
                    $this->manager->persist($connectAttribute);
419
                    $this->manager->persist($article);
420
                    $this->manager->persist($variant);
421
                    break;
422
                }
423
            }
424
        }
425
426
        if (count($details = $article->getDetails()) === 1) {
427
            $details->clear();
428
            $this->manager->remove($article);
429
        }
430
431
        //save category Ids before flush
432
        $oldCategoryIds = array_map(function ($category) {
433
            return $category->getId();
434
        }, $article->getCategories()->toArray());
435
436
        // Do not remove flush. It's needed when remove article,
437
        // because duplication of ordernumber. Even with remove before
438
        // persist calls mysql throws exception "Duplicate entry"
439
        $this->manager->flush();
440
        // always clear entity manager, because $article->getDetails() returns
441
        // more than 1 detail, but all of them were removed except main one.
442
        $this->manager->clear();
443
444
        // call this after flush because article has to be deleted that this works
445
        if (count($oldCategoryIds) > 0) {
446
            $this->categoryResolver->deleteEmptyConnectCategories($oldCategoryIds);
447
        }
448
    }
449
450
    /**
451
     * @param Product $product
452
     * @param $active
453
     * @param $isMainVariant
454
     * @return null|Article
455
     */
456
    private function getSWProductModel(Product $product, $active, &$isMainVariant)
457
    {
458
        if ($product->groupId !== null) {
459
            $model = $this->helper->getArticleByRemoteProduct($product);
460
            if (!$model instanceof \Shopware\Models\Article\Article) {
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Article\Article does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
461
                $model = $this->helper->createProductModel($product);
462
                $model->setActive($active);
463
                $isMainVariant = true;
464
            }
465
        } else {
466
            $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
467
            if (!$model instanceof \Shopware\Models\Article\Article) {
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Article\Article does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
468
                $model = $this->helper->createProductModel($product);
469
                $model->setActive($active);
470
            }
471
        }
472
473
        return $model;
474
    }
475
476
    /**
477
     * @param Product $product
478
     * @param $model
479
     * @return DetailModel
480
     */
481
    private function generateNewDetail(Product $product, $model)
482
    {
483
        $detail = new DetailModel();
484
        $detail->setActive($model->getActive());
485
        $this->manager->persist($detail);
486
        $detail->setArticle($model);
487
        $model->getDetails()->add($detail);
488
        if (!empty($product->variant)) {
489
            $this->variantConfigurator->configureVariantAttributes($product, $detail);
490
        }
491
492
        return $detail;
493
    }
494
495
    /**
496
     * @param DetailModel $detail
497
     * @param DetailModel $mainDetail
498
     * @return bool
499
     */
500
    private function checkIfMainVariant(DetailModel $detail, DetailModel $mainDetail)
501
    {
502
        return $detail->getId() === $mainDetail->getId();
503
    }
504
505
    /**
506
     * @param ProductModel $model
507
     * @param Product $product
508
     */
509
    private function updateConfiguratorSetTypeFromProduct(ProductModel $model, Product $product)
510
    {
511
        if (!empty($product->variant)) {
512
            $configSet = $model->getConfiguratorSet();
513
            $configSet->setType($product->configuratorSetType);
514
        }
515
    }
516
517
    /**
518
     * @param ProductModel $model
519
     * @param Product $product
520
     */
521
    private function cleanUpConfiguratorSet(ProductModel $model, Product $product)
522
    {
523
        if (empty($product->variant) && $model->getConfiguratorSet()) {
524
            $this->manager->getConnection()->executeQuery(
525
                'UPDATE s_articles SET configurator_set_id = NULL WHERE id = ?',
526
                [$model->getId()]
527
            );
528
        }
529
    }
530
531
    /**
532
     * @param DetailModel $detail
533
     * @param ProductModel $model
534
     * @return AttributeModel
535
     */
536
    private function getOrCreateAttributeModel(DetailModel $detail, ProductModel $model)
537
    {
538
        $detailAttribute = $detail->getAttribute();
539
        if (!$detailAttribute) {
540
            $detailAttribute = new AttributeModel();
541
            $detail->setAttribute($detailAttribute);
542
            $model->setAttribute($detailAttribute);
543
            $detailAttribute->setArticle($model);
544
            $detailAttribute->setArticleDetail($detail);
545
        }
546
547
        return $detailAttribute;
548
    }
549
550
    /**
551
     * Get array of update info for the known fields
552
     *
553
     * @param $model
554
     * @param $detail
555
     * @param $attribute
556
     * @param $product
557
     * @return array
558
     */
559
    public function getUpdateFields($model, $detail, $attribute, $product)
560
    {
561
        // This also defines the flags of these fields
562
        $fields = $this->helper->getUpdateFlags();
563
        $flagsByName = array_flip($fields);
564
565
        $flag = 0;
566
        $output = [];
567
        foreach ($fields as $key => $field) {
568
            // Don't handle the imageInitialImport flag
569
            if ($field == 'imageInitialImport') {
570
                continue;
571
            }
572
573
            // If this is a new product
574
            if (!$model->getId() && $field == 'image' && !$this->config->getConfig('importImagesOnFirstImport',
575
                    false)) {
576
                $output[$field] = false;
577
                $flag |= $flagsByName['imageInitialImport'];
578
                continue;
579
            }
580
581
            $updateAllowed = $this->isFieldUpdateAllowed($field, $model, $attribute);
582
            $output[$field] = $updateAllowed;
583
            if (!$updateAllowed && $this->hasFieldChanged($field, $model, $detail, $product)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $updateAllowed of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
584
                $flag |= $key;
585
            }
586
        }
587
588
        return [$output, $flag];
589
    }
590
591
    /**
592
     * Helper method to determine if a given $fields may/must be updated.
593
     * This method will check for the model->id in order to determine, if it is a new entity. Therefore
594
     * this method cannot be used after the model in question was already flushed.
595
     *
596
     * @param $field
597
     * @param $model ProductModel
598
     * @param $attribute ConnectAttribute
599
     * @throws \RuntimeException
600
     * @return bool|null
601
     */
602
    public function isFieldUpdateAllowed($field, ProductModel $model, ConnectAttribute $attribute)
603
    {
604
        $allowed = [
605
            'ShortDescription',
606
            'LongDescription',
607
            'AdditionalDescription',
608
            'Image',
609
            'Price',
610
            'Name',
611
            'MainImage',
612
        ];
613
614
        // Always allow updates for new models
615
        if (!$model->getId()) {
616
            return true;
617
        }
618
619
        $field = ucfirst($field);
620
        $attributeGetter = 'getUpdate' . $field;
621
        $configName = 'overwriteProduct' . $field;
622
623
        if (!in_array($field, $allowed)) {
624
            throw new \RuntimeException("Unknown update field {$field}");
625
        }
626
627
        $attributeValue = $attribute->$attributeGetter();
628
629
630
        // If the value is 'null' or 'inherit', the behaviour will be inherited from the global configuration
631
        // Once we have a supplier based configuration, we need to take it into account here
632
        if ($attributeValue == null || $attributeValue == 'inherit') {
633
            return $this->config->getConfig($configName, true);
634
        }
635
636
        return $attributeValue == 'overwrite';
637
    }
638
639
    /**
640
     * Determine if a given field has changed
641
     *
642
     * @param $field
643
     * @param ProductModel $model
644
     * @param DetailModel $detail
645
     * @param Product $product
646
     * @return bool
647
     */
648
    public function hasFieldChanged($field, ProductModel $model, DetailModel $detail, Product $product)
649
    {
650
        switch ($field) {
651
            case 'shortDescription':
652
                return $model->getDescription() != $product->shortDescription;
653
            case 'longDescription':
654
                return $model->getDescriptionLong() != $product->longDescription;
655
            case 'additionalDescription':
656
                return $detail->getAttribute()->getConnectProductDescription() != $product->additionalDescription;
657
            case 'name':
658
                return $model->getName() != $product->title;
659
            case 'image':
660
                return count($model->getImages()) != count($product->images);
661
            case 'mainImage':
662
                if ($product->images[0]) {
663
                    return $this->imageImport->hasMainImageChanged($product->images[0], $model->getId());
664
                }
665
666
                return false;
667
            case 'price':
668
                $prices = $detail->getPrices();
669
                if (empty($prices)) {
670
                    return true;
671
                }
672
                $price = $prices->first();
673
                if (!$price) {
674
                    return true;
675
                }
676
677
                return $prices->first()->getPrice() != $product->price;
678
        }
679
680
        throw new \InvalidArgumentException('Unrecognized field');
681
    }
682
683
    /**
684
     * @param array $updateFields
685
     * @param ProductModel $model
686
     * @param AttributeModel $detailAttribute
687
     * @param Product $product
688
     */
689
    private function setPropertiesForNewProducts(array $updateFields, ProductModel $model, AttributeModel $detailAttribute, Product $product)
690
    {
691
        /*
692
         * Make sure, that the following properties are set for
693
         * - new products
694
         * - products that have been configured to receive these updates
695
         */
696
        if ($updateFields['name']) {
697
            $model->setName($product->title);
698
        }
699
        if ($updateFields['shortDescription']) {
700
            $model->setDescription($product->shortDescription);
701
        }
702
        if ($updateFields['longDescription']) {
703
            $model->setDescriptionLong($product->longDescription);
704
        }
705
706
        if ($updateFields['additionalDescription']) {
707
            $detailAttribute->setConnectProductDescription($product->additionalDescription);
708
        }
709
710
        if ($product->vat !== null) {
711
            $repo = $this->manager->getRepository('Shopware\Models\Tax\Tax');
712
            $tax = round($product->vat * 100, 2);
713
            /** @var \Shopware\Models\Tax\Tax $tax */
714
            $tax = $repo->findOneBy(['tax' => $tax]);
715
            $model->setTax($tax);
716
        }
717
718
        if ($product->vendor !== null) {
719
            $repo = $this->manager->getRepository('Shopware\Models\Article\Supplier');
720
            $supplier = $repo->findOneBy(['name' => $product->vendor]);
721
            if ($supplier === null) {
722
                $supplier = $this->createSupplier($product->vendor);
723
            }
724
            $model->setSupplier($supplier);
725
        }
726
    }
727
728
    /**
729
     * @param $vendor
730
     * @return Supplier
731
     */
732
    private function createSupplier($vendor)
733
    {
734
        $supplier = new Supplier();
735
736
        if (is_array($vendor)) {
737
            $supplier->setName($vendor['name']);
738
            $supplier->setDescription($vendor['description']);
739
            if (array_key_exists('url', $vendor) && $vendor['url']) {
740
                $supplier->setLink($vendor['url']);
741
            }
742
743
            $supplier->setMetaTitle($vendor['page_title']);
744
745
            if (array_key_exists('logo_url', $vendor) && $vendor['logo_url']) {
746
                $this->imageImport->importImageForSupplier($vendor['logo_url'], $supplier);
747
            }
748
        } else {
749
            $supplier->setName($vendor);
750
        }
751
752
        //sets supplier attributes
753
        $attr = new \Shopware\Models\Attribute\ArticleSupplier();
754
        $attr->setConnectIsRemote(true);
755
756
        $supplier->setAttribute($attr);
757
758
        return $supplier;
759
    }
760
761
    /**
762
     * @param ProductModel $article
763
     * @param Product $product
764
     */
765
    private function applyProductProperties(ProductModel $article, Product $product)
766
    {
767
        if (empty($product->properties)) {
768
            return;
769
        }
770
771
        /** @var Property $firstProperty */
772
        $firstProperty = reset($product->properties);
773
        $groupRepo = $this->manager->getRepository(PropertyGroup::class);
774
        $group = $groupRepo->findOneBy(['name' => $firstProperty->groupName]);
775
776
        if (!$group) {
777
            $group = $this->createPropertyGroup($firstProperty);
778
        }
779
780
        $propertyValues = $article->getPropertyValues();
781
        $propertyValues->clear();
782
        $this->manager->persist($article);
783
        $this->manager->flush();
784
785
        $article->setPropertyGroup($group);
786
787
        $optionRepo = $this->manager->getRepository(PropertyOption::class);
788
        $valueRepo = $this->manager->getRepository(PropertyValue::class);
789
790
        foreach ($product->properties as $property) {
791
            $option = $optionRepo->findOneBy(['name' => $property->option]);
792
            $optionExists = $option instanceof PropertyOption;
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Property\Option does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
793
            if (!$option) {
794
                $option = new PropertyOption();
795
                $option->setName($property->option);
796
                $option->setFilterable($property->filterable);
797
798
                $attribute = new \Shopware\Models\Attribute\PropertyOption();
799
                $attribute->setPropertyOption($option);
800
                $attribute->setConnectIsRemote(true);
801
                $option->setAttribute($attribute);
802
803
                $this->manager->persist($option);
804
                $this->manager->flush($option);
805
            }
806
807
            if (!$optionExists || !$value = $valueRepo->findOneBy(['option' => $option, 'value' => $property->value])) {
808
                $value = new PropertyValue($option, $property->value);
809
                $value->setPosition($property->valuePosition);
810
811
                $attribute = new \Shopware\Models\Attribute\PropertyValue();
812
                $attribute->setPropertyValue($value);
813
                $attribute->setConnectIsRemote(true);
814
                $value->setAttribute($attribute);
815
816
                $this->manager->persist($value);
817
            }
818
819
            if (!$propertyValues->contains($value)) {
820
                //add only new values
821
                $propertyValues->add($value);
822
            }
823
824
            $filters = [
825
                ['property' => 'options.name', 'expression' => '=', 'value' => $property->option],
826
                ['property' => 'groups.name', 'expression' => '=', 'value' => $property->groupName],
827
            ];
828
829
            $query = $groupRepo->getPropertyRelationQuery($filters, null, 1, 0);
830
            $relation = $query->getOneOrNullResult();
831
832
            if (!$relation) {
833
                $group->addOption($option);
834
                $this->manager->persist($group);
835
                $this->manager->flush($group);
836
            }
837
        }
838
839
        $article->setPropertyValues($propertyValues);
840
841
        $this->manager->persist($article);
842
        $this->manager->flush();
843
    }
844
845
    /**
846
     * Read product attributes mapping and set to shopware attribute model
847
     *
848
     * @param AttributeModel $detailAttribute
849
     * @param Product $product
850
     * @return AttributeModel
851
     */
852
    private function applyMarketplaceAttributes(AttributeModel $detailAttribute, Product $product)
853
    {
854
        $detailAttribute->setConnectReference($product->sourceId);
855
        $detailAttribute->setConnectArticleShipping($product->shipping);
856
        //todo@sb: check if connectAttribute matches position of the marketplace attribute
857
        array_walk($product->attributes, function ($value, $key) use ($detailAttribute) {
858
            $shopwareAttribute = $this->marketplaceGateway->findShopwareMappingFor($key);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $shopwareAttribute is correct as $this->marketplaceGatewa...hopwareMappingFor($key) (which targets ShopwarePlugins\Connect\...indShopwareMappingFor()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
859
            if (strlen($shopwareAttribute) > 0) {
860
                $setter = 'set' . ucfirst($shopwareAttribute);
861
                $detailAttribute->$setter($value);
862
            }
863
        });
864
865
        return $detailAttribute;
866
    }
867
868
    /**
869
     * @param  ConnectAttribute $connectAttribute
870
     * @param Product $product
871
     */
872
    private function setConnectAttributesFromProduct(ConnectAttribute $connectAttribute, Product $product)
873
    {
874
        $connectAttribute->setShopId($product->shopId);
875
        $connectAttribute->setSourceId($product->sourceId);
876
        $connectAttribute->setExportStatus(null);
877
        $connectAttribute->setPurchasePrice($product->purchasePrice);
878
        $connectAttribute->setFixedPrice($product->fixedPrice);
879
        $connectAttribute->setStream($product->stream);
880
881
        // store purchasePriceHash and offerValidUntil
882
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
883
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
884
    }
885
886
    /**
887
     * @param DetailModel $detail
888
     * @param Product $product
889
     */
890
    private function updateDetailFromProduct(DetailModel $detail, Product $product)
891
    {
892
        $detail->setInStock($product->availability);
893
        $detail->setEan($product->ean);
894
        $detail->setShippingTime($product->deliveryWorkDays);
895
        $releaseDate = new \DateTime();
896
        $releaseDate->setTimestamp($product->deliveryDate);
897
        $detail->setReleaseDate($releaseDate);
898
        $detail->setMinPurchase($product->minPurchaseQuantity);
899
    }
900
901
    /**
902
     * @param DetailModel $detail
903
     * @param Product $product
904
     * @param $detailAttribute
905
     */
906
    private function detailSetUnit(DetailModel $detail, Product $product, $detailAttribute)
907
    {
908
        // if connect product has unit
909
        // find local unit with units mapping
910
        // and add to detail model
911
        if (array_key_exists('unit', $product->attributes) && $product->attributes['unit']) {
912
            $detailAttribute->setConnectRemoteUnit($product->attributes['unit']);
913
            if ($this->config->getConfig($product->attributes['unit']) == null) {
914
                $this->config->setConfig($product->attributes['unit'], '', null, 'units');
915
            }
916
917
            /** @var \ShopwarePlugins\Connect\Components\Utils\UnitMapper $unitMapper */
918
            $unitMapper = new UnitMapper($this->config, $this->manager);
919
920
            $shopwareUnit = $unitMapper->getShopwareUnit($product->attributes['unit']);
921
922
            /** @var \Shopware\Models\Article\Unit $unit */
923
            $unit = $this->helper->getUnit($shopwareUnit);
924
            $detail->setUnit($unit);
925
            $detail->setPurchaseUnit($product->attributes['quantity']);
926
            $detail->setReferenceUnit($product->attributes['ref_quantity']);
927
        } else {
928
            $detail->setUnit(null);
929
            $detail->setPurchaseUnit(null);
930
            $detail->setReferenceUnit(null);
931
        }
932
    }
933
934
    /**
935
     * @param DetailModel $detail
936
     * @param Product $product
937
     */
938
    private function detailSetAttributes(DetailModel $detail, Product $product)
939
    {
940
        // set dimension
941
        if (array_key_exists('dimension', $product->attributes) && $product->attributes['dimension']) {
942
            $dimension = explode('x', $product->attributes['dimension']);
943
            $detail->setLen($dimension[0]);
944
            $detail->setWidth($dimension[1]);
945
            $detail->setHeight($dimension[2]);
946
        } else {
947
            $detail->setLen(null);
948
            $detail->setWidth(null);
949
            $detail->setHeight(null);
950
        }
951
952
        // set weight
953
        if (array_key_exists('weight', $product->attributes) && $product->attributes['weight']) {
954
            $detail->setWeight($product->attributes['weight']);
955
        }
956
957
        //set package unit
958
        if (array_key_exists(Product::ATTRIBUTE_PACKAGEUNIT, $product->attributes)) {
959
            $detail->setPackUnit($product->attributes[Product::ATTRIBUTE_PACKAGEUNIT]);
960
        }
961
962
        //set basic unit
963
        if (array_key_exists(Product::ATTRIBUTE_BASICUNIT, $product->attributes)) {
964
            $detail->setMinPurchase($product->attributes[Product::ATTRIBUTE_BASICUNIT]);
965
        }
966
967
        //set manufacturer no.
968
        if (array_key_exists(Product::ATTRIBUTE_MANUFACTURERNUMBER, $product->attributes)) {
969
            $detail->setSupplierNumber($product->attributes[Product::ATTRIBUTE_MANUFACTURERNUMBER]);
970
        }
971
    }
972
973
    /**
974
     * @param ConnectAttribute $connectAttribute
975
     * @param Product $product
976
     */
977
    private function connectAttributeSetLastUpdate(ConnectAttribute $connectAttribute, Product $product)
978
    {
979
        // Whenever a product is updated, store a json encoded list of all fields that are updated optionally
980
        // This way a customer will be able to apply the most recent changes any time later
981
        $connectAttribute->setLastUpdate(json_encode([
982
            'shortDescription' => $product->shortDescription,
983
            'longDescription' => $product->longDescription,
984
            'additionalDescription' => $product->additionalDescription,
985
            'purchasePrice' => $product->purchasePrice,
986
            'image' => $product->images,
987
            'variantImages' => $product->variantImages,
988
            'price' => $product->price * ($product->vat + 1),
989
            'name' => $product->title,
990
            'vat' => $product->vat
991
        ]));
992
    }
993
994
    /**
995
     * @param ProductModel $model
996
     * @param array $categories
997
     */
998
    private function categoryDenormalization(ProductModel $model, array $categories)
999
    {
1000
        $this->categoryDenormalization->disableTransactions();
1001
        foreach ($categories as $category) {
1002
            $this->categoryDenormalization->addAssignment($model->getId(), $category);
1003
            $this->manager->getConnection()->executeQuery(
1004
                'INSERT IGNORE INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES (?,?)',
1005
                [$model->getId(), $category]
1006
            );
1007
            $parentId =$this->manager->getConnection()->fetchColumn(
1008
                'SELECT parent FROM `s_categories` WHERE id = ?',
1009
                [$category]
1010
            );
1011
            $this->categoryDenormalization->removeAssignment($model->getId(), $parentId);
1012
            $this->manager->getConnection()->executeQuery(
1013
                'DELETE FROM `s_articles_categories` WHERE `articleID` = ? AND `categoryID` = ?',
1014
                [$model->getId(), $parentId]
1015
            );
1016
        }
1017
        $this->categoryDenormalization->enableTransactions();
1018
    }
1019
1020
    /**
1021
     * @param Product $product
1022
     * @return ProductStream
1023
     */
1024
    private function getOrCreateStream(Product $product)
1025
    {
1026
        /** @var ProductStreamRepository $repo */
1027
        $repo = $this->manager->getRepository(ProductStreamAttribute::class);
1028
        $stream = $repo->findConnectByName($product->stream);
1029
1030
        if (!$stream) {
1031
            $stream = new ProductStream();
1032
            $stream->setName($product->stream);
1033
            $stream->setType(ProductStreamService::STATIC_STREAM);
1034
            $stream->setSorting(json_encode(
1035
                [ReleaseDateSorting::class => ['direction' => 'desc']]
1036
            ));
1037
1038
            //add attributes
1039
            $attribute = new \Shopware\Models\Attribute\ProductStream();
1040
            $attribute->setProductStream($stream);
1041
            $attribute->setConnectIsRemote(true);
1042
            $stream->setAttribute($attribute);
1043
1044
            $this->manager->persist($attribute);
1045
            $this->manager->persist($stream);
1046
            $this->manager->flush();
1047
        }
1048
1049
        return $stream;
1050
    }
1051
1052
    /**
1053
     * @param ProductStream $stream
1054
     * @param ProductModel $article
1055
     * @throws \Doctrine\DBAL\DBALException
1056
     */
1057
    private function addProductToStream(ProductStream $stream, ProductModel $article)
1058
    {
1059
        $conn = $this->manager->getConnection();
1060
        $sql = 'INSERT INTO `s_product_streams_selection` (`stream_id`, `article_id`)
1061
                VALUES (:streamId, :articleId)
1062
                ON DUPLICATE KEY UPDATE stream_id = :streamId, article_id = :articleId';
1063
        $stmt = $conn->prepare($sql);
1064
        $stmt->execute([':streamId' => $stream->getId(), ':articleId' => $article->getId()]);
1065
    }
1066
1067
    /**
1068
     * Set detail purchase price with plain SQL
1069
     * Entity usage throws exception when error handlers are disabled
1070
     *
1071
     * @param ProductModel $article
1072
     * @param DetailModel $detail
1073
     * @param Product $product
1074
     * @throws \Doctrine\DBAL\DBALException
1075
     */
1076
    private function setPrice(ProductModel $article, DetailModel $detail, Product $product)
1077
    {
1078
        // set price via plain SQL because shopware throws exception
1079
        // undefined index: key when error handler is disabled
1080
        $customerGroup = $this->helper->getDefaultCustomerGroup();
1081
1082
        if (!empty($product->priceRanges)) {
1083
            $this->setPriceRange($article, $detail, $product->priceRanges, $customerGroup);
1084
1085
            return;
1086
        }
1087
1088
        $id = $this->manager->getConnection()->fetchColumn(
1089
            'SELECT id FROM `s_articles_prices`
1090
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1091
            [$customerGroup->getKey(), 1, 'beliebig', $article->getId(), $detail->getId()]
1092
        );
1093
1094
        // todo@sb: test update prices
1095
        if ($id > 0) {
1096
            $this->manager->getConnection()->executeQuery(
1097
                'UPDATE `s_articles_prices` SET `price` = ?, `baseprice` = ? WHERE `id` = ?',
1098
                [$product->price, $product->purchasePrice, $id]
1099
            );
1100
        } else {
1101
            $this->manager->getConnection()->executeQuery(
1102
                'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `baseprice`)
1103
              VALUES (?, 1, "beliebig", ?, ?, ?, ?);',
1104
                [
1105
                    $customerGroup->getKey(),
1106
                    $article->getId(),
1107
                    $detail->getId(),
1108
                    $product->price,
1109
                    $product->purchasePrice
1110
                ]
1111
            );
1112
        }
1113
    }
1114
1115
    /**
1116
     * @param ProductModel $article
1117
     * @param DetailModel $detail
1118
     * @param array $priceRanges
1119
     * @param Group $group
1120
     * @throws \Doctrine\DBAL\ConnectionException
1121
     * @throws \Exception
1122
     */
1123
    private function setPriceRange(ProductModel $article, DetailModel $detail, array $priceRanges, Group $group)
1124
    {
1125
        $this->manager->getConnection()->beginTransaction();
1126
1127
        try {
1128
            // We always delete the prices,
1129
            // because we can not know which record is update
1130
            $this->manager->getConnection()->executeQuery(
1131
                'DELETE FROM `s_articles_prices` WHERE `articleID` = ? AND `articledetailsID` = ?',
1132
                [$article->getId(), $detail->getId()]
1133
            );
1134
1135
            /** @var PriceRange $priceRange */
1136
            foreach ($priceRanges as $priceRange) {
1137
                $priceTo = $priceRange->to == PriceRange::ANY ? 'beliebig' : $priceRange->to;
1138
1139
                //todo: maybe batch insert if possible?
1140
                $this->manager->getConnection()->executeQuery(
1141
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`)
1142
                      VALUES (?, ?, ?, ?, ?, ?);',
1143
                    [
1144
                        $group->getKey(),
1145
                        $priceRange->from,
1146
                        $priceTo,
1147
                        $article->getId(),
1148
                        $detail->getId(),
1149
                        $priceRange->price
1150
                    ]
1151
                );
1152
            }
1153
            $this->manager->getConnection()->commit();
1154
        } catch (\Exception $e) {
1155
            $this->manager->getConnection()->rollBack();
1156
            throw new \Exception($e->getMessage());
1157
        }
1158
    }
1159
1160
    /**
1161
     * Set detail purchase price with plain SQL
1162
     * Entity usage throws exception when error handlers are disabled
1163
     *
1164
     * @param DetailModel $detail
1165
     * @param float $purchasePrice
1166
     * @param Group $defaultGroup
1167
     * @throws \Doctrine\DBAL\DBALException
1168
     */
1169
    private function setPurchasePrice(DetailModel $detail, $purchasePrice, Group $defaultGroup)
1170
    {
1171
        if (method_exists($detail, 'setPurchasePrice')) {
1172
            $this->manager->getConnection()->executeQuery(
1173
                'UPDATE `s_articles_details` SET `purchaseprice` = ? WHERE `id` = ?',
1174
                [$purchasePrice, $detail->getId()]
1175
            );
1176
        } else {
1177
            $id = $this->manager->getConnection()->fetchColumn(
1178
                'SELECT id FROM `s_articles_prices`
1179
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1180
                [$defaultGroup->getKey(), 1, 'beliebig', $detail->getArticleId(), $detail->getId()]
1181
            );
1182
1183
            if ($id > 0) {
1184
                $this->manager->getConnection()->executeQuery(
1185
                    'UPDATE `s_articles_prices` SET `baseprice` = ? WHERE `id` = ?',
1186
                    [$purchasePrice, $id]
1187
                );
1188
            } else {
1189
                $this->manager->getConnection()->executeQuery(
1190
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `baseprice`)
1191
              VALUES (?, 1, "beliebig", ?, ?, ?);',
1192
                    [$defaultGroup->getKey(), $detail->getArticleId(), $detail->getId(), $purchasePrice]
1193
                );
1194
            }
1195
        }
1196
    }
1197
1198
    /**
1199
     * Adds translation record for given article
1200
     *
1201
     * @param ProductModel $article
1202
     * @param Product $sdkProduct
1203
     */
1204
    private function addArticleTranslations(ProductModel $article, Product $sdkProduct)
1205
    {
1206
        /** @var \Shopware\Connect\Struct\Translation $translation */
1207
        foreach ($sdkProduct->translations as $key => $translation) {
1208
            /** @var \Shopware\Models\Shop\Locale $locale */
1209
            $locale = $this->getLocaleRepository()->findOneBy(['locale' => LocaleMapper::getShopwareLocale($key)]);
1210
            /** @var \Shopware\Models\Shop\Shop $shop */
1211
            $shop = $this->getShopRepository()->findOneBy(['locale' => $locale]);
1212
            if (!$shop) {
1213
                continue;
1214
            }
1215
1216
            $this->productTranslationsGateway->addArticleTranslation($translation, $article->getId(), $shop->getId());
1217
        }
1218
    }
1219
1220
    /**
1221
     * dsadsa
1222
     * @return \Shopware\Components\Model\ModelRepository
1223
     */
1224
    private function getLocaleRepository()
1225
    {
1226
        if (!$this->localeRepository) {
1227
            $this->localeRepository = $this->manager->getRepository('Shopware\Models\Shop\Locale');
1228
        }
1229
1230
        return $this->localeRepository;
1231
    }
1232
1233
    private function getShopRepository()
1234
    {
1235
        if (!$this->shopRepository) {
1236
            $this->shopRepository = $this->manager->getRepository('Shopware\Models\Shop\Shop');
1237
        }
1238
1239
        return $this->shopRepository;
1240
    }
1241
1242
    /**
1243
     * Delete product or product variant with given shopId and sourceId.
1244
     *
1245
     * Only the combination of both identifies a product uniquely. Do NOT
1246
     * delete products just by their sourceId.
1247
     *
1248
     * You might receive delete requests for products, which are not available
1249
     * in your shop. Just ignore them.
1250
     *
1251
     * @param string $shopId
1252
     * @param string $sourceId
1253
     * @return void
1254
     */
1255
    public function delete($shopId, $sourceId)
1256
    {
1257
        $detail = $this->helper->getArticleDetailModelByProduct(new Product([
1258
            'shopId' => $shopId,
1259
            'sourceId' => $sourceId,
1260
        ]));
1261
        if ($detail === null) {
1262
            return;
1263
        }
1264
1265
        $this->deleteDetail($detail);
1266
    }
1267
1268
    public function update($shopId, $sourceId, ProductUpdate $product)
1269
    {
1270
        // find article detail id
1271
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1272
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1273
            [$sourceId, $shopId]
1274
        );
1275
1276
        $this->eventManager->notify(
1277
            'Connect_Merchant_Update_GeneralProductInformation',
1278
            [
1279
                'subject' => $this,
1280
                'shopId' => $shopId,
1281
                'sourceId' => $sourceId,
1282
                'articleDetailId' => $articleDetailId
1283
            ]
1284
        );
1285
1286
        // update purchasePriceHash, offerValidUntil and purchasePrice in connect attribute
1287
        $this->manager->getConnection()->executeUpdate(
1288
            'UPDATE s_plugin_connect_items SET purchase_price_hash = ?, offer_valid_until = ?, purchase_price = ?
1289
            WHERE source_id = ? AND shop_id = ?',
1290
            [
1291
                $product->purchasePriceHash,
1292
                $product->offerValidUntil,
1293
                $product->purchasePrice,
1294
                $sourceId,
1295
                $shopId,
1296
            ]
1297
        );
1298
1299
        // update stock in article detail
1300
        // update prices
1301
        // if purchase price is stored in article detail
1302
        // update it together with stock
1303
        // since shopware 5.2
1304
        if (method_exists('Shopware\Models\Article\Detail', 'getPurchasePrice')) {
1305
            $this->manager->getConnection()->executeUpdate(
1306
                'UPDATE s_articles_details SET instock = ?, purchaseprice = ? WHERE id = ?',
1307
                [$product->availability, $product->purchasePrice, $articleDetailId]
1308
            );
1309
        } else {
1310
            $this->manager->getConnection()->executeUpdate(
1311
                'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1312
                [$product->availability, $articleDetailId]
1313
            );
1314
        }
1315
        $this->manager->getConnection()->executeUpdate(
1316
            "UPDATE s_articles_prices SET price = ?, baseprice = ? WHERE articledetailsID = ? AND pricegroup = 'EK'",
1317
            [$product->price, $product->purchasePrice, $articleDetailId]
1318
        );
1319
    }
1320
1321
    public function changeAvailability($shopId, $sourceId, $availability)
1322
    {
1323
        // find article detail id
1324
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1325
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1326
            [$sourceId, $shopId]
1327
        );
1328
1329
        $this->eventManager->notify(
1330
            'Connect_Merchant_Update_GeneralProductInformation',
1331
            [
1332
                'subject' => $this,
1333
                'shopId' => $shopId,
1334
                'sourceId' => $sourceId,
1335
                'articleDetailId' => $articleDetailId
1336
            ]
1337
        );
1338
1339
        // update stock in article detail
1340
        $this->manager->getConnection()->executeUpdate(
1341
            'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1342
            [$availability, $articleDetailId]
1343
        );
1344
    }
1345
1346
    /**
1347
     * @inheritDoc
1348
     */
1349
    public function makeMainVariant($shopId, $sourceId, $groupId)
1350
    {
1351
        //find article detail which should be selected as main one
1352
        $newMainDetail = $this->helper->getConnectArticleDetailModel($sourceId, $shopId);
1353
        if (!$newMainDetail) {
1354
            return;
1355
        }
1356
1357
        /** @var \Shopware\Models\Article\Article $article */
1358
        $article = $newMainDetail->getArticle();
1359
1360
        $this->eventManager->notify(
1361
            'Connect_Merchant_Update_ProductMainVariant_Before',
1362
            [
1363
                'subject' => $this,
1364
                'shopId' => $shopId,
1365
                'sourceId' => $sourceId,
1366
                'articleId' => $article->getId(),
1367
                'articleDetailId' => $newMainDetail->getId()
1368
            ]
1369
        );
1370
1371
        // replace current main detail with new one
1372
        $currentMainDetail = $article->getMainDetail();
1373
        $currentMainDetail->setKind(2);
1374
        $newMainDetail->setKind(1);
1375
        $article->setMainDetail($newMainDetail);
1376
1377
        $this->manager->persist($newMainDetail);
1378
        $this->manager->persist($currentMainDetail);
1379
        $this->manager->persist($article);
1380
        $this->manager->flush();
1381
    }
1382
1383
    /**
1384
     * Updates the status of an Order
1385
     *
1386
     * @param string $localOrderId
1387
     * @param string $orderStatus
1388
     * @param string $trackingNumber
1389
     * @return void
1390
     */
1391
    public function updateOrderStatus($localOrderId, $orderStatus, $trackingNumber)
1392
    {
1393
        if ($this->config->getConfig('updateOrderStatus') == 1) {
1394
            $this->updateDeliveryStatus($localOrderId, $orderStatus);
1395
        }
1396
1397
        if ($trackingNumber) {
1398
            $this->updateTrackingNumber($localOrderId, $trackingNumber);
1399
        }
1400
    }
1401
1402
    /**
1403
     * @param string $localOrderId
1404
     * @param string $orderStatus
1405
     */
1406
    private function updateDeliveryStatus($localOrderId, $orderStatus)
1407
    {
1408
        $status = false;
1409
        if ($orderStatus === OrderStatus::STATE_IN_PROCESS) {
1410
            $status = Status::ORDER_STATE_PARTIALLY_DELIVERED;
1411
        } elseif ($orderStatus === OrderStatus::STATE_DELIVERED) {
1412
            $status = Status::ORDER_STATE_COMPLETELY_DELIVERED;
1413
        }
1414
1415
        if ($status) {
1416
            $this->manager->getConnection()->executeQuery(
1417
                'UPDATE s_order 
1418
                SET status = :orderStatus
1419
                WHERE ordernumber = :orderNumber',
1420
                [
1421
                    ':orderStatus' => $status,
1422
                    ':orderNumber' => $localOrderId
1423
                ]
1424
            );
1425
        }
1426
    }
1427
1428
    /**
1429
     * @param string $localOrderId
1430
     * @param string $trackingNumber
1431
     */
1432
    private function updateTrackingNumber($localOrderId, $trackingNumber)
1433
    {
1434
        $currentTrackingCode = $this->manager->getConnection()->fetchColumn(
1435
            'SELECT trackingcode
1436
            FROM s_order
1437
            WHERE ordernumber = :orderNumber',
1438
            [
1439
                ':orderNumber' => $localOrderId
1440
            ]
1441
        );
1442
1443
        if (!$currentTrackingCode) {
1444
            $newTracking = $trackingNumber;
1445
        } else {
1446
            $newTracking = $this->combineTrackingNumbers($trackingNumber, $currentTrackingCode);
1447
        }
1448
1449
        $this->manager->getConnection()->executeQuery(
1450
            'UPDATE s_order 
1451
            SET trackingcode = :trackingCode
1452
            WHERE ordernumber = :orderNumber',
1453
            [
1454
                ':trackingCode' => $newTracking,
1455
                ':orderNumber' => $localOrderId
1456
            ]
1457
        );
1458
    }
1459
1460
    /**
1461
     * @param string $newTrackingCode
1462
     * @param string $currentTrackingCode
1463
     * @return string
1464
     */
1465
    private function combineTrackingNumbers($newTrackingCode, $currentTrackingCode)
1466
    {
1467
        $currentTrackingCodes = $this->getTrackingNumberAsArray($currentTrackingCode);
1468
        $newTrackingCodes = $this->getTrackingNumberAsArray($newTrackingCode);
1469
        $newTrackingCodes = array_unique(array_merge($currentTrackingCodes, $newTrackingCodes));
1470
        $newTracking = implode(',', $newTrackingCodes);
1471
1472
        return $newTracking;
1473
    }
1474
1475
    /**
1476
     * @param string $trackingCode
1477
     * @return string[]
1478
     */
1479
    private function getTrackingNumberAsArray($trackingCode)
1480
    {
1481
        if (strpos($trackingCode, ',') !== false) {
1482
            return explode(',', $trackingCode);
1483
        }
1484
1485
        return [$trackingCode];
1486
    }
1487
1488
    /**
1489
     * @param Product $product
1490
     * @param ProductModel $model
1491
     */
1492
    private function saveVat(Product $product, ProductModel $model)
1493
    {
1494
        if ($product->vat !== null) {
1495
            $repo = $this->manager->getRepository(Tax::class);
1496
            $taxRate = round($product->vat * 100, 2);
1497
            /** @var \Shopware\Models\Tax\Tax $tax */
1498
            $tax = $repo->findOneBy(['tax' => $taxRate]);
1499
            if (!$tax) {
1500
                $tax = new Tax();
1501
                $tax->setTax($taxRate);
1502
                //this is to get rid of zeroes behind the decimal point
1503
                $name = strval(round($taxRate, 2)) . '%';
1504
                $tax->setName($name);
1505
                $this->manager->persist($tax);
1506
            }
1507
            $model->setTax($tax);
1508
        }
1509
    }
1510
1511
    /**
1512
     * @param int $articleId
1513
     * @param Product $product
1514
     */
1515
    private function applyCrossSelling($articleId, Product $product)
1516
    {
1517
        $this->deleteRemovedRelations($articleId, $product);
1518
        $this->storeCrossSellingInformationInverseSide($articleId, $product->sourceId, $product->shopId);
1519
        if ($product->similar || $product->related) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $product->similar of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $product->related of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1520
            $this->storeCrossSellingInformationOwningSide($articleId, $product);
1521
        }
1522
    }
1523
1524
    /**
1525
     * @param int $articleId
1526
     * @param Product $product
1527
     */
1528
    private function storeCrossSellingInformationOwningSide($articleId, $product)
1529
    {
1530
        foreach ($product->related as $relatedId) {
1531
            $this->insertNewRelations($articleId, $product->shopId, $relatedId, self::RELATION_TYPE_RELATED);
1532
        }
1533
1534
        foreach ($product->similar as $similarId) {
1535
            $this->insertNewRelations($articleId, $product->shopId, $similarId, self::RELATION_TYPE_SIMILAR);
1536
        }
1537
    }
1538
1539
    /**
1540
     * @param int $articleId
1541
     * @param int $shopId
1542
     * @param int $relatedId
1543
     * @param string $relationType
1544
     */
1545
    private function insertNewRelations($articleId, $shopId, $relatedId, $relationType)
1546
    {
1547
        $inserted = false;
1548
        try {
1549
            $this->manager->getConnection()->executeQuery('INSERT INTO s_plugin_connect_article_relations (article_id, shop_id, related_article_local_id, relationship_type) VALUES (?, ?, ?, ?)',
1550
                [$articleId, $shopId, $relatedId, $relationType]);
1551
            $inserted = true;
1552
        } catch (\Doctrine\DBAL\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...
1553
            // No problems here. Just means that the row already existed.
1554
        }
1555
1556
        //outside of try catch because we don't want to catch exceptions -> this method should not throw any
1557
        if ($inserted) {
1558
            $relatedLocalId = $this->manager->getConnection()->fetchColumn('SELECT article_id FROM s_plugin_connect_items WHERE shop_id = ? AND source_id = ?',
1559
                [$shopId, $relatedId]);
1560
            if ($relatedLocalId) {
1561
                $this->manager->getConnection()->executeQuery("INSERT IGNORE INTO s_articles_$relationType (articleID, relatedarticle) VALUES (?, ?)",
1562
                    [$articleId, $relatedLocalId]);
1563
            }
1564
        }
1565
    }
1566
1567
    /**
1568
     * @param int $articleId
1569
     * @param string $sourceId
1570
     * @param int $shopId
1571
     */
1572
    private function storeCrossSellingInformationInverseSide($articleId, $sourceId, $shopId)
1573
    {
1574
        $relatedArticles = $this->manager->getConnection()->fetchAll('SELECT article_id, relationship_type FROM s_plugin_connect_article_relations WHERE shop_id = ? AND related_article_local_id = ?',
1575
            [$shopId, $sourceId]);
1576
1577
        foreach ($relatedArticles as $relatedArticle) {
1578
            $relationType = $relatedArticle['relationship_type'];
1579
            $this->manager->getConnection()->executeQuery("INSERT IGNORE INTO s_articles_$relationType (articleID, relatedarticle) VALUES (?, ?)",
1580
                [$relatedArticle['article_id'], $articleId]);
1581
        }
1582
    }
1583
1584
    /**
1585
     * @param $articleId
1586
     * @param $product
1587
     */
1588
    private function deleteRemovedRelations($articleId, $product)
1589
    {
1590 View Code Duplication
        if (count($product->related) > 0) {
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...
1591
            $this->manager->getConnection()->executeQuery('DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND related_article_local_id NOT IN (?) AND relationship_type = ?',
1592
                [$articleId, $product->shopId, $product->related, self::RELATION_TYPE_RELATED],
1593
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR]);
1594
1595
            $oldRelatedIds = $this->manager->getConnection()->executeQuery(
1596
                'SELECT ar.id 
1597
                FROM s_articles_relationships AS ar
1598
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1599
                WHERE ar.articleID = ? AND ci.shop_id = ? AND ci.source_id NOT IN (?)',
1600
                [$articleId, $product->shopId, $product->related],
1601
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY])
1602
                ->fetchAll(\PDO::FETCH_COLUMN);
1603
        } else {
1604
            $this->manager->getConnection()->executeQuery('DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND relationship_type = ?',
1605
                [$articleId, $product->shopId, self::RELATION_TYPE_RELATED]);
1606
1607
            $oldRelatedIds = $this->manager->getConnection()->executeQuery(
1608
                'SELECT ar.id 
1609
                FROM s_articles_relationships AS ar
1610
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1611
                WHERE ar.articleID = ? AND ci.shop_id = ?',
1612
                [$articleId, $product->shopId])
1613
                ->fetchAll(\PDO::FETCH_COLUMN);
1614
        }
1615
1616
        $this->manager->getConnection()->executeQuery('DELETE FROM s_articles_relationships WHERE id IN (?)',
1617
            [$oldRelatedIds],
1618
            [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]);
1619
1620 View Code Duplication
        if (count($product->similar) > 0) {
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...
1621
            $this->manager->getConnection()->executeQuery('DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND related_article_local_id NOT IN (?) AND relationship_type = ?',
1622
                [$articleId, $product->shopId, $product->similar, self::RELATION_TYPE_SIMILAR],
1623
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR]);
1624
1625
            $oldSimilarIds = $this->manager->getConnection()->executeQuery(
1626
                'SELECT ar.id 
1627
                FROM s_articles_similar AS ar
1628
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1629
                WHERE ar.articleID = ? AND ci.shop_id = ? AND ci.source_id NOT IN (?)',
1630
                [$articleId, $product->shopId, $product->similar],
1631
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY])
1632
                ->fetchAll(\PDO::FETCH_COLUMN);
1633
        } else {
1634
            $this->manager->getConnection()->executeQuery('DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND relationship_type = ?',
1635
                [$articleId, $product->shopId, self::RELATION_TYPE_SIMILAR]);
1636
1637
            $oldSimilarIds = $this->manager->getConnection()->executeQuery(
1638
                'SELECT ar.id 
1639
                FROM s_articles_similar AS ar
1640
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1641
                WHERE ar.articleID = ? AND ci.shop_id = ?',
1642
                [$articleId, $product->shopId])
1643
                ->fetchAll(\PDO::FETCH_COLUMN);
1644
        }
1645
1646
        $this->manager->getConnection()->executeQuery('DELETE FROM s_articles_similar WHERE id IN (?)',
1647
            [$oldSimilarIds],
1648
            [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]);
1649
    }
1650
1651
    /**  
1652
     * @param Property $property
1653
     * @return PropertyGroup
1654
     */
1655
    private function createPropertyGroup(Property $property)
1656
    {
1657
        $group = new PropertyGroup();
1658
        $group->setName($property->groupName);
1659
        $group->setComparable($property->comparable);
1660
        $group->setSortMode($property->sortMode);
1661
        $group->setPosition($property->groupPosition);
1662
1663
        $attribute = new \Shopware\Models\Attribute\PropertyGroup();
1664
        $attribute->setPropertyGroup($group);
1665
        $attribute->setConnectIsRemote(true);
1666
        $group->setAttribute($attribute);
1667
1668
        $this->manager->persist($attribute);
1669
        $this->manager->persist($group);
1670
        $this->manager->flush();
1671
1672
        return $group;
1673
    }
1674
}
1675