Completed
Push — master ( 1a6f67...12dacc )
by Sebastian
05:32
created

ProductToShop::deleteRemovedRelations()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 78
Code Lines 45

Duplication

Lines 50
Ratio 64.1 %

Importance

Changes 0
Metric Value
cc 3
eloc 45
nc 4
nop 2
dl 50
loc 78
rs 8.9019
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Status;
18
use Shopware\Models\Article\Detail as DetailModel;
19
use Shopware\Models\Attribute\Article as AttributeModel;
20
use Shopware\Components\Model\ModelManager;
21
use Shopware\Connect\Struct\PriceRange;
22
use Shopware\Connect\Struct\ProductUpdate;
23
use Shopware\CustomModels\Connect\ProductStreamAttribute;
24
use Shopware\Models\Customer\Group;
25
use Shopware\Connect\Struct\Property;
26
use Shopware\Models\ProductStream\ProductStream;
27
use Shopware\Models\Property\Group as PropertyGroup;
28
use Shopware\Models\Property\Option as PropertyOption;
29
use Shopware\Models\Property\Value as PropertyValue;
30
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamRepository;
31
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamService;
32
use ShopwarePlugins\Connect\Components\Translations\LocaleMapper;
33
use ShopwarePlugins\Connect\Components\Gateway\ProductTranslationsGateway;
34
use ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway;
35
use ShopwarePlugins\Connect\Components\Utils\UnitMapper;
36
use Shopware\CustomModels\Connect\Attribute as ConnectAttribute;
37
use Shopware\Models\Article\Supplier;
38
use Shopware\Models\Tax\Tax;
39
use Shopware\Models\Article\Configurator\Set;
40
41
/**
42
 * The interface for products imported *from* connect *to* the local shop
43
 *
44
 * @category  Shopware
45
 * @package   Shopware\Plugins\SwagConnect
46
 */
47
class ProductToShop implements ProductToShopBase
48
{
49
    const RELATION_TYPE_RELATED = 'relationships';
50
    const RELATION_TYPE_SIMILAR = 'similar';
51
52
    /**
53
     * @var Helper
54
     */
55
    private $helper;
56
57
    /**
58
     * @var ModelManager
59
     */
60
    private $manager;
61
62
    /**
63
     * @var \ShopwarePlugins\Connect\Components\Config
64
     */
65
    private $config;
66
67
    /**
68
     * @var ImageImport
69
     */
70
    private $imageImport;
71
72
    /**
73
     * @var \ShopwarePlugins\Connect\Components\VariantConfigurator
74
     */
75
    private $variantConfigurator;
76
77
    /**
78
     * @var MarketplaceGateway
79
     */
80
    private $marketplaceGateway;
81
82
    /**
83
     * @var ProductTranslationsGateway
84
     */
85
    private $productTranslationsGateway;
86
87
    /**
88
     * @var \Shopware\Models\Shop\Repository
89
     */
90
    private $shopRepository;
91
92
    private $localeRepository;
93
94
    /**
95
     * @var CategoryResolver
96
     */
97
    private $categoryResolver;
98
99
    /**
100
     * @var \Shopware\Connect\Gateway
101
     */
102
    private $connectGateway;
103
104
    /**
105
     * @var \Enlight_Event_EventManager
106
     */
107
    private $eventManager;
108
109
    /**
110
     * @var CategoryDenormalization
111
     */
112
    private $categoryDenormalization;
113
114
    /**
115
     * @param Helper $helper
116
     * @param ModelManager $manager
117
     * @param ImageImport $imageImport
118
     * @param \ShopwarePlugins\Connect\Components\Config $config
119
     * @param VariantConfigurator $variantConfigurator
120
     * @param \ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway $marketplaceGateway
121
     * @param ProductTranslationsGateway $productTranslationsGateway
122
     * @param CategoryResolver $categoryResolver
123
     * @param Gateway $connectGateway
124
     * @param \Enlight_Event_EventManager $eventManager
125
     * @param CategoryDenormalization $categoryDenormalization
126
     */
127
    public function __construct(
128
        Helper $helper,
129
        ModelManager $manager,
130
        ImageImport $imageImport,
131
        Config $config,
132
        VariantConfigurator $variantConfigurator,
133
        MarketplaceGateway $marketplaceGateway,
134
        ProductTranslationsGateway $productTranslationsGateway,
135
        CategoryResolver $categoryResolver,
136
        Gateway $connectGateway,
137
        \Enlight_Event_EventManager $eventManager,
138
        CategoryDenormalization $categoryDenormalization
139
    ) {
140
        $this->helper = $helper;
141
        $this->manager = $manager;
142
        $this->config = $config;
143
        $this->imageImport = $imageImport;
144
        $this->variantConfigurator = $variantConfigurator;
145
        $this->marketplaceGateway = $marketplaceGateway;
146
        $this->productTranslationsGateway = $productTranslationsGateway;
147
        $this->categoryResolver = $categoryResolver;
148
        $this->connectGateway = $connectGateway;
149
        $this->eventManager = $eventManager;
150
        $this->categoryDenormalization = $categoryDenormalization;
151
    }
152
153
    /**
154
     * Start transaction
155
     *
156
     * Starts a transaction, which includes all insertOrUpdate and delete
157
     * operations, as well as the revision updates.
158
     *
159
     * @return void
160
     */
161
    public function startTransaction()
162
    {
163
        $this->manager->getConnection()->beginTransaction();
164
    }
165
166
    /**
167
     * Commit transaction
168
     *
169
     * Commits the transactions, once all operations are queued.
170
     *
171
     * @return void
172
     */
173
    public function commit()
174
    {
175
        $this->manager->getConnection()->commit();
176
    }
177
178
    /**
179
     * Import or update given product
180
     *
181
     * Store product in your shop database as an external product. The
182
     * associated sourceId
183
     *
184
     * @param Product $product
185
     */
186
    public function insertOrUpdate(Product $product)
187
    {
188
        /** @var Product $product */
189
        $product = $this->eventManager->filter(
190
            'Connect_ProductToShop_InsertOrUpdate_Before',
191
            $product
192
        );
193
194
        // todo@dn: Set dummy values and make product inactive
195
        if (empty($product->title) || empty($product->vendor)) {
196
            return;
197
        }
198
199
        $number = $this->generateSKU($product);
200
201
        $detail = $this->helper->getArticleDetailModelByProduct($product);
202
        $detail = $this->eventManager->filter(
203
            'Connect_Merchant_Get_Article_Detail_After',
204
            $detail,
205
            [
206
                'product' => $product,
207
                'subject' => $this
208
            ]
209
        );
210
211
        $isMainVariant = false;
212
        if ($detail === null) {
213
            $active = $this->config->getConfig('activateProductsAutomatically', false) ? true : false;
214
215
            $model = $this->getSWProductModel($product, $active, $isMainVariant);
216
217
            $detail = $this->generateNewDetail($product, $model);
218
        } else {
219
            /** @var ProductModel $model */
220
            $model = $detail->getArticle();
221
            // fix for isMainVariant flag
222
            // in connect attribute table
223
            $mainDetail = $model->getMainDetail();
224
            $isMainVariant = $this->checkIfMainVariant($detail, $mainDetail);
225
            $this->variantConfigurator->configureVariantAttributes($product, $detail);
226
            $this->updateConfiguratorSetTypeFromProduct($model, $product);
227
228
            $this->cleanUpConfiguratorSet($model, $product);
229
        }
230
231
        $detail->setNumber($number);
232
233
        $detailAttribute = $this->getOrCreateAttributeModel($detail, $model);
234
235
        $connectAttribute = $this->helper->getConnectAttributeByModel($detail) ?: new ConnectAttribute;
236
        // configure main variant and groupId
237
        if ($isMainVariant === true) {
238
            $connectAttribute->setIsMainVariant(true);
239
        }
240
        $connectAttribute->setGroupId($product->groupId);
241
242
        list($updateFields, $flag) = $this->getUpdateFields($model, $detail, $connectAttribute, $product);
243
        $this->setPropertiesForNewProducts($updateFields, $model, $detailAttribute, $product);
244
245
        $this->saveVat($product, $model);
246
247
        $this->applyProductProperties($model, $product);
248
249
        $detailAttribute = $this->applyMarketplaceAttributes($detailAttribute, $product);
250
251
        $this->setConnectAttributesFromProduct($connectAttribute, $product);
252
253
        // store product categories to connect attribute
254
        $connectAttribute->setCategory($product->categories);
255
256
        $connectAttribute->setLastUpdateFlag($flag);
257
258
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
259
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
260
261
        $this->updateDetailFromProduct($detail, $product);
262
263
        // some shops have feature "sell not in stock",
264
        // then end customer should be able to by the product with stock = 0
265
        $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
266
        if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
267
            $model->setLastStock(false);
268
        } else {
269
            $model->setLastStock(true);
270
        }
271
272
        $this->detailSetUnit($detail, $product, $detailAttribute);
273
274
        $this->detailSetAttributes($detail, $product);
275
276
        $this->connectAttributeSetLastUpdate($connectAttribute, $product);
277
278
        if ($model->getMainDetail() === null) {
279
            $model->setMainDetail($detail);
280
        }
281
282
        if ($detail->getAttribute() === null) {
283
            $detail->setAttribute($detailAttribute);
284
            $detailAttribute->setArticle($model);
285
        }
286
287
        $connectAttribute->setArticle($model);
288
        $connectAttribute->setArticleDetail($detail);
289
290
        $this->eventManager->notify(
291
            'Connect_Merchant_Saving_ArticleAttribute_Before',
292
            [
293
                'subject' => $this,
294
                'connectAttribute' => $connectAttribute
295
            ]
296
        );
297
298
        //article has to be flushed
299
        $this->manager->persist($model);
300
        $this->manager->persist($connectAttribute);
301
        $this->manager->persist($detail);
302
        $this->manager->flush();
303
304
        $this->categoryResolver->storeRemoteCategories($product->categories, $model->getId(), $product->shopId);
305
        $categories = $this->categoryResolver->resolve($product->categories, $product->shopId, $product->stream);
306
        if (count($categories) > 0) {
307
            $detailAttribute->setConnectMappedCategory(true);
308
        }
309
310
        $this->manager->persist($detailAttribute);
311
        $this->manager->flush();
312
313
        $this->categoryDenormalization($model, $categories);
314
315
        $defaultCustomerGroup = $this->helper->getDefaultCustomerGroup();
316
        // Only set prices, if fixedPrice is active or price updates are configured
317
        if (count($detail->getPrices()) == 0 || $connectAttribute->getFixedPrice() || $updateFields['price']) {
318
            $this->setPrice($model, $detail, $product);
319
        }
320
        // If the price is not being update, update the purchasePrice anyway
321
        $this->setPurchasePrice($detail, $product->purchasePrice, $defaultCustomerGroup);
322
323
        $this->manager->clear();
324
325
        $this->addArticleTranslations($model, $product);
326
327
        if ($isMainVariant || $product->groupId === null) {
328
            $this->applyCrossSelling($model->getId(), $product);
329
        }
330
331
        //clear cache for that article
332
        $this->helper->clearArticleCache($model->getId());
333
334
        if ($updateFields['image']) {
335
            // Reload the model in order to not to work an the already flushed model
336
            $model = $this->helper->getArticleModelByProduct($product);
337
            // import only global images for article
338
            $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 336 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...
339
            if ($updateFields['mainImage'] && isset($product->images[0])) {
340
                $this->imageImport->importMainImage($product->images[0], $model->getId());
341
            }
342
            // Reload the article detail model in order to not to work an the already flushed model
343
            $detail = $this->helper->getArticleDetailModelByProduct($product);
344
            // import only specific images for variant
345
            $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 343 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...
346
        }
347
348
        $this->eventManager->notify(
349
            'Connect_ProductToShop_InsertOrUpdate_After',
350
            [
351
                'connectProduct' => $product,
352
                'shopArticleDetail' => $detail
353
            ]
354
        );
355
356
        $stream = $this->getOrCreateStream($product);
357
        $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...
358
    }
