Completed
Pull Request — master (#415)
by Jonas
03:12
created

ProductToShop::getLocaleRepository()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 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
            // Reload the article detail model in order to not to work an the already flushed model
342
            $detail = $this->helper->getArticleDetailModelByProduct($product);
343
            // import only specific images for variant
344
            $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 342 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...
345
        }
346
347
        $this->eventManager->notify(
348
            'Connect_ProductToShop_InsertOrUpdate_After',
349
            [
350
                'connectProduct' => $product,
351
                'shopArticleDetail' => $detail
352
            ]
353
        );
354
355
        $stream = $this->getOrCreateStream($product);
356
        $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...
357
    }
358
359
    /**
360
     * @param Product $product
361
     * @return string
362
     */
363
    private function generateSKU(Product $product)
364
    {
365
        if (!empty($product->sku)) {
366
            $number = 'SC-' . $product->shopId . '-' . $product->sku;
367
            $duplicatedDetail = $this->helper->getDetailByNumber($number);
368
            if ($duplicatedDetail
369
                && $this->helper->getConnectAttributeByModel($duplicatedDetail)->getSourceId() != $product->sourceId
370
            ) {
371
                $this->deleteDetail($duplicatedDetail);
372
            }
373
        } else {
374
            $number = 'SC-' . $product->shopId . '-' . $product->sourceId;
375
        }
376
377
        return $number;
378
    }
379
380
    /**
381
     * @param DetailModel $detailModel
382
     */
383
    private function deleteDetail(DetailModel $detailModel)
384
    {
385
        $this->eventManager->notify(
386
            'Connect_Merchant_Delete_Product_Before',
387
            [
388
                'subject' => $this,
389
                'articleDetail' => $detailModel
390
            ]
391
        );
392
393
        $article = $detailModel->getArticle();
394
        // Not sure why, but the Attribute can be NULL
395
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
396
        $this->manager->remove($detailModel);
397
398
        if ($attribute) {
399
            $this->manager->remove($attribute);
400
        }
401
402
        // if removed variant is main variant
403
        // find first variant which is not main and mark it
404
        if ($detailModel->getKind() === 1) {
405
            /** @var \Shopware\Models\Article\Detail $variant */
406
            foreach ($article->getDetails() as $variant) {
407
                if ($variant->getId() != $detailModel->getId()) {
408
                    $variant->setKind(1);
409
                    $article->setMainDetail($variant);
410
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
411
                    if (!$connectAttribute) {
412
                        continue;
413
                    }
414
                    $connectAttribute->setIsMainVariant(true);
415
                    $this->manager->persist($connectAttribute);
416
                    $this->manager->persist($article);
417
                    $this->manager->persist($variant);
418
                    break;
419
                }
420
            }
421
        }
422
423
        if (count($details = $article->getDetails()) === 1) {
424
            $details->clear();
425
            $this->manager->remove($article);
426
        }
427
428
        //save category Ids before flush
429
        $oldCategoryIds = array_map(function ($category) {
430
            return $category->getId();
431
        }, $article->getCategories()->toArray());
432
433
        // Do not remove flush. It's needed when remove article,
434
        // because duplication of ordernumber. Even with remove before
435
        // persist calls mysql throws exception "Duplicate entry"
436
        $this->manager->flush();
437
        // always clear entity manager, because $article->getDetails() returns
438
        // more than 1 detail, but all of them were removed except main one.
439
        $this->manager->clear();
440
441
        // call this after flush because article has to be deleted that this works
442
        if (count($oldCategoryIds) > 0) {
443
            $this->categoryResolver->deleteEmptyConnectCategories($oldCategoryIds);
444
        }
445
    }
446
447
    /**
448
     * @param Product $product
449
     * @param $active
450
     * @param $isMainVariant
451
     * @return null|Article
452
     */
453
    private function getSWProductModel(Product $product, $active, &$isMainVariant)
454
    {
455
        if ($product->groupId !== null) {
456
            $model = $this->helper->getArticleByRemoteProduct($product);
457
            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...
458
                $model = $this->helper->createProductModel($product);
459
                $model->setActive($active);
460
                $isMainVariant = true;
461
            }
462
        } else {
463
            $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
464
            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...
465
                $model = $this->helper->createProductModel($product);
466
                $model->setActive($active);
467
            }
468
        }
469
470
        return $model;
471
    }
472
473
    /**
474
     * @param Product $product
475
     * @param $model
476
     * @return DetailModel
477
     */
478
    private function generateNewDetail(Product $product, $model)
479
    {
480
        $detail = new DetailModel();
481
        $detail->setActive($model->getActive());
482
        $this->manager->persist($detail);
483
        $detail->setArticle($model);
484
        $model->getDetails()->add($detail);
485
        if (!empty($product->variant)) {
486
            $this->variantConfigurator->configureVariantAttributes($product, $detail);
487
        }
488
489
        return $detail;
490
    }
491
492
    /**
493
     * @param DetailModel $detail
494
     * @param DetailModel $mainDetail
495
     * @return bool
496
     */
497
    private function checkIfMainVariant(DetailModel $detail, DetailModel $mainDetail)
498
    {
499
        return $detail->getId() === $mainDetail->getId();
500
    }
501
502
    /**
503
     * @param ProductModel $model
504
     * @param Product $product
505
     */
506
    private function updateConfiguratorSetTypeFromProduct(ProductModel $model, Product $product)
507
    {
508
        if (!empty($product->variant)) {
509
            $configSet = $model->getConfiguratorSet();
510
            $configSet->setType($product->configuratorSetType);
511
        }
512
    }
513
514
    /**
515
     * @param ProductModel $model
516
     * @param Product $product
517
     */
518
    private function cleanUpConfiguratorSet(ProductModel $model, Product $product)
519
    {
520
        if (empty($product->variant) && $model->getConfiguratorSet()) {
521
            $this->manager->getConnection()->executeQuery(
522
                'UPDATE s_articles SET configurator_set_id = NULL WHERE id = ?',
523
                [$model->getId()]
524
            );
525
        }
526
    }
527
528
    /**
529
     * @param DetailModel $detail
530
     * @param ProductModel $model
531
     * @return AttributeModel
532
     */
533
    private function getOrCreateAttributeModel(DetailModel $detail, ProductModel $model)
