Completed
Pull Request — master (#415)
by Jonas
04:08
created

ProductToShop::commit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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