359
360
    /**
361
     * @param Product $product
362
     * @return string
363
     */
364
    private function generateSKU(Product $product)
365
    {
366
        if (!empty($product->sku)) {
367
            $number = 'SC-' . $product->shopId . '-' . $product->sku;
368
            $duplicatedDetail = $this->helper->getDetailByNumber($number);
369
            if ($duplicatedDetail
370
                && $this->helper->getConnectAttributeByModel($duplicatedDetail)->getSourceId() != $product->sourceId
371
            ) {
372
                $this->deleteDetail($duplicatedDetail);
373
            }
374
        } else {
375
            $number = 'SC-' . $product->shopId . '-' . $product->sourceId;
376
        }
377
378
        return $number;
379
    }
380
381
    /**
382
     * @param DetailModel $detailModel
383
     */
384
    private function deleteDetail(DetailModel $detailModel)
385
    {
386
        $this->eventManager->notify(
387
            'Connect_Merchant_Delete_Product_Before',
388
            [
389
                'subject' => $this,
390
                'articleDetail' => $detailModel
391
            ]
392
        );
393
394
        $article = $detailModel->getArticle();
395
        // Not sure why, but the Attribute can be NULL
396
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
397
        $this->manager->remove($detailModel);
398
399
        if ($attribute) {
400
            $this->manager->remove($attribute);
401
        }
402
403
        // if removed variant is main variant
404
        // find first variant which is not main and mark it
405
        if ($detailModel->getKind() === 1) {
406
            /** @var \Shopware\Models\Article\Detail $variant */
407
            foreach ($article->getDetails() as $variant) {
408
                if ($variant->getId() != $detailModel->getId()) {
409
                    $variant->setKind(1);
410
                    $article->setMainDetail($variant);
411
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
412
                    if (!$connectAttribute) {
413
                        continue;
414
                    }
415
                    $connectAttribute->setIsMainVariant(true);
416
                    $this->manager->persist($connectAttribute);
417
                    $this->manager->persist($article);
418
                    $this->manager->persist($variant);
419
                    break;
420
                }
421
            }
422
        }
423
424
        if (count($details = $article->getDetails()) === 1) {
425
            $details->clear();
426
            $this->manager->remove($article);
427
        }
428
429
        //save category Ids before flush
430
        $oldCategoryIds = array_map(function ($category) {
431
            return $category->getId();
432
        }, $article->getCategories()->toArray());
433
434
        // Do not remove flush. It's needed when remove article,
435
        // because duplication of ordernumber. Even with remove before
436
        // persist calls mysql throws exception "Duplicate entry"
437
        $this->manager->flush();
438
        // always clear entity manager, because $article->getDetails() returns
439
        // more than 1 detail, but all of them were removed except main one.
440
        $this->manager->clear();
441
442
        // call this after flush because article has to be deleted that this works
443
        if (count($oldCategoryIds) > 0) {
444
            $this->categoryResolver->deleteEmptyConnectCategories($oldCategoryIds);
445
        }
446
    }