534
    {
535
        $detailAttribute = $detail->getAttribute();
536
        if (!$detailAttribute) {
537
            $detailAttribute = new AttributeModel();
538
            $detail->setAttribute($detailAttribute);
539
            $model->setAttribute($detailAttribute);
540
            $detailAttribute->setArticle($model);
541
            $detailAttribute->setArticleDetail($detail);
542
        }
543
544
        return $detailAttribute;
545
    }
546
547
    /**
548
     * Get array of update info for the known fields
549
     *
550
     * @param $model
551
     * @param $detail
552
     * @param $attribute
553
     * @param $product
554
     * @return array
555
     */
556
    public function getUpdateFields($model, $detail, $attribute, $product)
557
    {
558
        // This also defines the flags of these fields
559
        $fields = $this->helper->getUpdateFlags();
560
        $flagsByName = array_flip($fields);
561
562
        $flag = 0;
563
        $output = [];
564
        foreach ($fields as $key => $field) {
565
            // Don't handle the imageInitialImport flag
566
            if ($field == 'imageInitialImport') {
567
                continue;
568
            }
569
570
            // If this is a new product
571
            if (!$model->getId() && $field == 'image' && !$this->config->getConfig('importImagesOnFirstImport',
572
                    false)) {
573
                $output[$field] = false;
574
                $flag |= $flagsByName['imageInitialImport'];
575
                continue;
576
            }
577
578
            $updateAllowed = $this->isFieldUpdateAllowed($field, $model, $attribute);
579
            $output[$field] = $updateAllowed;
580
            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...
581
                $flag |= $key;
582
            }
583
        }
584
585
        return [$output, $flag];
586
    }
587
588
    /**
589
     * Helper method to determine if a given $fields may/must be updated.
590
     * This method will check for the model->id in order to determine, if it is a new entity. Therefore
591
     * this method cannot be used after the model in question was already flushed.
592
     *
593
     * @param $field
594
     * @param $model ProductModel
595
     * @param $attribute ConnectAttribute
596
     * @throws \RuntimeException
597
     * @return bool|null
598
     */
599
    public function isFieldUpdateAllowed($field, ProductModel $model, ConnectAttribute $attribute)
600
    {
601
        $allowed = [
602
            'ShortDescription',
603
            'LongDescription',
604
            'AdditionalDescription',
605
            'Image',
606
            'Price',
607
            'Name',
608
        ];
609
610
        // Always allow updates for new models
611
        if (!$model->getId()) {
612
            return true;
613
        }
614
615
        $field = ucfirst($field);
616
        $attributeGetter = 'getUpdate' . $field;
617
        $configName = 'overwriteProduct' . $field;
618
619
        if (!in_array($field, $allowed)) {
620
            throw new \RuntimeException("Unknown update field {$field}");
621
        }
622
623
        $attributeValue = $attribute->$attributeGetter();
624
625
626
        // If the value is 'null' or 'inherit', the behaviour will be inherited from the global configuration
627
        // Once we have a supplier based configuration, we need to take it into account here
628
        if ($attributeValue == null || $attributeValue == 'inherit') {
629
            return $this->config->getConfig($configName, true);
630
        }
631
632
        return $attributeValue == 'overwrite';
633
    }
634
635
    /**
636
     * Determine if a given field has changed
637
     *
638
     * @param $field
639
     * @param ProductModel $model
640
     * @param DetailModel $detail
641
     * @param Product $product
642
     * @return bool
643
     */
644
    public function hasFieldChanged($field, ProductModel $model, DetailModel $detail, Product $product)
645
    {
646
        switch ($field) {
647
            case 'shortDescription':
648
                return $model->getDescription() != $product->shortDescription;
649
            case 'longDescription':
650
                return $model->getDescriptionLong() != $product->longDescription;
651
            case 'additionalDescription':
652
                return $detail->getAttribute()->getConnectProductDescription() != $product->additionalDescription;
653
            case 'name':
654
                return $model->getName() != $product->title;
655
            case 'image':
656
                return count($model->getImages()) != count($product->images);
657
            case 'price':
658
                $prices = $detail->getPrices();
659
                if (empty($prices)) {
660
                    return true;
661
                }
662
                $price = $prices->first();
663
                if (!$price) {
664
                    return true;
665
                }
666
667
                return $prices->first()->getPrice() != $product->price;
668
        }
669
670
        throw new \InvalidArgumentException('Unrecognized field');
671
    }
672
673
    /**
674
     * @param array $updateFields
675
     * @param ProductModel $model
676
     * @param AttributeModel $detailAttribute
677
     * @param Product $product
678
     */
679
    private function setPropertiesForNewProducts(array $updateFields, ProductModel $model, AttributeModel $detailAttribute, Product $product)
680
    {
681
        /*
682
         * Make sure, that the following properties are set for
683
         * - new products
684
         * - products that have been configured to receive these updates
685
         */
686
        if ($updateFields['name']) {
687
            $model->setName($product->title);
688
        }
689
        if ($updateFields['shortDescription']) {
690
            $model->setDescription($product->shortDescription);
691
        }
692
        if ($updateFields['longDescription']) {
693
            $model->setDescriptionLong($product->longDescription);
694
        }
695
696
        if ($updateFields['additionalDescription']) {
697
            $detailAttribute->setConnectProductDescription($product->additionalDescription);
698
        }
699
700
        if ($product->vat !== null) {
701
            $repo = $this->manager->getRepository('Shopware\Models\Tax\Tax');
702
            $tax = round($product->vat * 100, 2);
703
            /** @var \Shopware\Models\Tax\Tax $tax */
704
            $tax = $repo->findOneBy(['tax' => $tax]);
705
            $model->setTax($tax);
706
        }
707
708
        if ($product->vendor !== null) {
709
            $repo = $this->manager->getRepository('Shopware\Models\Article\Supplier');
710
            $supplier = $repo->findOneBy(['name' => $product->vendor]);
711
            if ($supplier === null) {
712
                $supplier = $this->createSupplier($product->vendor);
713
            }
714
            $model->setSupplier($supplier);
715
        }
716
    }
717
718
    /**
719
     * @param $vendor
720
     * @return Supplier
721
     */
722
    private function createSupplier($vendor)
723
    {
724
        $supplier = new Supplier();
725
726
        if (is_array($vendor)) {
727
            $supplier->setName($vendor['name']);
728
            $supplier->setDescription($vendor['description']);
729
            if (array_key_exists('url', $vendor) && $vendor['url']) {
730
                $supplier->setLink($vendor['url']);
731
            }
732
733
            $supplier->setMetaTitle($vendor['page_title']);
734
735
            if (array_key_exists('logo_url', $vendor) && $vendor['logo_url']) {
736
                $this->imageImport->importImageForSupplier($vendor['logo_url'], $supplier);
737
            }
738
        } else {
739
            $supplier->setName($vendor);
740
        }
741
742
        //sets supplier attributes
743
        $attr = new \Shopware\Models\Attribute\ArticleSupplier();
744
        $attr->setConnectIsRemote(true);
745
746
        $supplier->setAttribute($attr);
747
748
        return $supplier;
749
    }
750
751
    /**
752
     * @param ProductModel $article
753
     * @param Product $product
754
     */
755
    private function applyProductProperties(ProductModel $article, Product $product)
756
    {
757
        if (empty($product->properties)) {
758
            return;
759
        }
760
761
        /** @var Property $firstProperty */
762
        $firstProperty = reset($product->properties);
763
        $groupRepo = $this->manager->getRepository(PropertyGroup::class);
764
        $group = $groupRepo->findOneBy(['name' => $firstProperty->groupName]);
765
766
        if (!$group) {
767
            $group = $this->createPropertyGroup($firstProperty);
768
        }
769
770
        $propertyValues = $article->getPropertyValues();
771
        $propertyValues->clear();
772
        $this->manager->persist($article);
773
        $this->manager->flush();
774
775
        $article->setPropertyGroup($group);
776
777
        $optionRepo = $this->manager->getRepository(PropertyOption::class);
778
        $valueRepo = $this->manager->getRepository(PropertyValue::class);
779
780
        foreach ($product->properties as $property) {
781
            $option = $optionRepo->findOneBy(['name' => $property->option]);
782
            $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...
783
            if (!$option) {
784
                $option = new PropertyOption();
785
                $option->setName($property->option);
786
                $option->setFilterable($property->filterable);
787
788
                $attribute = new \Shopware\Models\Attribute\PropertyOption();
789
                $attribute->setPropertyOption($option);
790
                $attribute->setConnectIsRemote(true);
791
                $option->setAttribute($attribute);
792
793
                $this->manager->persist($option);
794
                $this->manager->flush($option);
795
            }
796
797
            if (!$optionExists || !$value = $valueRepo->findOneBy(['option' => $option, 'value' => $property->value])) {
798
                $value = new PropertyValue($option, $property->value);
799
                $value->setPosition($property->valuePosition);
800
801
                $attribute = new \Shopware\Models\Attribute\PropertyValue();
802
                $attribute->setPropertyValue($value);
803
                $attribute->setConnectIsRemote(true);
804
                $value->setAttribute($attribute);
805
806
                $this->manager->persist($value);
807
            }
808
809
            if (!$propertyValues->contains($value)) {
810
                //add only new values
811
                $propertyValues->add($value);
812
            }
813
814
            $filters = [
815
                ['property' => 'options.name', 'expression' => '=', 'value' => $property->option],
816
                ['property' => 'groups.name', 'expression' => '=', 'value' => $property->groupName],
817
            ];
818
819
            $query = $groupRepo->getPropertyRelationQuery($filters, null, 1, 0);
820
            $relation = $query->getOneOrNullResult();
821
822
            if (!$relation) {
823
                $group->addOption($option);
824
                $this->manager->persist($group);
825
                $this->manager->flush($group);
826
            }
827
        }
828
829
        $article->setPropertyValues($propertyValues);
830
831
        $this->manager->persist($article);
832
        $this->manager->flush();
833
    }
834
835
    /**
836
     * Read product attributes mapping and set to shopware attribute model
837
     *
838
     * @param AttributeModel $detailAttribute
839
     * @param Product $product
840
     * @return AttributeModel
841
     */
842
    private function applyMarketplaceAttributes(AttributeModel $detailAttribute, Product $product)
843
    {
844
        $detailAttribute->setConnectReference($product->sourceId);
845
        $detailAttribute->setConnectArticleShipping($product->shipping);
846
        //todo@sb: check if connectAttribute matches position of the marketplace attribute
847
        array_walk($product->attributes, function ($value, $key) use ($detailAttribute) {
848
            $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...
849
            if (strlen($shopwareAttribute) > 0) {
850
                $setter = 'set' . ucfirst($shopwareAttribute);
851
                $detailAttribute->$setter($value);
852
            }
853
        });
854
855
        return $detailAttribute;
856
    }
857
858
    /**
859
     * @param  ConnectAttribute $connectAttribute
860
     * @param Product $product
861
     */
862
    private function setConnectAttributesFromProduct(ConnectAttribute $connectAttribute, Product $product)
863
    {
864
        $connectAttribute->setShopId($product->shopId);
865
        $connectAttribute->setSourceId($product->sourceId);
866
        $connectAttribute->setExportStatus(null);
867
        $connectAttribute->setPurchasePrice($product->purchasePrice);
868
        $connectAttribute->setFixedPrice($product->fixedPrice);
869
        $connectAttribute->setStream($product->stream);
870
871
        // store purchasePriceHash and offerValidUntil
872
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
873
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
874
    }
875
876
    /**
877
     * @param DetailModel $detail
878
     * @param Product $product
879
     */
880
    private function updateDetailFromProduct(DetailModel $detail, Product $product)
881
    {
882
        $detail->setInStock($product->availability);
883
        $detail->setEan($product->ean);
884
        $detail->setShippingTime($product->deliveryWorkDays);
885
        $releaseDate = new \DateTime();
886
        $releaseDate->setTimestamp($product->deliveryDate);
887
        $detail->setReleaseDate($releaseDate);
888
        $detail->setMinPurchase($product->minPurchaseQuantity);
889
    }
890
891
    /**
892
     * @param DetailModel $detail
893
     * @param Product $product
894
     * @param $detailAttribute
895
     */
896
    private function detailSetUnit(DetailModel $detail, Product $product, $detailAttribute)