447
448
    /**
449
     * @param Product $product
450
     * @param $active
451
     * @param $isMainVariant
452
     * @return null|Article
453
     */
454
    private function getSWProductModel(Product $product, $active, &$isMainVariant)
455
    {
456
        if ($product->groupId !== null) {
457
            $model = $this->helper->getArticleByRemoteProduct($product);
458
            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...
459
                $model = $this->helper->createProductModel($product);
460
                $model->setActive($active);
461
                $isMainVariant = true;
462
            }
463
        } else {
464
            $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
465
            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...
466
                $model = $this->helper->createProductModel($product);
467
                $model->setActive($active);
468
            }
469
        }
470
471
        return $model;
472
    }
473
474
    /**
475
     * @param Product $product
476
     * @param $model
477
     * @return DetailModel
478
     */
479
    private function generateNewDetail(Product $product, $model)
480
    {
481
        $detail = new DetailModel();
482
        $detail->setActive($model->getActive());
483
        // added for 5.4 compatibility
484
        if (method_exists($detail, 'setLastStock')) {
485
            $detail->setLastStock($product->lastStock);
486
        }
487
        $this->manager->persist($detail);
488
        $detail->setArticle($model);
489
        $model->getDetails()->add($detail);
490
        $this->variantConfigurator->configureVariantAttributes($product, $detail);
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
        $configSet = $model->getConfiguratorSet();
512
        if (!empty($product->variant) && $configSet instanceof Set) {
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Article\Configurator\Set 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...
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(
575
                'importImagesOnFirstImport',
576
                    false
577
            )) {
578
                $output[$field] = false;
579
                $flag |= $flagsByName['imageInitialImport'];
580
                continue;
581
            }
582
583
            $updateAllowed = $this->isFieldUpdateAllowed($field, $model, $attribute);
584
            $output[$field] = $updateAllowed;
585
            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...
586
                $flag |= $key;
587
            }
588
        }
589
590
        return [$output, $flag];
591
    }
592
593
    /**
594
     * Helper method to determine if a given $fields may/must be updated.
595
     * This method will check for the model->id in order to determine, if it is a new entity. Therefore
596
     * this method cannot be used after the model in question was already flushed.
597
     *
598
     * @param $field
599
     * @param $model ProductModel
600
     * @param $attribute ConnectAttribute
601
     * @throws \RuntimeException
602
     * @return bool|null
603
     */
604
    public function isFieldUpdateAllowed($field, ProductModel $model, ConnectAttribute $attribute)
605
    {
606
        $allowed = [
607
            'ShortDescription',
608
            'LongDescription',
609
            'AdditionalDescription',
610
            'Image',
611
            'Price',
612
            'Name',
613
            'MainImage',
614
        ];
615
616
        // Always allow updates for new models
617
        if (!$model->getId()) {
618
            return true;
619
        }
620
621
        $field = ucfirst($field);
622
        $attributeGetter = 'getUpdate' . $field;
623
        $configName = 'overwriteProduct' . $field;
624
625
        if (!in_array($field, $allowed)) {
626
            throw new \RuntimeException("Unknown update field {$field}");
627
        }
628
629
        $attributeValue = $attribute->$attributeGetter();
630
631
632
        // If the value is 'null' or 'inherit', the behaviour will be inherited from the global configuration
633
        // Once we have a supplier based configuration, we need to take it into account here
634
        if ($attributeValue == null || $attributeValue == 'inherit') {
635
            return $this->config->getConfig($configName, true);
636
        }
637
638
        return $attributeValue == 'overwrite';
639
    }
640
641
    /**
642
     * Determine if a given field has changed
643
     *
644
     * @param $field
645
     * @param ProductModel $model
646
     * @param DetailModel $detail
647
     * @param Product $product
648
     * @return bool
649
     */
650
    public function hasFieldChanged($field, ProductModel $model, DetailModel $detail, Product $product)
651
    {
652
        switch ($field) {
653
            case 'shortDescription':
654
                return $model->getDescription() != $product->shortDescription;
655
            case 'longDescription':
656
                return $model->getDescriptionLong() != $product->longDescription;
657
            case 'additionalDescription':
658
                return $detail->getAttribute()->getConnectProductDescription() != $product->additionalDescription;
659
            case 'name':
660
                return $model->getName() != $product->title;
661
            case 'image':
662
                return count($model->getImages()) != count($product->images);
663
            case 'mainImage':
664
                if ($product->images[0]) {
665
                    return $this->imageImport->hasMainImageChanged($product->images[0], $model->getId());
666
                }
667
668
                return false;
669
            case 'price':
670
                $prices = $detail->getPrices();
671
                if (empty($prices)) {
672
                    return true;
673
                }
674
                $price = $prices->first();
675
                if (!$price) {
676
                    return true;
677
                }
678
679
                return $prices->first()->getPrice() != $product->price;
680
        }
681
682
        throw new \InvalidArgumentException('Unrecognized field');
683
    }
684
685
    /**
686
     * @param array $updateFields
687
     * @param ProductModel $model
688
     * @param AttributeModel $detailAttribute
689
     * @param Product $product
690
     */
691
    private function setPropertiesForNewProducts(array $updateFields, ProductModel $model, AttributeModel $detailAttribute, Product $product)
692
    {
693
        /*
694
         * Make sure, that the following properties are set for
695
         * - new products
696
         * - products that have been configured to receive these updates
697
         */
698
        if ($updateFields['name']) {
699
            $model->setName($product->title);
700
        }
701
        if ($updateFields['shortDescription']) {
702
            $model->setDescription($product->shortDescription);
703
        }
704
        if ($updateFields['longDescription']) {
705
            $model->setDescriptionLong($product->longDescription);
706
        }
707
708
        if ($updateFields['additionalDescription']) {
709
            $detailAttribute->setConnectProductDescription($product->additionalDescription);
710
        }
711
712
        if ($product->vat !== null) {
713
            $repo = $this->manager->getRepository('Shopware\Models\Tax\Tax');
714
            $tax = round($product->vat * 100, 2);
715
            /** @var \Shopware\Models\Tax\Tax $tax */
716
            $tax = $repo->findOneBy(['tax' => $tax]);
717
            $model->setTax($tax);
718
        }
719
720
        if ($product->vendor !== null) {
721
            $repo = $this->manager->getRepository('Shopware\Models\Article\Supplier');
722
            $supplier = $repo->findOneBy(['name' => $product->vendor]);
723
            if ($supplier === null) {
724
                $supplier = $this->createSupplier($product->vendor);
725
            }
726
            $model->setSupplier($supplier);
727
        }
728
    }
729
730
    /**
731
     * @param $vendor
732
     * @return Supplier
733
     */
734
    private function createSupplier($vendor)