897
    {
898
        // if connect product has unit
899
        // find local unit with units mapping
900
        // and add to detail model
901
        if (array_key_exists('unit', $product->attributes) && $product->attributes['unit']) {
902
            $detailAttribute->setConnectRemoteUnit($product->attributes['unit']);
903
            if ($this->config->getConfig($product->attributes['unit']) == null) {
904
                $this->config->setConfig($product->attributes['unit'], '', null, 'units');
905
            }
906
907
            /** @var \ShopwarePlugins\Connect\Components\Utils\UnitMapper $unitMapper */
908
            $unitMapper = new UnitMapper($this->config, $this->manager);
909
910
            $shopwareUnit = $unitMapper->getShopwareUnit($product->attributes['unit']);
911
912
            /** @var \Shopware\Models\Article\Unit $unit */
913
            $unit = $this->helper->getUnit($shopwareUnit);
914
            $detail->setUnit($unit);
915
            $detail->setPurchaseUnit($product->attributes['quantity']);
916
            $detail->setReferenceUnit($product->attributes['ref_quantity']);
917
        } else {
918
            $detail->setUnit(null);
919
            $detail->setPurchaseUnit(null);
920
            $detail->setReferenceUnit(null);
921
        }
922
    }
923
924
    /**
925
     * @param DetailModel $detail
926
     * @param Product $product
927
     */
928
    private function detailSetAttributes(DetailModel $detail, Product $product)
929
    {
930
        // set dimension
931
        if (array_key_exists('dimension', $product->attributes) && $product->attributes['dimension']) {
932
            $dimension = explode('x', $product->attributes['dimension']);
933
            $detail->setLen($dimension[0]);
934
            $detail->setWidth($dimension[1]);
935
            $detail->setHeight($dimension[2]);
936
        } else {
937
            $detail->setLen(null);
938
            $detail->setWidth(null);
939
            $detail->setHeight(null);
940
        }
941
942
        // set weight
943
        if (array_key_exists('weight', $product->attributes) && $product->attributes['weight']) {
944
            $detail->setWeight($product->attributes['weight']);
945
        }
946
947
        //set package unit
948
        if (array_key_exists(Product::ATTRIBUTE_PACKAGEUNIT, $product->attributes)) {
949
            $detail->setPackUnit($product->attributes[Product::ATTRIBUTE_PACKAGEUNIT]);
950
        }
951
952
        //set basic unit
953
        if (array_key_exists(Product::ATTRIBUTE_BASICUNIT, $product->attributes)) {
954
            $detail->setMinPurchase($product->attributes[Product::ATTRIBUTE_BASICUNIT]);
955
        }
956
957
        //set manufacturer no.
958
        if (array_key_exists(Product::ATTRIBUTE_MANUFACTURERNUMBER, $product->attributes)) {
959
            $detail->setSupplierNumber($product->attributes[Product::ATTRIBUTE_MANUFACTURERNUMBER]);
960
        }
961
    }
962
963
    /**
964
     * @param ConnectAttribute $connectAttribute
965
     * @param Product $product
966
     */
967
    private function connectAttributeSetLastUpdate(ConnectAttribute $connectAttribute, Product $product)
968
    {
969
        // Whenever a product is updated, store a json encoded list of all fields that are updated optionally
970
        // This way a customer will be able to apply the most recent changes any time later
971
        $connectAttribute->setLastUpdate(json_encode([
972
            'shortDescription' => $product->shortDescription,
973
            'longDescription' => $product->longDescription,
974
            'additionalDescription' => $product->additionalDescription,
975
            'purchasePrice' => $product->purchasePrice,
976
            'image' => $product->images,
977
            'variantImages' => $product->variantImages,
978
            'price' => $product->price * ($product->vat + 1),
979
            'name' => $product->title,
980
            'vat' => $product->vat
981
        ]));
982
    }
983
984
    /**
985
     * @param ProductModel $model
986
     * @param array $categories
987
     */
988
    private function categoryDenormalization(ProductModel $model, array $categories)
989
    {
990
        $this->categoryDenormalization->disableTransactions();
991
        foreach ($categories as $category) {
992
            $this->categoryDenormalization->addAssignment($model->getId(), $category);
993
            $this->manager->getConnection()->executeQuery(
994
                'INSERT IGNORE INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES (?,?)',
995
                [$model->getId(), $category]
996
            );
997
            $parentId =$this->manager->getConnection()->fetchColumn(
998
                'SELECT parent FROM `s_categories` WHERE id = ?',
999
                [$category]
1000
            );
1001
            $this->categoryDenormalization->removeAssignment($model->getId(), $parentId);
1002
            $this->manager->getConnection()->executeQuery(
1003
                'DELETE FROM `s_articles_categories` WHERE `articleID` = ? AND `categoryID` = ?',
1004
                [$model->getId(), $parentId]
1005
            );
1006
        }
1007
        $this->categoryDenormalization->enableTransactions();
1008
    }
1009
1010
    /**
1011
     * @param Product $product
1012
     * @return ProductStream
1013
     */
1014
    private function getOrCreateStream(Product $product)
1015
    {
1016
        /** @var ProductStreamRepository $repo */
1017
        $repo = $this->manager->getRepository(ProductStreamAttribute::class);
1018
        $stream = $repo->findConnectByName($product->stream);
1019
1020
        if (!$stream) {
1021
            $stream = new ProductStream();
1022
            $stream->setName($product->stream);
1023
            $stream->setType(ProductStreamService::STATIC_STREAM);
1024
            $stream->setSorting(json_encode(
1025
                [ReleaseDateSorting::class => ['direction' => 'desc']]
1026
            ));
1027
1028
            //add attributes
1029
            $attribute = new \Shopware\Models\Attribute\ProductStream();
1030
            $attribute->setProductStream($stream);
1031
            $attribute->setConnectIsRemote(true);
1032
            $stream->setAttribute($attribute);
1033
1034
            $this->manager->persist($attribute);
1035
            $this->manager->persist($stream);
1036
            $this->manager->flush();
1037
        }
1038
1039
        return $stream;
1040
    }
1041
1042
    /**
1043
     * @param ProductStream $stream
1044
     * @param ProductModel $article
1045
     * @throws \Doctrine\DBAL\DBALException
1046
     */
1047
    private function addProductToStream(ProductStream $stream, ProductModel $article)
1048
    {
1049
        $conn = $this->manager->getConnection();
1050
        $sql = 'INSERT INTO `s_product_streams_selection` (`stream_id`, `article_id`)
1051
                VALUES (:streamId, :articleId)
1052
                ON DUPLICATE KEY UPDATE stream_id = :streamId, article_id = :articleId';
1053
        $stmt = $conn->prepare($sql);
1054
        $stmt->execute([':streamId' => $stream->getId(), ':articleId' => $article->getId()]);
1055
    }
1056
1057
    /**
1058
     * Set detail purchase price with plain SQL
1059
     * Entity usage throws exception when error handlers are disabled
1060
     *
1061
     * @param ProductModel $article
1062
     * @param DetailModel $detail
1063
     * @param Product $product
1064
     * @throws \Doctrine\DBAL\DBALException
1065
     */
1066
    private function setPrice(ProductModel $article, DetailModel $detail, Product $product)
1067
    {
1068
        // set price via plain SQL because shopware throws exception
1069
        // undefined index: key when error handler is disabled
1070
        $customerGroup = $this->helper->getDefaultCustomerGroup();
1071
1072
        if (!empty($product->priceRanges)) {
1073
            $this->setPriceRange($article, $detail, $product->priceRanges, $customerGroup);
1074
1075
            return;
1076
        }
1077
1078
        $id = $this->manager->getConnection()->fetchColumn(
1079
            'SELECT id FROM `s_articles_prices`
1080
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1081
            [$customerGroup->getKey(), 1, 'beliebig', $article->getId(), $detail->getId()]
1082
        );
1083
1084
        // todo@sb: test update prices
1085
        if ($id > 0) {
1086
            $this->manager->getConnection()->executeQuery(
1087
                'UPDATE `s_articles_prices` SET `price` = ?, `baseprice` = ? WHERE `id` = ?',
1088
                [$product->price, $product->purchasePrice, $id]
1089
            );
1090
        } else {
1091
            $this->manager->getConnection()->executeQuery(
1092
                'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `baseprice`)
1093
              VALUES (?, 1, "beliebig", ?, ?, ?, ?);',
1094
                [
1095
                    $customerGroup->getKey(),
1096
                    $article->getId(),
1097
                    $detail->getId(),
1098
                    $product->price,
1099
                    $product->purchasePrice
1100
                ]
1101
            );
1102
        }
1103
    }
1104
1105
    /**
1106
     * @param ProductModel $article
1107
     * @param DetailModel $detail
1108
     * @param array $priceRanges
1109
     * @param Group $group
1110
     * @throws \Doctrine\DBAL\ConnectionException
1111
     * @throws \Exception
1112
     */
1113
    private function setPriceRange(ProductModel $article, DetailModel $detail, array $priceRanges, Group $group)