735
    {
736
        $supplier = new Supplier();
737
738
        if (is_array($vendor)) {
739
            $supplier->setName($vendor['name']);
740
            $supplier->setDescription($vendor['description']);
741
            if (array_key_exists('url', $vendor) && $vendor['url']) {
742
                $supplier->setLink($vendor['url']);
743
            }
744
745
            $supplier->setMetaTitle($vendor['page_title']);
746
747
            if (array_key_exists('logo_url', $vendor) && $vendor['logo_url']) {
748
                $this->imageImport->importImageForSupplier($vendor['logo_url'], $supplier);
749
            }
750
        } else {
751
            $supplier->setName($vendor);
752
        }
753
754
        //sets supplier attributes
755
        $attr = new \Shopware\Models\Attribute\ArticleSupplier();
756
        $attr->setConnectIsRemote(true);
757
758
        $supplier->setAttribute($attr);
759
760
        return $supplier;
761
    }
762
763
    /**
764
     * @param ProductModel $article
765
     * @param Product $product
766
     */
767
    private function applyProductProperties(ProductModel $article, Product $product)
768
    {
769
        if (empty($product->properties)) {
770
            return;
771
        }
772
773
        /** @var Property $firstProperty */
774
        $firstProperty = reset($product->properties);
775
        $groupRepo = $this->manager->getRepository(PropertyGroup::class);
776
        $group = $groupRepo->findOneBy(['name' => $firstProperty->groupName]);
777
778
        if (!$group) {
779
            $group = $this->createPropertyGroup($firstProperty);
780
        }
781
782
        $propertyValues = $article->getPropertyValues();
783
        $propertyValues->clear();
784
        $this->manager->persist($article);
785
        $this->manager->flush();
786
787
        $article->setPropertyGroup($group);
788
789
        $optionRepo = $this->manager->getRepository(PropertyOption::class);
790
        $valueRepo = $this->manager->getRepository(PropertyValue::class);
791
792
        foreach ($product->properties as $property) {
793
            $option = $optionRepo->findOneBy(['name' => $property->option]);
794
            $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...
795
            if (!$option) {
796
                $option = new PropertyOption();
797
                $option->setName($property->option);
798
                $option->setFilterable($property->filterable);
799
800
                $attribute = new \Shopware\Models\Attribute\PropertyOption();
801
                $attribute->setPropertyOption($option);
802
                $attribute->setConnectIsRemote(true);
803
                $option->setAttribute($attribute);
804
805
                $this->manager->persist($option);
806
                $this->manager->flush($option);
807
            }
808
809
            if (!$optionExists || !$value = $valueRepo->findOneBy(['option' => $option, 'value' => $property->value])) {
810
                $value = new PropertyValue($option, $property->value);
811
                $value->setPosition($property->valuePosition);
812
813
                $attribute = new \Shopware\Models\Attribute\PropertyValue();
814
                $attribute->setPropertyValue($value);
815
                $attribute->setConnectIsRemote(true);
816
                $value->setAttribute($attribute);
817
818
                $this->manager->persist($value);
819
            }
820
821
            if (!$propertyValues->contains($value)) {
822
                //add only new values
823
                $propertyValues->add($value);
824
            }
825
826
            $filters = [
827
                ['property' => 'options.name', 'expression' => '=', 'value' => $property->option],
828
                ['property' => 'groups.name', 'expression' => '=', 'value' => $property->groupName],
829
            ];
830
831
            $query = $groupRepo->getPropertyRelationQuery($filters, null, 1, 0);
832
            $relation = $query->getOneOrNullResult();
833
834
            if (!$relation) {
835
                $group->addOption($option);
836
                $this->manager->persist($group);
837
                $this->manager->flush($group);
838
            }
839
        }
840
841
        $article->setPropertyValues($propertyValues);
842
843
        $this->manager->persist($article);
844
        $this->manager->flush();
845
    }
846
847
    /**
848
     * Read product attributes mapping and set to shopware attribute model
849
     *
850
     * @param AttributeModel $detailAttribute
851
     * @param Product $product
852
     * @return AttributeModel
853
     */
854
    private function applyMarketplaceAttributes(AttributeModel $detailAttribute, Product $product)
855
    {
856
        $detailAttribute->setConnectReference($product->sourceId);
857
        $detailAttribute->setConnectArticleShipping($product->shipping);
858
        //todo@sb: check if connectAttribute matches position of the marketplace attribute
859
        array_walk($product->attributes, function ($value, $key) use ($detailAttribute) {
860
            $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...
861
            if (strlen($shopwareAttribute) > 0) {
862
                $setter = 'set' . ucfirst($shopwareAttribute);
863
                $detailAttribute->$setter($value);
864
            }
865
        });
866
867
        return $detailAttribute;
868
    }
869
870
    /**
871
     * @param  ConnectAttribute $connectAttribute
872
     * @param Product $product
873
     */
874
    private function setConnectAttributesFromProduct(ConnectAttribute $connectAttribute, Product $product)
875
    {
876
        $connectAttribute->setShopId($product->shopId);
877
        $connectAttribute->setSourceId($product->sourceId);
878
        $connectAttribute->setExportStatus(null);
879
        $connectAttribute->setPurchasePrice($product->purchasePrice);
880
        $connectAttribute->setFixedPrice($product->fixedPrice);
881
        $connectAttribute->setStream($product->stream);
882
883
        // store purchasePriceHash and offerValidUntil
884
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
885
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
886
    }
887
888
    /**
889
     * @param DetailModel $detail
890
     * @param Product $product
891
     */
892
    private function updateDetailFromProduct(DetailModel $detail, Product $product)
893
    {
894
        $detail->setInStock($product->availability);
895
        $detail->setEan($product->ean);
896
        $detail->setShippingTime($product->deliveryWorkDays);
897
        $releaseDate = new \DateTime();
898
        $releaseDate->setTimestamp($product->deliveryDate);
899
        $detail->setReleaseDate($releaseDate);
900
        $detail->setMinPurchase($product->minPurchaseQuantity);
901
    }
902
903
    /**
904
     * @param DetailModel $detail
905
     * @param Product $product
906
     * @param $detailAttribute
907
     */
908
    private function detailSetUnit(DetailModel $detail, Product $product, $detailAttribute)
909
    {
910
        // if connect product has unit
911
        // find local unit with units mapping
912
        // and add to detail model
913
        if (array_key_exists('unit', $product->attributes) && $product->attributes['unit']) {
914
            $detailAttribute->setConnectRemoteUnit($product->attributes['unit']);
915
            if ($this->config->getConfig($product->attributes['unit']) == null) {
916
                $this->config->setConfig($product->attributes['unit'], '', null, 'units');
917
            }
918
919
            /** @var \ShopwarePlugins\Connect\Components\Utils\UnitMapper $unitMapper */
920
            $unitMapper = new UnitMapper($this->config, $this->manager);
921
922
            $shopwareUnit = $unitMapper->getShopwareUnit($product->attributes['unit']);
923
924
            /** @var \Shopware\Models\Article\Unit $unit */
925
            $unit = $this->helper->getUnit($shopwareUnit);
926
            $detail->setUnit($unit);
927
            $detail->setPurchaseUnit($product->attributes['quantity']);
928
            $detail->setReferenceUnit($product->attributes['ref_quantity']);
929
        } else {
930
            $detail->setUnit(null);
931
            $detail->setPurchaseUnit(null);
932
            $detail->setReferenceUnit(null);
933
        }
934
    }
935
936
    /**
937
     * @param DetailModel $detail
938
     * @param Product $product
939
     */
940
    private function detailSetAttributes(DetailModel $detail, Product $product)