1114
    {
1115
        $this->manager->getConnection()->beginTransaction();
1116
1117
        try {
1118
            // We always delete the prices,
1119
            // because we can not know which record is update
1120
            $this->manager->getConnection()->executeQuery(
1121
                'DELETE FROM `s_articles_prices` WHERE `articleID` = ? AND `articledetailsID` = ?',
1122
                [$article->getId(), $detail->getId()]
1123
            );
1124
1125
            /** @var PriceRange $priceRange */
1126
            foreach ($priceRanges as $priceRange) {
1127
                $priceTo = $priceRange->to == PriceRange::ANY ? 'beliebig' : $priceRange->to;
1128
1129
                //todo: maybe batch insert if possible?
1130
                $this->manager->getConnection()->executeQuery(
1131
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`)
1132
                      VALUES (?, ?, ?, ?, ?, ?);',
1133
                    [
1134
                        $group->getKey(),
1135
                        $priceRange->from,
1136
                        $priceTo,
1137
                        $article->getId(),
1138
                        $detail->getId(),
1139
                        $priceRange->price
1140
                    ]
1141
                );
1142
            }
1143
            $this->manager->getConnection()->commit();
1144
        } catch (\Exception $e) {
1145
            $this->manager->getConnection()->rollBack();
1146
            throw new \Exception($e->getMessage());
1147
        }
1148
    }
1149
1150
    /**
1151
     * Set detail purchase price with plain SQL
1152
     * Entity usage throws exception when error handlers are disabled
1153
     *
1154
     * @param DetailModel $detail
1155
     * @param float $purchasePrice
1156
     * @param Group $defaultGroup
1157
     * @throws \Doctrine\DBAL\DBALException
1158
     */
1159
    private function setPurchasePrice(DetailModel $detail, $purchasePrice, Group $defaultGroup)
1160
    {
1161
        if (method_exists($detail, 'setPurchasePrice')) {
1162
            $this->manager->getConnection()->executeQuery(
1163
                'UPDATE `s_articles_details` SET `purchaseprice` = ? WHERE `id` = ?',
1164
                [$purchasePrice, $detail->getId()]
1165
            );
1166
        } else {
1167
            $id = $this->manager->getConnection()->fetchColumn(
1168
                'SELECT id FROM `s_articles_prices`
1169
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1170
                [$defaultGroup->getKey(), 1, 'beliebig', $detail->getArticleId(), $detail->getId()]
1171
            );
1172
1173
            if ($id > 0) {
1174
                $this->manager->getConnection()->executeQuery(
1175
                    'UPDATE `s_articles_prices` SET `baseprice` = ? WHERE `id` = ?',
1176
                    [$purchasePrice, $id]
1177
                );
1178
            } else {
1179
                $this->manager->getConnection()->executeQuery(
1180
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `baseprice`)
1181
              VALUES (?, 1, "beliebig", ?, ?, ?);',
1182
                    [$defaultGroup->getKey(), $detail->getArticleId(), $detail->getId(), $purchasePrice]
1183
                );
1184
            }
1185
        }
1186
    }
1187
1188
    /**
1189
     * Adds translation record for given article
1190
     *
1191
     * @param ProductModel $article
1192
     * @param Product $sdkProduct
1193
     */
1194
    private function addArticleTranslations(ProductModel $article, Product $sdkProduct)
1195
    {
1196
        /** @var \Shopware\Connect\Struct\Translation $translation */
1197
        foreach ($sdkProduct->translations as $key => $translation) {
1198
            /** @var \Shopware\Models\Shop\Locale $locale */
1199
            $locale = $this->getLocaleRepository()->findOneBy(['locale' => LocaleMapper::getShopwareLocale($key)]);
1200
            /** @var \Shopware\Models\Shop\Shop $shop */
1201
            $shop = $this->getShopRepository()->findOneBy(['locale' => $locale]);
1202
            if (!$shop) {
1203
                continue;
1204
            }
1205
1206
            $this->productTranslationsGateway->addArticleTranslation($translation, $article->getId(), $shop->getId());
1207
        }
1208
    }
1209
1210
    /**
1211
     * dsadsa
1212
     * @return \Shopware\Components\Model\ModelRepository
1213
     */
1214
    private function getLocaleRepository()
1215
    {
1216
        if (!$this->localeRepository) {
1217
            $this->localeRepository = $this->manager->getRepository('Shopware\Models\Shop\Locale');
1218
        }
1219
1220
        return $this->localeRepository;
1221
    }
1222
1223
    private function getShopRepository()
1224
    {
1225
        if (!$this->shopRepository) {
1226
            $this->shopRepository = $this->manager->getRepository('Shopware\Models\Shop\Shop');
1227
        }
1228
1229
        return $this->shopRepository;
1230
    }
1231
1232
    /**
1233
     * Delete product or product variant with given shopId and sourceId.
1234
     *
1235
     * Only the combination of both identifies a product uniquely. Do NOT
1236
     * delete products just by their sourceId.
1237
     *
1238
     * You might receive delete requests for products, which are not available
1239
     * in your shop. Just ignore them.
1240
     *
1241
     * @param string $shopId
1242
     * @param string $sourceId
1243
     * @return void
1244
     */
1245
    public function delete($shopId, $sourceId)
1246
    {
1247
        $detail = $this->helper->getArticleDetailModelByProduct(new Product([
1248
            'shopId' => $shopId,
1249
            'sourceId' => $sourceId,
1250
        ]));
1251
        if ($detail === null) {
1252
            return;
1253
        }
1254
1255
        $this->deleteDetail($detail);
1256
    }
1257
1258
    public function update($shopId, $sourceId, ProductUpdate $product)
1259
    {
1260
        // find article detail id
1261
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1262
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1263
            [$sourceId, $shopId]
1264
        );
1265
1266
        $this->eventManager->notify(
1267
            'Connect_Merchant_Update_GeneralProductInformation',
1268
            [
1269
                'subject' => $this,
1270
                'shopId' => $shopId,
1271
                'sourceId' => $sourceId,
1272
                'articleDetailId' => $articleDetailId
1273
            ]
1274
        );
1275
1276
        // update purchasePriceHash, offerValidUntil and purchasePrice in connect attribute
1277
        $this->manager->getConnection()->executeUpdate(
1278
            'UPDATE s_plugin_connect_items SET purchase_price_hash = ?, offer_valid_until = ?, purchase_price = ?
1279
            WHERE source_id = ? AND shop_id = ?',
1280
            [
1281
                $product->purchasePriceHash,
1282
                $product->offerValidUntil,
1283
                $product->purchasePrice,
1284
                $sourceId,
1285
                $shopId,
1286
            ]
1287
        );
1288
1289
        // update stock in article detail
1290
        // update prices
1291
        // if purchase price is stored in article detail
1292
        // update it together with stock
1293
        // since shopware 5.2
1294
        if (method_exists('Shopware\Models\Article\Detail', 'getPurchasePrice')) {
1295
            $this->manager->getConnection()->executeUpdate(
1296
                'UPDATE s_articles_details SET instock = ?, purchaseprice = ? WHERE id = ?',
1297
                [$product->availability, $product->purchasePrice, $articleDetailId]
1298
            );
1299
        } else {
1300
            $this->manager->getConnection()->executeUpdate(
1301
                'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1302
                [$product->availability, $articleDetailId]
1303
            );
1304
        }
1305
        $this->manager->getConnection()->executeUpdate(
1306
            "UPDATE s_articles_prices SET price = ?, baseprice = ? WHERE articledetailsID = ? AND pricegroup = 'EK'",
1307
            [$product->price, $product->purchasePrice, $articleDetailId]
1308
        );
1309
    }
1310
1311
    public function changeAvailability($shopId, $sourceId, $availability)
1312
    {
1313
        // find article detail id
1314
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1315
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1316
            [$sourceId, $shopId]
1317
        );
1318
1319
        $this->eventManager->notify(
1320
            'Connect_Merchant_Update_GeneralProductInformation',
1321
            [
1322
                'subject' => $this,
1323
                'shopId' => $shopId,
1324
                'sourceId' => $sourceId,
1325
                'articleDetailId' => $articleDetailId
1326
            ]
1327
        );
1328
1329
        // update stock in article detail
1330
        $this->manager->getConnection()->executeUpdate(
1331
            'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1332
            [$availability, $articleDetailId]
1333
        );
1334
    }
1335
1336
    /**
1337
     * @inheritDoc
1338
     */
1339
    public function makeMainVariant($shopId, $sourceId, $groupId)
1340
    {
1341
        //find article detail which should be selected as main one
1342
        $newMainDetail = $this->helper->getConnectArticleDetailModel($sourceId, $shopId);
1343
        if (!$newMainDetail) {
1344
            return;
1345
        }
1346
1347
        /** @var \Shopware\Models\Article\Article $article */
1348
        $article = $newMainDetail->getArticle();
1349
1350
        $this->eventManager->notify(
1351
            'Connect_Merchant_Update_ProductMainVariant_Before',
1352
            [
1353
                'subject' => $this,
1354
                'shopId' => $shopId,
1355
                'sourceId' => $sourceId,
1356
                'articleId' => $article->getId(),
1357
                'articleDetailId' => $newMainDetail->getId()
1358
            ]
1359
        );
1360
1361
        // replace current main detail with new one
1362
        $currentMainDetail = $article->getMainDetail();
1363
        $currentMainDetail->setKind(2);
1364
        $newMainDetail->setKind(1);
1365
        $article->setMainDetail($newMainDetail);
1366
1367
        $this->manager->persist($newMainDetail);
1368
        $this->manager->persist($currentMainDetail);
1369
        $this->manager->persist($article);
1370
        $this->manager->flush();
1371
    }
1372
1373
    /**
1374
     * Updates the status of an Order
1375
     *
1376
     * @param string $localOrderId
1377
     * @param string $orderStatus
1378
     * @param string $trackingNumber
1379
     * @return void
1380
     */
1381
    public function updateOrderStatus($localOrderId, $orderStatus, $trackingNumber)
1382
    {
1383
        if ($this->config->getConfig('updateOrderStatus') == 1) {
1384
            $this->updateDeliveryStatus($localOrderId, $orderStatus);
1385
        }
1386
1387
        if ($trackingNumber) {
1388
            $this->updateTrackingNumber($localOrderId, $trackingNumber);
1389
        }
1390
    }
1391
1392
    /**
1393
     * @param string $localOrderId
1394
     * @param string $orderStatus
1395
     */
1396
    private function updateDeliveryStatus($localOrderId, $orderStatus)
1397
    {
1398
        $status = false;
1399
        if ($orderStatus === OrderStatus::STATE_IN_PROCESS) {
1400
            $status = Status::ORDER_STATE_PARTIALLY_DELIVERED;
1401
        } elseif ($orderStatus === OrderStatus::STATE_DELIVERED) {
1402
            $status = Status::ORDER_STATE_COMPLETELY_DELIVERED;
1403
        }
1404
1405
        if ($status) {
1406
            $this->manager->getConnection()->executeQuery(
1407
                'UPDATE s_order 
1408
                SET status = :orderStatus
1409
                WHERE ordernumber = :orderNumber',
1410
                [
1411
                    ':orderStatus' => $status,
1412
                    ':orderNumber' => $localOrderId
1413
                ]
1414
            );
1415
        }
1416
    }
1417
1418
    /**
1419
     * @param string $localOrderId
1420
     * @param string $trackingNumber
1421
     */
1422
    private function updateTrackingNumber($localOrderId, $trackingNumber)
1423
    {
1424
        $currentTrackingCode = $this->manager->getConnection()->fetchColumn(
1425
            'SELECT trackingcode
1426
            FROM s_order
1427
            WHERE ordernumber = :orderNumber',
1428
            [
1429
                ':orderNumber' => $localOrderId
1430
            ]
1431
        );
1432
1433
        if (!$currentTrackingCode) {
1434
            $newTracking = $trackingNumber;
1435
        } else {
1436
            $newTracking = $this->combineTrackingNumbers($trackingNumber, $currentTrackingCode);
1437
        }
1438
1439
        $this->manager->getConnection()->executeQuery(
1440
            'UPDATE s_order 
1441
            SET trackingcode = :trackingCode
1442
            WHERE ordernumber = :orderNumber',
1443
            [
1444
                ':trackingCode' => $newTracking,
1445
                ':orderNumber' => $localOrderId
1446
            ]
1447
        );
1448
    }
1449
1450
    /**
1451
     * @param string $newTrackingCode
1452
     * @param string $currentTrackingCode
1453
     * @return string
1454
     */
1455
    private function combineTrackingNumbers($newTrackingCode, $currentTrackingCode)
1456
    {
1457
        $currentTrackingCodes = $this->getTrackingNumberAsArray($currentTrackingCode);
1458
        $newTrackingCodes = $this->getTrackingNumberAsArray($newTrackingCode);
1459
        $newTrackingCodes = array_unique(array_merge($currentTrackingCodes, $newTrackingCodes));
1460
        $newTracking = implode(',', $newTrackingCodes);
1461
1462
        return $newTracking;
1463
    }
1464
1465
    /**
1466
     * @param string $trackingCode
1467
     * @return string[]
1468
     */
1469
    private function getTrackingNumberAsArray($trackingCode)
1470
    {
1471
        if (strpos($trackingCode, ',') !== false) {
1472
            return explode(',', $trackingCode);
1473
        }
1474
1475
        return [$trackingCode];
1476
    }
1477
1478
    /**
1479
     * @param Product $product
1480
     * @param ProductModel $model
1481
     */
1482
    private function saveVat(Product $product, ProductModel $model)
1483
    {
1484
        if ($product->vat !== null) {
1485
            $repo = $this->manager->getRepository(Tax::class);
1486
            $taxRate = round($product->vat * 100, 2);
1487
            /** @var \Shopware\Models\Tax\Tax $tax */
1488
            $tax = $repo->findOneBy(['tax' => $taxRate]);
1489
            if (!$tax) {
1490
                $tax = new Tax();
1491
                $tax->setTax($taxRate);
1492
                //this is to get rid of zeroes behind the decimal point
1493
                $name = strval(round($taxRate, 2)) . '%';
1494
                $tax->setName($name);
1495
                $this->manager->persist($tax);
1496
            }
1497
            $model->setTax($tax);
1498
        }
1499
    }
1500
1501
    /**
1502
     * @param int $articleId
1503
     * @param Product $product
1504
     */
1505
    private function applyCrossSelling($articleId, Product $product)
1506
    {
1507
        $this->deleteRemovedRelations($articleId, $product);
1508
        $this->storeCrossSellingInformationInverseSide($articleId, $product->sourceId, $product->shopId);
1509
        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...
1510
            $this->storeCrossSellingInformationOwningSide($articleId, $product);
1511
        }
1512
    }
1513
1514
    /**
1515
     * @param int $articleId
1516
     * @param Product $product
1517
     */
1518
    private function storeCrossSellingInformationOwningSide($articleId, $product)
1519
    {
1520
        foreach ($product->related as $relatedId) {
1521
            $this->insertNewRelations($articleId, $product->shopId, $relatedId, self::RELATION_TYPE_RELATED);
1522
        }
1523
1524
        foreach ($product->similar as $similarId) {
1525
            $this->insertNewRelations($articleId, $product->shopId, $similarId, self::RELATION_TYPE_SIMILAR);
1526
        }
1527
    }
1528
1529
    /**
1530
     * @param int $articleId
1531
     * @param int $shopId
1532
     * @param int $relatedId
1533
     * @param string $relationType
1534
     */
1535
    private function insertNewRelations($articleId, $shopId, $relatedId, $relationType)
1536
    {
1537
        $inserted = false;
1538
        try {
1539
            $this->manager->getConnection()->executeQuery('INSERT INTO s_plugin_connect_article_relations (article_id, shop_id, related_article_local_id, relationship_type) VALUES (?, ?, ?, ?)',
1540
                [$articleId, $shopId, $relatedId, $relationType]);
1541
            $inserted = true;
1542
        } 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...
1543
            // No problems here. Just means that the row already existed.
1544
        }
1545
1546
        //outside of try catch because we don't want to catch exceptions -> this method should not throw any
1547
        if ($inserted) {
1548
            $relatedLocalId = $this->manager->getConnection()->fetchColumn('SELECT article_id FROM s_plugin_connect_items WHERE shop_id = ? AND source_id = ?',
1549
                [$shopId, $relatedId]);
1550
            if ($relatedLocalId) {
1551
                $this->manager->getConnection()->executeQuery("INSERT IGNORE INTO s_articles_$relationType (articleID, relatedarticle) VALUES (?, ?)",
1552
                    [$articleId, $relatedLocalId]);
1553
            }
1554
        }
1555
    }
1556
1557
    /**
1558
     * @param int $articleId
1559
     * @param string $sourceId
1560
     * @param int $shopId
1561
     */
1562
    private function storeCrossSellingInformationInverseSide($articleId, $sourceId, $shopId)
1563
    {
1564
        $relatedArticles = $this->manager->getConnection()->fetchAll('SELECT article_id, relationship_type FROM s_plugin_connect_article_relations WHERE shop_id = ? AND related_article_local_id = ?',
1565
            [$shopId, $sourceId]);
1566
1567
        foreach ($relatedArticles as $relatedArticle) {
1568
            $relationType = $relatedArticle['relationship_type'];
1569
            $this->manager->getConnection()->executeQuery("INSERT IGNORE INTO s_articles_$relationType (articleID, relatedarticle) VALUES (?, ?)",
1570
                [$relatedArticle['article_id'], $articleId]);
1571
        }
1572
    }
1573
1574
    /**
1575
     * @param $articleId
1576
     * @param $product
1577
     */
1578
    private function deleteRemovedRelations($articleId, $product)
1579
    {
1580 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...
1581
            $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 = ?',
1582
                [$articleId, $product->shopId, $product->related, self::RELATION_TYPE_RELATED],
1583
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR]);
1584
1585
            $oldRelatedIds = $this->manager->getConnection()->executeQuery(
1586
                'SELECT ar.id 
1587
                FROM s_articles_relationships AS ar
1588
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1589
                WHERE ar.articleID = ? AND ci.shop_id = ? AND ci.source_id NOT IN (?)',
1590
                [$articleId, $product->shopId, $product->related],
1591
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY])
1592
                ->fetchAll(\PDO::FETCH_COLUMN);
1593
        } else {
1594
            $this->manager->getConnection()->executeQuery('DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND relationship_type = ?',
1595
                [$articleId, $product->shopId, self::RELATION_TYPE_RELATED]);
1596
1597
            $oldRelatedIds = $this->manager->getConnection()->executeQuery(
1598
                'SELECT ar.id 
1599
                FROM s_articles_relationships AS ar
1600
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1601
                WHERE ar.articleID = ? AND ci.shop_id = ?',
1602
                [$articleId, $product->shopId])
1603
                ->fetchAll(\PDO::FETCH_COLUMN);
1604
        }
1605
1606
        $this->manager->getConnection()->executeQuery('DELETE FROM s_articles_relationships WHERE id IN (?)',
1607
            [$oldRelatedIds],
1608
            [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]);
1609
1610 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...
1611
            $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 = ?',
1612
                [$articleId, $product->shopId, $product->similar, self::RELATION_TYPE_SIMILAR],
1613
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR]);
1614
1615
            $oldSimilarIds = $this->manager->getConnection()->executeQuery(
1616
                'SELECT ar.id 
1617
                FROM s_articles_similar AS ar
1618
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1619
                WHERE ar.articleID = ? AND ci.shop_id = ? AND ci.source_id NOT IN (?)',
1620
                [$articleId, $product->shopId, $product->similar],
1621
                [\PDO::PARAM_INT, \PDO::PARAM_INT, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY])
1622
                ->fetchAll(\PDO::FETCH_COLUMN);
1623
        } else {
1624
            $this->manager->getConnection()->executeQuery('DELETE FROM s_plugin_connect_article_relations WHERE article_id = ? AND shop_id = ? AND relationship_type = ?',
1625
                [$articleId, $product->shopId, self::RELATION_TYPE_SIMILAR]);
1626
1627
            $oldSimilarIds = $this->manager->getConnection()->executeQuery(
1628
                'SELECT ar.id 
1629
                FROM s_articles_similar AS ar
1630
                INNER JOIN s_plugin_connect_items AS ci ON ar.relatedarticle = ci.article_id
1631
                WHERE ar.articleID = ? AND ci.shop_id = ?',
1632
                [$articleId, $product->shopId])
1633
                ->fetchAll(\PDO::FETCH_COLUMN);
1634
        }
1635
1636
        $this->manager->getConnection()->executeQuery('DELETE FROM s_articles_similar WHERE id IN (?)',
1637
            [$oldSimilarIds],
1638
            [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]);
1639
    }
1640
1641
    /**  
1642
     * @param Property $property
1643
     * @return PropertyGroup
1644
     */
1645
    private function createPropertyGroup(Property $property)
1646
    {
1647
        $group = new PropertyGroup();
1648
        $group->setName($property->groupName);
1649
        $group->setComparable($property->comparable);
1650
        $group->setSortMode($property->sortMode);
1651
        $group->setPosition($property->groupPosition);
1652
1653
        $attribute = new \Shopware\Models\Attribute\PropertyGroup();
1654
        $attribute->setPropertyGroup($group);
1655
        $attribute->setConnectIsRemote(true);
1656
        $group->setAttribute($attribute);
1657
1658
        $this->manager->persist($attribute);
1659
        $this->manager->persist($group);
1660
        $this->manager->flush();
1661
1662
        return $group;
1663
    }
1664
}
1665