941
    {
942
        // set dimension
943
        if (array_key_exists('dimension', $product->attributes) && $product->attributes['dimension']) {
944
            $dimension = explode('x', $product->attributes['dimension']);
945
            $detail->setLen($dimension[0]);
946
            $detail->setWidth($dimension[1]);
947
            $detail->setHeight($dimension[2]);
948
        } else {
949
            $detail->setLen(null);
950
            $detail->setWidth(null);
951
            $detail->setHeight(null);
952
        }
953
954
        // set weight
955
        if (array_key_exists('weight', $product->attributes) && $product->attributes['weight']) {
956
            $detail->setWeight($product->attributes['weight']);
957
        }
958
959
        //set package unit
960
        if (array_key_exists(Product::ATTRIBUTE_PACKAGEUNIT, $product->attributes)) {
961
            $detail->setPackUnit($product->attributes[Product::ATTRIBUTE_PACKAGEUNIT]);
962
        }
963
964
        //set basic unit
965
        if (array_key_exists(Product::ATTRIBUTE_BASICUNIT, $product->attributes)) {
966
            $detail->setMinPurchase($product->attributes[Product::ATTRIBUTE_BASICUNIT]);
967
        }
968
969
        //set manufacturer no.
970
        if (array_key_exists(Product::ATTRIBUTE_MANUFACTURERNUMBER, $product->attributes)) {
971
            $detail->setSupplierNumber($product->attributes[Product::ATTRIBUTE_MANUFACTURERNUMBER]);
972
        }
973
    }
974
975
    /**
976
     * @param ConnectAttribute $connectAttribute
977
     * @param Product $product
978
     */
979
    private function connectAttributeSetLastUpdate(ConnectAttribute $connectAttribute, Product $product)
980
    {
981
        // Whenever a product is updated, store a json encoded list of all fields that are updated optionally
982
        // This way a customer will be able to apply the most recent changes any time later
983
        $connectAttribute->setLastUpdate(json_encode([
984
            'shortDescription' => $product->shortDescription,
985
            'longDescription' => $product->longDescription,
986
            'additionalDescription' => $product->additionalDescription,
987
            'purchasePrice' => $product->purchasePrice,
988
            'image' => $product->images,
989
            'variantImages' => $product->variantImages,
990
            'price' => $product->price * ($product->vat + 1),
991
            'name' => $product->title,
992
            'vat' => $product->vat
993
        ]));
994
    }
995
996
    /**
997
     * @param ProductModel $model
998
     * @param array $categories
999
     */
1000
    private function categoryDenormalization(ProductModel $model, array $categories)
1001
    {
1002
        $this->categoryDenormalization->disableTransactions();
1003
        foreach ($categories as $category) {
1004
            $this->categoryDenormalization->addAssignment($model->getId(), $category);
1005
            $this->manager->getConnection()->executeQuery(
1006
                'INSERT IGNORE INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES (?,?)',
1007
                [$model->getId(), $category]
1008
            );
1009
            $parentId =$this->manager->getConnection()->fetchColumn(
1010
                'SELECT parent FROM `s_categories` WHERE id = ?',
1011
                [$category]
1012
            );
1013
            $this->categoryDenormalization->removeAssignment($model->getId(), $parentId);
1014
            $this->manager->getConnection()->executeQuery(
1015
                'DELETE FROM `s_articles_categories` WHERE `articleID` = ? AND `categoryID` = ?',
1016
                [$model->getId(), $parentId]
1017
            );
1018
        }
1019
        $this->categoryDenormalization->enableTransactions();
1020
    }
1021
1022
    /**
1023
     * @param Product $product
1024
     * @return ProductStream
1025
     */
1026
    private function getOrCreateStream(Product $product)
1027
    {
1028
        /** @var ProductStreamRepository $repo */
1029
        $repo = $this->manager->getRepository(ProductStreamAttribute::class);
1030
        $stream = $repo->findConnectByName($product->stream);
1031
1032
        if (!$stream) {
1033
            $stream = new ProductStream();
1034
            $stream->setName($product->stream);
1035
            $stream->setType(ProductStreamService::STATIC_STREAM);
1036
            $stream->setSorting(json_encode(
1037
                [ReleaseDateSorting::class => ['direction' => 'desc']]
1038
            ));
1039
1040
            //add attributes
1041
            $attribute = new \Shopware\Models\Attribute\ProductStream();
1042
            $attribute->setProductStream($stream);
1043
            $attribute->setConnectIsRemote(true);
1044
            $stream->setAttribute($attribute);
1045
1046
            $this->manager->persist($attribute);
1047
            $this->manager->persist($stream);
1048
            $this->manager->flush();
1049
        }
1050
1051
        return $stream;
1052
    }
1053
1054
    /**
1055
     * @param ProductStream $stream
1056
     * @param ProductModel $article
1057
     * @throws \Doctrine\DBAL\DBALException
1058
     */
1059
    private function addProductToStream(ProductStream $stream, ProductModel $article)
1060
    {
1061
        $conn = $this->manager->getConnection();
1062
        $sql = 'INSERT INTO `s_product_streams_selection` (`stream_id`, `article_id`)
1063
                VALUES (:streamId, :articleId)
1064
                ON DUPLICATE KEY UPDATE stream_id = :streamId, article_id = :articleId';
1065
        $stmt = $conn->prepare($sql);
1066
        $stmt->execute([':streamId' => $stream->getId(), ':articleId' => $article->getId()]);
1067
    }
1068
1069
    /**
1070
     * Set detail purchase price with plain SQL
1071
     * Entity usage throws exception when error handlers are disabled
1072
     *
1073
     * @param ProductModel $article
1074
     * @param DetailModel $detail
1075
     * @param Product $product
1076
     * @throws \Doctrine\DBAL\DBALException
1077
     */
1078
    private function setPrice(ProductModel $article, DetailModel $detail, Product $product)
1079
    {
1080
        // set price via plain SQL because shopware throws exception
1081
        // undefined index: key when error handler is disabled
1082
        $customerGroup = $this->helper->getDefaultCustomerGroup();
1083
1084
        if (!empty($product->priceRanges)) {
1085
            $this->setPriceRange($article, $detail, $product->priceRanges, $customerGroup);
1086
1087
            return;
1088
        }
1089
1090
        $id = $this->manager->getConnection()->fetchColumn(
1091
            'SELECT id FROM `s_articles_prices`
1092
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1093
            [$customerGroup->getKey(), 1, 'beliebig', $article->getId(), $detail->getId()]
1094
        );
1095
1096
        // todo@sb: test update prices
1097
        if ($id > 0) {
1098
            $this->manager->getConnection()->executeQuery(
1099
                'UPDATE `s_articles_prices` SET `price` = ?, `baseprice` = ? WHERE `id` = ?',
1100
                [$product->price, $product->purchasePrice, $id]
1101
            );
1102
        } else {
1103
            $this->manager->getConnection()->executeQuery(
1104
                'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `baseprice`)
1105
              VALUES (?, 1, "beliebig", ?, ?, ?, ?);',
1106
                [
1107
                    $customerGroup->getKey(),
1108
                    $article->getId(),
1109
                    $detail->getId(),
1110
                    $product->price,
1111
                    $product->purchasePrice
1112
                ]
1113
            );
1114
        }
1115
    }
1116
1117
    /**
1118
     * @param ProductModel $article
1119
     * @param DetailModel $detail
1120
     * @param array $priceRanges
1121
     * @param Group $group
1122
     * @throws \Doctrine\DBAL\ConnectionException
1123
     * @throws \Exception
1124
     */
1125
    private function setPriceRange(ProductModel $article, DetailModel $detail, array $priceRanges, Group $group)
1126
    {
1127
        $this->manager->getConnection()->beginTransaction();
1128
1129
        try {
1130
            // We always delete the prices,
1131
            // because we can not know which record is update
1132
            $this->manager->getConnection()->executeQuery(
1133
                'DELETE FROM `s_articles_prices` WHERE `articleID` = ? AND `articledetailsID` = ?',
1134
                [$article->getId(), $detail->getId()]
1135
            );
1136
1137
            /** @var PriceRange $priceRange */
1138
            foreach ($priceRanges as $priceRange) {
1139
                $priceTo = $priceRange->to == PriceRange::ANY ? 'beliebig' : $priceRange->to;
1140
1141
                //todo: maybe batch insert if possible?
1142
                $this->manager->getConnection()->executeQuery(
1143
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`)
1144
                      VALUES (?, ?, ?, ?, ?, ?);',
1145
                    [
1146
                        $group->getKey(),
1147
                        $priceRange->from,
1148
                        $priceTo,
1149
                        $article->getId(),
1150
                        $detail->getId(),
1151
                        $priceRange->price
1152
                    ]
1153
                );
1154
            }
1155
            $this->manager->getConnection()->commit();
1156
        } catch (\Exception $e) {
1157
            $this->manager->getConnection()->rollBack();
1158
            throw new \Exception($e->getMessage());
1159
        }
1160
    }
1161
1162
    /**
1163
     * Set detail purchase price with plain SQL
1164
     * Entity usage throws exception when error handlers are disabled
1165
     *
1166
     * @param DetailModel $detail
1167
     * @param float $purchasePrice
1168
     * @param Group $defaultGroup
1169
     * @throws \Doctrine\DBAL\DBALException
1170
     */
1171
    private function setPurchasePrice(DetailModel $detail, $purchasePrice, Group $defaultGroup)
1172
    {
1173
        if (method_exists($detail, 'setPurchasePrice')) {
1174
            $this->manager->getConnection()->executeQuery(
1175
                'UPDATE `s_articles_details` SET `purchaseprice` = ? WHERE `id` = ?',
1176
                [$purchasePrice, $detail->getId()]
1177
            );
1178
        } else {
1179
            $id = $this->manager->getConnection()->fetchColumn(
1180
                'SELECT id FROM `s_articles_prices`
1181
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1182
                [$defaultGroup->getKey(), 1, 'beliebig', $detail->getArticleId(), $detail->getId()]
1183
            );
1184
1185
            if ($id > 0) {
1186
                $this->manager->getConnection()->executeQuery(
1187
                    'UPDATE `s_articles_prices` SET `baseprice` = ? WHERE `id` = ?',
1188
                    [$purchasePrice, $id]
1189
                );
1190
            } else {
1191
                $this->manager->getConnection()->executeQuery(
1192
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `baseprice`)
1193
              VALUES (?, 1, "beliebig", ?, ?, ?);',
1194
                    [$defaultGroup->getKey(), $detail->getArticleId(), $detail->getId(), $purchasePrice]
1195
                );
1196
            }
1197
        }
1198
    }
1199
1200
    /**
1201
     * Adds translation record for given article
1202
     *
1203
     * @param ProductModel $article
1204
     * @param Product $sdkProduct
1205
     */
1206
    private function addArticleTranslations(ProductModel $article, Product $sdkProduct)
1207
    {
1208
        /** @var \Shopware\Connect\Struct\Translation $translation */
1209
        foreach ($sdkProduct->translations as $key => $translation) {
1210
            /** @var \Shopware\Models\Shop\Locale $locale */
1211
            $locale = $this->getLocaleRepository()->findOneBy(['locale' => LocaleMapper::getShopwareLocale($key)]);
1212
            /** @var \Shopware\Models\Shop\Shop $shop */
1213
            $shop = $this->getShopRepository()->findOneBy(['locale' => $locale]);
1214
            if (!$shop) {
1215
                continue;
1216
            }
1217
1218
            $this->productTranslationsGateway->addArticleTranslation($translation, $article->getId(), $shop->getId());
1219
        }
1220
    }
1221
1222
    /**
1223
     * dsadsa
1224
     * @return \Shopware\Components\Model\ModelRepository
1225
     */
1226
    private function getLocaleRepository()
1227
    {
1228
        if (!$this->localeRepository) {
1229
            $this->localeRepository = $this->manager->getRepository('Shopware\Models\Shop\Locale');
1230
        }
1231
1232
        return $this->localeRepository;
1233
    }
1234
1235
    private function getShopRepository()
1236
    {
1237
        if (!$this->shopRepository) {
1238
            $this->shopRepository = $this->manager->getRepository('Shopware\Models\Shop\Shop');
1239
        }
1240
1241
        return $this->shopRepository;
1242
    }
1243
1244
    /**
1245
     * Delete product or product variant with given shopId and sourceId.
1246
     *
1247
     * Only the combination of both identifies a product uniquely. Do NOT
1248
     * delete products just by their sourceId.
1249
     *
1250
     * You might receive delete requests for products, which are not available
1251
     * in your shop. Just ignore them.
1252
     *
1253
     * @param string $shopId
1254
     * @param string $sourceId
1255
     * @return void
1256
     */
1257
    public function delete($shopId, $sourceId)
1258
    {
1259
        $detail = $this->helper->getArticleDetailModelByProduct(new Product([
1260
            'shopId' => $shopId,
1261
            'sourceId' => $sourceId,
1262
        ]));
1263
        if ($detail === null) {
1264
            return;
1265
        }
1266
1267
        $this->deleteDetail($detail);
1268
    }
1269
1270
    public function update($shopId, $sourceId, ProductUpdate $product)
1271
    {
1272
        // find article detail id
1273
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1274
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1275
            [$sourceId, $shopId]
1276
        );
1277
1278
        $this->eventManager->notify(
1279
            'Connect_Merchant_Update_GeneralProductInformation',
1280
            [
1281
                'subject' => $this,
1282
                'shopId' => $shopId,
1283
                'sourceId' => $sourceId,
1284
                'articleDetailId' => $articleDetailId
1285
            ]
1286
        );
1287
1288
        // update purchasePriceHash, offerValidUntil and purchasePrice in connect attribute
1289
        $this->manager->getConnection()->executeUpdate(
1290
            'UPDATE s_plugin_connect_items SET purchase_price_hash = ?, offer_valid_until = ?, purchase_price = ?
1291
            WHERE source_id = ? AND shop_id = ?',
1292
            [
1293
                $product->purchasePriceHash,
1294
                $product->offerValidUntil,
1295
                $product->purchasePrice,
1296
                $sourceId,
1297
                $shopId,
1298
            ]
1299
        );
1300
1301
        // update stock in article detail
1302
        // update prices
1303
        // if purchase price is stored in article detail
1304
        // update it together with stock
1305
        // since shopware 5.2
1306
        if (method_exists('Shopware\Models\Article\Detail', 'getPurchasePrice')) {
1307
            $this->manager->getConnection()->executeUpdate(
1308
                'UPDATE s_articles_details SET instock = ?, purchaseprice = ? WHERE id = ?',
1309
                [$product->availability, $product->purchasePrice, $articleDetailId]
1310
            );
1311
        } else {
1312
            $this->manager->getConnection()->executeUpdate(
1313
                'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1314
                [$product->availability, $articleDetailId]
1315
            );
1316
        }
1317
        $this->manager->getConnection()->executeUpdate(
1318
            "UPDATE s_articles_prices SET price = ?, baseprice = ? WHERE articledetailsID = ? AND pricegroup = 'EK'",
1319
            [$product->price, $product->purchasePrice, $articleDetailId]
1320
        );
1321
    }
1322
1323
    public function changeAvailability($shopId, $sourceId, $availability)
1324
    {
1325
        // find article detail id
1326
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1327
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1328
            [$sourceId, $shopId]
1329
        );
1330
1331
        $this->eventManager->notify(
1332
            'Connect_Merchant_Update_GeneralProductInformation',
1333
            [
1334
                'subject' => $this,
1335
                'shopId' => $shopId,
1336
                'sourceId' => $sourceId,
1337
                'articleDetailId' => $articleDetailId
1338
            ]
1339
        );
1340
1341
        // update stock in article detail
1342
        $this->manager->getConnection()->executeUpdate(
1343
            'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1344
            [$availability, $articleDetailId]
1345
        );
1346
    }
1347
1348
    /**
1349
     * @inheritDoc
1350
     */
1351
    public function makeMainVariant($shopId, $sourceId, $groupId)
1352
    {
1353
        //find article detail which should be selected as main one
1354
        $newMainDetail = $this->helper->getConnectArticleDetailModel($sourceId, $shopId);
1355
        if (!$newMainDetail) {
1356
            return;
1357
        }
1358
1359
        /** @var \Shopware\Models\Article\Article $article */
1360
        $article = $newMainDetail->getArticle();
1361
1362
        $this->eventManager->notify(
1363
            'Connect_Merchant_Update_ProductMainVariant_Before',
1364
            [
1365
                'subject' => $this,
1366
                'shopId' => $shopId,
1367
                'sourceId' => $sourceId,
1368
                'articleId' => $article->getId(),
1369
                'articleDetailId' => $newMainDetail->getId()
1370
            ]
1371
        );
1372
1373
        // replace current main detail with new one
1374
        $currentMainDetail = $article->getMainDetail();
1375
        $currentMainDetail->setKind(2);
1376
        $newMainDetail->setKind(1);
1377
        $article->setMainDetail($newMainDetail);
1378
1379
        $this->manager->persist($newMainDetail);
1380
        $this->manager->persist($currentMainDetail);
1381
        $this->manager->persist($article);
1382
        $this->manager->flush();
1383
    }
1384
1385
    /**
1386
     * Updates the status of an Order
1387
     *
1388
     * @param string $localOrderId
1389
     * @param string $orderStatus
1390
     * @param string $trackingNumber
1391
     * @return void
1392
     */
1393
    public function updateOrderStatus($localOrderId, $orderStatus, $trackingNumber)
1394
    {
1395
        if ($this->config->getConfig('updateOrderStatus') == 1) {
1396
            $this->updateDeliveryStatus($localOrderId, $orderStatus);
1397
        }
1398
1399
        if ($trackingNumber) {
1400
            $this->updateTrackingNumber($localOrderId, $trackingNumber);
1401
        }
1402
    }
1403
1404
    /**
1405
     * @param string $localOrderId
1406
     * @param string $orderStatus
1407
     */
1408
    private function updateDeliveryStatus($localOrderId, $orderStatus)
1409
    {
1410
        $status = false;
1411
        if ($orderStatus === OrderStatus::STATE_IN_PROCESS) {
1412
            $status = Status::ORDER_STATE_PARTIALLY_DELIVERED;
1413
        } elseif ($orderStatus === OrderStatus::STATE_DELIVERED) {
1414
            $status = Status::ORDER_STATE_COMPLETELY_DELIVERED;
1415
        }
1416
1417
        if ($status) {
1418
            $this->manager->getConnection()->executeQuery(
1419
                'UPDATE s_order 
1420
                SET status = :orderStatus
1421
                WHERE ordernumber = :orderNumber',
1422
                [
1423
                    ':orderStatus' => $status,
1424
                    ':orderNumber' => $localOrderId
1425
                ]
1426
            );
1427
        }
1428
    }
1429
1430
    /**
1431
     * @param string $localOrderId
1432
     * @param string $trackingNumber
1433
     */
1434
    private function updateTrackingNumber($localOrderId, $trackingNumber)
1435
    {
1436
        $currentTrackingCode = $this->manager->getConnection()->fetchColumn(
1437
            'SELECT trackingcode
1438
            FROM s_order
1439
            WHERE ordernumber = :orderNumber',
1440
            [
1441
                ':orderNumber' => $localOrderId
1442
            ]
1443
        );
1444
1445
        if (!$currentTrackingCode) {
1446
            $newTracking = $trackingNumber;
1447
        } else {
1448
            $newTracking = $this->combineTrackingNumbers($trackingNumber, $currentTrackingCode);
1449
        }
1450
1451
        $this->manager->getConnection()->executeQuery(
1452
            'UPDATE s_order 
1453
            SET trackingcode = :trackingCode
1454
            WHERE ordernumber = :orderNumber',
1455
            [
1456
                ':trackingCode' => $newTracking,
1457
                ':orderNumber' => $localOrderId
1458
            ]
1459
        );
1460
    }
1461
1462
    /**
1463
     * @param string $newTrackingCode
1464
     * @param string $currentTrackingCode
1465
     * @return string
1466
     */
1467
    private function combineTrackingNumbers($newTrackingCode, $currentTrackingCode)
1468
    {
1469
        $currentTrackingCodes = $this->getTrackingNumberAsArray($currentTrackingCode);
1470
        $newTrackingCodes = $this->getTrackingNumberAsArray($newTrackingCode);
1471
        $newTrackingCodes = array_unique(array_merge($currentTrackingCodes, $newTrackingCodes));
1472
        $newTracking = implode(',', $newTrackingCodes);
1473
1474
        return $newTracking;
1475
    }
1476
1477
    /**
1478
     * @param string $trackingCode
1479
     * @return string[]
1480
     */
1481
    private function getTrackingNumberAsArray($trackingCode)
1482
    {
1483
        if (strpos($trackingCode, ',') !== false) {
1484
            return explode(',', $trackingCode);
1485
        }
1486
1487
        return [$trackingCode];
1488
    }
1489
1490
    /**
1491
     * @param Product $product
1492
     * @param ProductModel $model
1493
     */
1494
    private function saveVat(Product $product, ProductModel $model)
1495
    {
1496
        if ($product->vat !== null) {
1497
            $repo = $this->manager->getRepository(Tax::class);
1498
            $taxRate = round($product->vat * 100, 2);
1499
            /** @var \Shopware\Models\Tax\Tax $tax */
1500
            $tax = $repo->findOneBy(['tax' => $taxRate]);
1501
            if (!$tax) {
1502
                $tax = new Tax();
1503
                $tax->setTax($taxRate);
1504
                //this is to get rid of zeroes behind the decimal point
1505
                $name = strval(round($taxRate, 2)) . '%';
1506
                $tax->setName($name);
1507
                $this->manager->persist($tax);
1508
            }
1509
            $model->setTax($tax);
1510
        }
1511
    }
1512
1513
    /**
1514
     * @param int $articleId
1515
     * @param Product $product
1516
     */
1517
    private function applyCrossSelling($articleId, Product $product)
1518
    {
1519
        $this->deleteRemovedRelations($articleId, $product);
1520
        $this->storeCrossSellingInformationInverseSide($articleId, $product->sourceId, $product->shopId);
1521
        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...
1522
            $this->storeCrossSellingInformationOwningSide($articleId, $product);
1523
        }
1524
    }
1525
1526
    /**
1527
     * @param int $articleId
1528
     * @param Product $product
1529
     */
1530
    private function storeCrossSellingInformationOwningSide($articleId, $product)
1531
    {
1532
        foreach ($product->related as $relatedId) {
1533
            $this->insertNewRelations($articleId, $product->shopId, $relatedId, self::RELATION_TYPE_RELATED);
1534
        }
1535
1536
        foreach ($product->similar as $similarId) {
1537
            $this->insertNewRelations($articleId, $product->shopId, $similarId, self::RELATION_TYPE_SIMILAR);
1538
        }
1539
    }
1540
1541
    /**
1542
     * @param int $articleId
1543
     * @param int $shopId
1544
     * @param int $relatedId
1545
     * @param string $relationType
1546
     */
1547
    private function insertNewRelations($articleId, $shopId, $relatedId, $relationType)
1548
    {
1549
        $inserted = false;
1550
        try {
1551
            $this->manager->getConnection()->executeQuery(
1552
                'INSERT INTO s_plugin_connect_article_relations (article_id, shop_id, related_article_local_id, relationship_type) VALUES (?, ?, ?, ?)',
1553
                [$articleId, $shopId, $relatedId, $relationType]
1554
            );
1555
            $inserted = true;
1556
        } 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...
1557
            // No problems here. Just means that the row already existed.
1558
        }
1559
1560
        //outside of try catch because we don't want to catch exceptions -> this method should not throw any
1561
        if ($inserted) {
1562
            $relatedLocalId = $this->manager->getConnection()->fetchColumn(
1563
                'SELECT article_id FROM s_plugin_connect_items WHERE shop_id = ? AND source_id = ?',
1564
                [$shopId, $relatedId]
1565
            );
1566
            if ($relatedLocalId) {
1567
                $this->manager->getConnection()->executeQuery(
1568
                    "INSERT IGNORE INTO s_articles_$relationType (articleID, relatedarticle) VALUES (?, ?)",
1569
                    [$articleId, $relatedLocalId]
1570
                );
1571
            }
1572
        }
1573
    }
1574
1575
    /**
1576
     * @param int $articleId
1577
     * @param string $sourceId
1578
     * @param int $shopId
1579
     */
1580
    private function storeCrossSellingInformationInverseSide($articleId, $sourceId, $shopId)
1581
    {
1582
        $relatedArticles = $this->manager->getConnection()->fetchAll(
1583
            'SELECT article_id, relationship_type FROM s_plugin_connect_article_relations WHERE shop_id = ? AND related_article_local_id = ?',
1584
            [$shopId, $sourceId]
1585
        );
1586
1587
        foreach ($relatedArticles as $relatedArticle) {
1588
            $relationType = $relatedArticle['relationship_type'];
1589
            $this->manager->getConnection()->executeQuery(
1590
                "INSERT IGNORE INTO s_articles_$relationType (articleID, relatedarticle) VALUES (?, ?)",
1591
                [$relatedArticle['article_id'], $articleId]
1592
            );
1593
        }
1594
    }
1595
1596
    /**
1597
     * @param $articleId
1598
     * @param $product
1599
     */
1600
    private function deleteRemovedRelations($articleId, $product)
1601
    {
1602 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...
1603
            $this->manager->getConnection()->executeQuery(
1604
                'DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND related_article_local_id NOT IN (?) AND relationship_type = ?',
1605
                [$articleId, $product->shopId, $product->related, self::RELATION_TYPE_RELATED],
1606
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR]
1607
            );
1608
1609
            $oldRelatedIds = $this->manager->getConnection()->executeQuery(
1610
                'SELECT ar.id 
1611
                FROM s_articles_relationships AS ar
1612
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1613
                WHERE ar.articleID = ? AND ci.shop_id = ? AND ci.source_id NOT IN (?)',
1614
                [$articleId, $product->shopId, $product->related],
1615
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
1616
            )
1617
                ->fetchAll(\PDO::FETCH_COLUMN);
1618
        } else {
1619
            $this->manager->getConnection()->executeQuery(
1620
                'DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND relationship_type = ?',
1621
                [$articleId, $product->shopId, self::RELATION_TYPE_RELATED]
1622
            );
1623
1624
            $oldRelatedIds = $this->manager->getConnection()->executeQuery(
1625
                'SELECT ar.id 
1626
                FROM s_articles_relationships AS ar
1627
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1628
                WHERE ar.articleID = ? AND ci.shop_id = ?',
1629
                [$articleId, $product->shopId]
1630
            )
1631
                ->fetchAll(\PDO::FETCH_COLUMN);
1632
        }
1633
1634
        $this->manager->getConnection()->executeQuery(
1635
            'DELETE FROM s_articles_relationships WHERE id IN (?)',
1636
            [$oldRelatedIds],
1637
            [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
1638
        );
1639
1640 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...
1641
            $this->manager->getConnection()->executeQuery(
1642
                'DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND related_article_local_id NOT IN (?) AND relationship_type = ?',
1643
                [$articleId, $product->shopId, $product->similar, self::RELATION_TYPE_SIMILAR],
1644
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR]
1645
            );
1646
1647
            $oldSimilarIds = $this->manager->getConnection()->executeQuery(
1648
                'SELECT ar.id 
1649
                FROM s_articles_similar AS ar
1650
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1651
                WHERE ar.articleID = ? AND ci.shop_id = ? AND ci.source_id NOT IN (?)',
1652
                [$articleId, $product->shopId, $product->similar],
1653
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
1654
            )
1655
                ->fetchAll(\PDO::FETCH_COLUMN);
1656
        } else {
1657
            $this->manager->getConnection()->executeQuery(
1658
                'DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND relationship_type = ?',
1659
                [$articleId, $product->shopId, self::RELATION_TYPE_SIMILAR]
1660
            );
1661
1662
            $oldSimilarIds = $this->manager->getConnection()->executeQuery(
1663
                'SELECT ar.id 
1664
                FROM s_articles_similar AS ar
1665
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1666
                WHERE ar.articleID = ? AND ci.shop_id = ?',
1667
                [$articleId, $product->shopId]
1668
            )
1669
                ->fetchAll(\PDO::FETCH_COLUMN);
1670
        }
1671
1672
        $this->manager->getConnection()->executeQuery(
1673
            'DELETE FROM s_articles_similar WHERE id IN (?)',
1674
            [$oldSimilarIds],
1675
            [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
1676
        );
1677
    }
1678
1679
    /**
1680
     * @param Property $property
1681
     * @return PropertyGroup
1682
     */
1683
    private function createPropertyGroup(Property $property)
1684
    {
1685
        $group = new PropertyGroup();
1686
        $group->setName($property->groupName);
1687
        $group->setComparable($property->comparable);
1688
        $group->setSortMode($property->sortMode);
1689
        $group->setPosition($property->groupPosition);
1690
1691
        $attribute = new \Shopware\Models\Attribute\PropertyGroup();
1692
        $attribute->setPropertyGroup($group);
1693
        $attribute->setConnectIsRemote(true);
1694
        $group->setAttribute($attribute);
1695
1696
        $this->manager->persist($attribute);
1697
        $this->manager->persist($group);
1698
        $this->manager->flush();
1699
1700
        return $group;
1701
    }
1702
}
1703