Completed
Pull Request — master (#378)
by Stefan
07:18 queued 02:55
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\Connect\ProductToShop as ProductToShopBase;
13
use Shopware\Connect\Struct\Product;
14
use Shopware\Models\Article\Article as ProductModel;
15
use Shopware\Models\Article\Detail as DetailModel;
16
use Shopware\Models\Attribute\Article as AttributeModel;
17
use Shopware\Components\Model\ModelManager;
18
use Shopware\Connect\Struct\PriceRange;
19
use Shopware\Connect\Struct\ProductUpdate;
20
use Shopware\CustomModels\Connect\ProductStreamAttribute;
21
use Shopware\Models\Customer\Group;
22
use Shopware\Connect\Struct\Property;
23
use Shopware\Models\ProductStream\ProductStream;
24
use Shopware\Models\Property\Group as PropertyGroup;
25
use Shopware\Models\Property\Option as PropertyOption;
26
use Shopware\Models\Property\Value as PropertyValue;
27
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamRepository;
28
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamService;
29
use ShopwarePlugins\Connect\Components\Translations\LocaleMapper;
30
use ShopwarePlugins\Connect\Components\Gateway\ProductTranslationsGateway;
31
use ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway;
32
use ShopwarePlugins\Connect\Components\Utils\UnitMapper;
33
use Shopware\CustomModels\Connect\Attribute as ConnectAttribute;
34
use Shopware\Models\Article\Image;
35
use Shopware\Models\Article\Supplier;
36
37
/**
38
 * The interface for products imported *from* connect *to* the local shop
39
 *
40
 * @category  Shopware
41
 * @package   Shopware\Plugins\SwagConnect
42
 */
43
class ProductToShop implements ProductToShopBase
44
{
45
    /**
46
     * @var Helper
47
     */
48
    private $helper;
49
50
    /**
51
     * @var ModelManager
52
     */
53
    private $manager;
54
55
    /**
56
     * @var \ShopwarePlugins\Connect\Components\Config
57
     */
58
    private $config;
59
60
    /**
61
     * @var ImageImport
62
     */
63
    private $imageImport;
64
65
    /**
66
     * @var \ShopwarePlugins\Connect\Components\VariantConfigurator
67
     */
68
    private $variantConfigurator;
69
70
    /**
71
     * @var MarketplaceGateway
72
     */
73
    private $marketplaceGateway;
74
75
    /**
76
     * @var ProductTranslationsGateway
77
     */
78
    private $productTranslationsGateway;
79
80
    /**
81
     * @var \Shopware\Models\Shop\Repository
82
     */
83
    private $shopRepository;
84
85
    private $localeRepository;
86
87
    /**
88
     * @var CategoryResolver
89
     */
90
    private $categoryResolver;
91
92
    /**
93
     * @var \Shopware\Connect\Gateway
94
     */
95
    private $connectGateway;
96
97
    /**
98
     * @var \Enlight_Event_EventManager
99
     */
100
    private $eventManager;
101
102
    /**
103
     * @param Helper $helper
104
     * @param ModelManager $manager
105
     * @param ImageImport $imageImport
106
     * @param \ShopwarePlugins\Connect\Components\Config $config
107
     * @param VariantConfigurator $variantConfigurator
108
     * @param \ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway $marketplaceGateway
109
     * @param ProductTranslationsGateway $productTranslationsGateway
110
     * @param CategoryResolver $categoryResolver
111
     * @param Gateway $connectGateway
112
     * @param \Enlight_Event_EventManager $eventManager
113
     */
114
    public function __construct(
115
        Helper $helper,
116
        ModelManager $manager,
117
        ImageImport $imageImport,
118
        Config $config,
119
        VariantConfigurator $variantConfigurator,
120
        MarketplaceGateway $marketplaceGateway,
121
        ProductTranslationsGateway $productTranslationsGateway,
122
        CategoryResolver $categoryResolver,
123
        Gateway $connectGateway,
124
        \Enlight_Event_EventManager $eventManager
125
    ) {
126
        $this->helper = $helper;
127
        $this->manager = $manager;
128
        $this->config = $config;
129
        $this->imageImport = $imageImport;
130
        $this->variantConfigurator = $variantConfigurator;
131
        $this->marketplaceGateway = $marketplaceGateway;
132
        $this->productTranslationsGateway = $productTranslationsGateway;
133
        $this->categoryResolver = $categoryResolver;
134
        $this->connectGateway = $connectGateway;
135
        $this->eventManager = $eventManager;
136
    }
137
138
    /**
139
     * Start transaction
140
     *
141
     * Starts a transaction, which includes all insertOrUpdate and delete
142
     * operations, as well as the revision updates.
143
     *
144
     * @return void
145
     */
146
    public function startTransaction()
147
    {
148
        $this->manager->getConnection()->beginTransaction();
149
    }
150
151
    /**
152
     * Commit transaction
153
     *
154
     * Commits the transactions, once all operations are queued.
155
     *
156
     * @return void
157
     */
158
    public function commit()
159
    {
160
        $this->manager->getConnection()->commit();
161
    }
162
163
    /**
164
     * Import or update given product
165
     *
166
     * Store product in your shop database as an external product. The
167
     * associated sourceId
168
     *
169
     * @param Product $product
170
     */
171
    public function insertOrUpdate(Product $product)
172
    {
173
        /** @var Product $product */
174
        $product = $this->eventManager->filter(
175
            'Connect_ProductToShop_InsertOrUpdate_Before',
176
            $product
177
        );
178
179
        // todo@dn: Set dummy values and make product inactive
180
        if (empty($product->title) || empty($product->vendor)) {
181
            return;
182
        }
183
184
        if (!empty($product->sku)) {
185
            $number = 'SC-' . $product->shopId . '-' . $product->sku;
186
            $duplicatedDetail = $this->helper->getDetailByNumber($number);
187
            if ($duplicatedDetail
188
                && $this->helper->getConnectAttributeByModel($duplicatedDetail)->getSourceId() != $product->sourceId
189
            ) {
190
                $this->deleteDetail($duplicatedDetail);
191
            }
192
        } else {
193
            $number = 'SC-' . $product->shopId . '-' . $product->sourceId;
194
        }
195
196
        $detail = $this->helper->getArticleDetailModelByProduct($product);
197
        $detail = $this->eventManager->filter(
198
            'Connect_Merchant_Get_Article_Detail_After',
199
            $detail,
200
            [
201
                'product' => $product,
202
                'subject' => $this
203
            ]
204
        );
205
206
        $isMainVariant = false;
207
        if ($detail === null) {
208
            $active = $this->config->getConfig('activateProductsAutomatically', false) ? true : false;
209
            if ($product->groupId !== null) {
210
                $model = $this->helper->getArticleByRemoteProduct($product);
211
                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...
212
                    $model = $this->helper->createProductModel($product);
213
                    $model->setActive($active);
214
                    $isMainVariant = true;
215
                }
216
            } else {
217
                $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
218
                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...
219
                    $model = $this->helper->createProductModel($product);
220
                    $model->setActive($active);
221
                }
222
            }
223
224
            $detail = new DetailModel();
225
            $detail->setActive($model->getActive());
226
227
            $detail->setArticle($model);
228
229
            if (!empty($product->variant)) {
230
                $this->variantConfigurator->configureVariantAttributes($product, $detail);
231
            }
232
        } else {
233
            $model = $detail->getArticle();
234
            // fix for isMainVariant flag
235
            // in connect attribute table
236
            $mainDetail = $model->getMainDetail();
237
            if ($detail->getId() === $mainDetail->getId()) {
238
                $isMainVariant = true;
239
            }
240
        }
241
242
        $detail->setNumber($number);
243
244
        /** @var \Shopware\Models\Category\Category $category */
245
        foreach ($model->getCategories() as $category) {
246
            $attribute = $category->getAttribute();
247
            if (!$attribute) {
248
                continue;
249
            }
250
251
            if ($attribute->getConnectImported()) {
252
                $model->removeCategory($category);
253
            }
254
        }
255
256
        $detailAttribute = $detail->getAttribute();
257
        if (!$detailAttribute) {
258
            $detailAttribute = new AttributeModel();
259
            $detail->setAttribute($detailAttribute);
260
            $detailAttribute->setArticle($model);
261
        }
262
263
        $categories = $this->categoryResolver->resolve($product->categories);
264
        $hasMappedCategory = count($categories) > 0;
265
        $detailAttribute->setConnectMappedCategory($hasMappedCategory);
266
        foreach ($categories as $remoteCategory) {
267
            $model->addCategory($remoteCategory);
268
        }
269
270
        $connectAttribute = $this->helper->getConnectAttributeByModel($detail) ?: new ConnectAttribute;
271
        // configure main variant and groupId
272
        if ($isMainVariant === true) {
273
            $connectAttribute->setIsMainVariant(true);
274
        }
275
        $connectAttribute->setGroupId($product->groupId);
276
277
        list($updateFields, $flag) = $this->getUpdateFields($model, $detail, $connectAttribute, $product);
278
        /*
279
         * Make sure, that the following properties are set for
280
         * - new products
281
         * - products that have been configured to receive these updates
282
         */
283
        if ($updateFields['name']) {
284
            $model->setName($product->title);
285
        }
286
        if ($updateFields['shortDescription']) {
287
            $model->setDescription($product->shortDescription);
288
        }
289
        if ($updateFields['longDescription']) {
290
            $model->setDescriptionLong($product->longDescription);
291
        }
292
293
        if ($updateFields['additionalDescription']) {
294
            $detailAttribute->setConnectProductDescription($product->additionalDescription);
295
        }
296
297
        if ($product->vat !== null) {
298
            $repo = $this->manager->getRepository('Shopware\Models\Tax\Tax');
299
            $tax = round($product->vat * 100, 2);
300
            /** @var \Shopware\Models\Tax\Tax $tax */
301
            $tax = $repo->findOneBy(['tax' => $tax]);
302
            $model->setTax($tax);
303
        }
304
305
        if ($product->vendor !== null) {
306
            $repo = $this->manager->getRepository('Shopware\Models\Article\Supplier');
307
            $supplier = $repo->findOneBy(['name' => $product->vendor]);
308
            if ($supplier === null) {
309
                $supplier = $this->createSupplier($product->vendor);
310
            }
311
            $model->setSupplier($supplier);
312
        }
313
314
        //set product properties
315
        $this->applyProductProperties($model, $product);
316
317
        // apply marketplace attributes
318
        $detailAttribute = $this->applyMarketplaceAttributes($detailAttribute, $product);
319
320
        $connectAttribute->setShopId($product->shopId);
321
        $connectAttribute->setSourceId($product->sourceId);
322
        $connectAttribute->setExportStatus(null);
323
        $connectAttribute->setPurchasePrice($product->purchasePrice);
324
        $connectAttribute->setFixedPrice($product->fixedPrice);
325
        $connectAttribute->setStream($product->stream);
326
327
        // store product categories to connect attribute
328
        $connectAttribute->setCategory($product->categories);
329
330
        $connectAttribute->setLastUpdateFlag($flag);
331
        // store purchasePriceHash and offerValidUntil
332
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
333
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
334
335
        $detail->setInStock($product->availability);
336
        $detail->setEan($product->ean);
337
        $detail->setShippingTime($product->deliveryWorkDays);
338
        $releaseDate = new \DateTime();
339
        $releaseDate->setTimestamp($product->deliveryDate);
340
        $detail->setReleaseDate($releaseDate);
341
        $detail->setMinPurchase($product->minPurchaseQuantity);
342
343
        // some shops have feature "sell not in stock",
344
        // then end customer should be able to by the product with stock = 0
345
        $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
346
        if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
347
            $model->setLastStock(false);
348
        } else {
349
            $model->setLastStock(true);
350
        }
351
352
        // if connect product has unit
353
        // find local unit with units mapping
354
        // and add to detail model
355
        if (array_key_exists('unit', $product->attributes) && $product->attributes['unit']) {
356
            $detailAttribute->setConnectRemoteUnit($product->attributes['unit']);
357
            if ($this->config->getConfig($product->attributes['unit']) == null) {
358
                $this->config->setConfig($product->attributes['unit'], '', null, 'units');
359
            }
360
361
            /** @var \ShopwarePlugins\Connect\Components\Utils\UnitMapper $unitMapper */
362
            $unitMapper = new UnitMapper($this->config, $this->manager);
363
364
            $shopwareUnit = $unitMapper->getShopwareUnit($product->attributes['unit']);
365
366
            /** @var \Shopware\Models\Article\Unit $unit */
367
            $unit = $this->helper->getUnit($shopwareUnit);
368
            $detail->setUnit($unit);
369
            $detail->setPurchaseUnit($product->attributes['quantity']);
370
            $detail->setReferenceUnit($product->attributes['ref_quantity']);
371
        } else {
372
            $detail->setUnit(null);
373
            $detail->setPurchaseUnit(null);
374
            $detail->setReferenceUnit(null);
375
        }
376
377
        // set dimension
378
        if (array_key_exists('dimension', $product->attributes) && $product->attributes['dimension']) {
379
            $dimension = explode('x', $product->attributes['dimension']);
380
            $detail->setLen($dimension[0]);
381
            $detail->setWidth($dimension[1]);
382
            $detail->setHeight($dimension[2]);
383
        } else {
384
            $detail->setLen(null);
385
            $detail->setWidth(null);
386
            $detail->setHeight(null);
387
        }
388
389
        // set weight
390
        if (array_key_exists('weight', $product->attributes) && $product->attributes['weight']) {
391
            $detail->setWeight($product->attributes['weight']);
392
        }
393
394
        //set package unit
395
        if (array_key_exists(Product::ATTRIBUTE_PACKAGEUNIT, $product->attributes)) {
396
            $detail->setPackUnit($product->attributes[Product::ATTRIBUTE_PACKAGEUNIT]);
397
        }
398
399
        //set basic unit
400
        if (array_key_exists(Product::ATTRIBUTE_BASICUNIT, $product->attributes)) {
401
            $detail->setMinPurchase($product->attributes[Product::ATTRIBUTE_BASICUNIT]);
402
        }
403
404
        //set manufacturer no.
405
        if (array_key_exists(Product::ATTRIBUTE_MANUFACTURERNUMBER, $product->attributes)) {
406
            $detail->setSupplierNumber($product->attributes[Product::ATTRIBUTE_MANUFACTURERNUMBER]);
407
        }
408
409
        // Whenever a product is updated, store a json encoded list of all fields that are updated optionally
410
        // This way a customer will be able to apply the most recent changes any time later
411
        $connectAttribute->setLastUpdate(json_encode([
412
            'shortDescription' => $product->shortDescription,
413
            'longDescription' => $product->longDescription,
414
            'additionalDescription' => $product->additionalDescription,
415
            'purchasePrice' => $product->purchasePrice,
416
            'image' => $product->images,
417
            'variantImages' => $product->variantImages,
418
            'price' => $product->price * ($product->vat + 1),
419
            'name' => $product->title,
420
            'vat' => $product->vat
421
        ]));
422
423
        if ($model->getMainDetail() === null) {
424
            $model->setMainDetail($detail);
425
        }
426
427
        if ($detail->getAttribute() === null) {
428
            $detail->setAttribute($detailAttribute);
429
            $detailAttribute->setArticle($model);
430
        }
431
432
        $connectAttribute->setArticle($model);
433
        $connectAttribute->setArticleDetail($detail);
434
435
        $this->eventManager->notify(
436
            'Connect_Merchant_Saving_ArticleAttribute_Before',
437
            [
438
                'subject' => $this,
439
                'connectAttribute' => $connectAttribute
440
            ]
441
        );
442
443
        $this->manager->persist($connectAttribute);
444
        $this->manager->persist($detail);
445
446
        $this->manager->flush();
447
448
        $defaultCustomerGroup = $this->helper->getDefaultCustomerGroup();
449
        // Only set prices, if fixedPrice is active or price updates are configured
450
        if (count($detail->getPrices()) == 0 || $connectAttribute->getFixedPrice() || $updateFields['price']) {
451
            $this->setPrice($model, $detail, $product);
452
        }
453
        // If the price is not being update, update the purchasePrice anyway
454
        $this->setPurchasePrice($detail, $product->purchasePrice, $defaultCustomerGroup);
455
        $this->manager->clear();
456
457
        $this->addArticleTranslations($model, $product);
458
459
        //clear cache for that article
460
        $this->helper->clearArticleCache($model->getId());
461
462
        if ($updateFields['image']) {
463
            // Reload the model in order to not to work an the already flushed model
464
            $model = $this->helper->getArticleModelByProduct($product);
465
            // import only global images for article
466
            $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 464 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...
467
            // Reload the article detail model in order to not to work an the already flushed model
468
            $detail = $this->helper->getArticleDetailModelByProduct($product);
469
            // import only specific images for variant
470
            $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 468 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...
471
        }
472
        $this->categoryResolver->storeRemoteCategories($product->categories, $model->getId());
473
474
        $this->eventManager->notify(
475
            'Connect_ProductToShop_InsertOrUpdate_After',
476
            [
477
                'connectProduct' => $product,
478
                'shopArticleDetail' => $detail
479
            ]
480
        );
481
482
        $stream = $this->getOrCreateStream($product);
483
        $this->addProductToStream($stream, $model);
484
    }
485
486
    /**
487
     * @param ProductModel $article
488
     * @param Product $product
489
     */
490
    private function applyProductProperties(ProductModel $article, Product $product)
491
    {
492
        if (empty($product->properties)) {
493
            return;
494
        }
495
496
        /** @var Property $firstProperty */
497
        $firstProperty = reset($product->properties);
498
        $groupRepo = $this->manager->getRepository(PropertyGroup::class);
499
        $group = $groupRepo->findOneBy(['name' => $firstProperty->groupName]);
500
501
        if (!$group) {
502
            $group = new PropertyGroup();
503
            $group->setName($firstProperty->groupName);
504
            $group->setComparable($firstProperty->comparable);
505
            $group->setSortMode($firstProperty->sortMode);
506
            $group->setPosition($firstProperty->groupPosition);
507
508
            $attribute = new \Shopware\Models\Attribute\PropertyGroup();
509
            $attribute->setPropertyGroup($group);
510
            $attribute->setConnectIsRemote(true);
511
            $group->setAttribute($attribute);
512
513
            $this->manager->persist($attribute);
514
            $this->manager->persist($group);
515
            $this->manager->flush();
516
        }
517
518
        $propertyValues = $article->getPropertyValues();
519
        $propertyValues->clear();
520
        $this->manager->persist($article);
521
        $this->manager->flush();
522
523
        $article->setPropertyGroup($group);
524
525
        $optionRepo = $this->manager->getRepository(PropertyOption::class);
526
        $valueRepo = $this->manager->getRepository(PropertyValue::class);
527
528
        foreach ($product->properties as $property) {
529
            $option = $optionRepo->findOneBy(['name' => $property->option]);
530
            $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...
531
            if (!$option) {
532
                $option = new PropertyOption();
533
                $option->setName($property->option);
534
                $option->setFilterable($property->filterable);
535
536
                $attribute = new \Shopware\Models\Attribute\PropertyOption();
537
                $attribute->setPropertyOption($option);
538
                $attribute->setConnectIsRemote(true);
539
                $option->setAttribute($attribute);
540
541
                $this->manager->persist($option);
542
                $this->manager->flush($option);
543
            }
544
545
            if (!$optionExists || !$value = $valueRepo->findOneBy(['option' => $option, 'value' => $property->value])) {
546
                $value = new PropertyValue($option, $property->value);
547
                $value->setPosition($property->valuePosition);
548
549
                $attribute = new \Shopware\Models\Attribute\PropertyValue();
550
                $attribute->setPropertyValue($value);
551
                $attribute->setConnectIsRemote(true);
552
                $value->setAttribute($attribute);
553
554
                $this->manager->persist($value);
555
            }
556
557
            if (!$propertyValues->contains($value)) {
558
                //add only new values
559
                $propertyValues->add($value);
560
            }
561
562
            $filters = [
563
                ['property' => 'options.name', 'expression' => '=', 'value' => $property->option],
564
                ['property' => 'groups.name', 'expression' => '=', 'value' => $property->groupName],
565
            ];
566
567
            $query = $groupRepo->getPropertyRelationQuery($filters, null, 1, 0);
568
            $relation = $query->getOneOrNullResult();
569
570
            if (!$relation) {
571
                $group->addOption($option);
572
                $this->manager->persist($group);
573
                $this->manager->flush($group);
574
            }
575
        }
576
577
        $article->setPropertyValues($propertyValues);
578
579
        $this->manager->persist($article);
580
        $this->manager->flush();
581
    }
582
583
    /**
584
     * @param Product $product
585
     * @return ProductStream
586
     */
587
    private function getOrCreateStream(Product $product)
588
    {
589
        /** @var ProductStreamRepository $repo */
590
        $repo = $this->manager->getRepository(ProductStreamAttribute::class);
591
        $stream = $repo->findConnectByName($product->stream);
592
593
        if (!$stream) {
594
            $stream = new ProductStream();
595
            $stream->setName($product->stream);
596
            $stream->setType(ProductStreamService::STATIC_STREAM);
597
            $stream->setSorting(json_encode(
598
                [ReleaseDateSorting::class => ['direction' => 'desc']]
599
            ));
600
601
            //add attributes
602
            $attribute = new \Shopware\Models\Attribute\ProductStream();
603
            $attribute->setProductStream($stream);
604
            $attribute->setConnectIsRemote(true);
605
            $stream->setAttribute($attribute);
606
607
            $this->manager->persist($attribute);
608
            $this->manager->persist($stream);
609
            $this->manager->flush();
610
        }
611
612
        return $stream;
613
    }
614
615
    /**
616
     * @param ProductStream $stream
617
     * @param ProductModel $article
618
     * @throws \Doctrine\DBAL\DBALException
619
     */
620
    private function addProductToStream(ProductStream $stream, ProductModel $article)
621
    {
622
        $conn = $this->manager->getConnection();
623
        $sql = 'INSERT INTO `s_product_streams_selection` (`stream_id`, `article_id`)
624
                VALUES (:streamId, :articleId)
625
                ON DUPLICATE KEY UPDATE stream_id = :streamId, article_id = :articleId';
626
        $stmt = $conn->prepare($sql);
627
        $stmt->execute([':streamId' => $stream->getId(), ':articleId' => $article->getId()]);
628
    }
629
630
    /**
631
     * Set detail purchase price with plain SQL
632
     * Entity usage throws exception when error handlers are disabled
633
     *
634
     * @param ProductModel $article
635
     * @param DetailModel $detail
636
     * @param Product $product
637
     * @throws \Doctrine\DBAL\DBALException
638
     */
639
    private function setPrice(ProductModel $article, DetailModel $detail, Product $product)
640
    {
641
        // set price via plain SQL because shopware throws exception
642
        // undefined index: key when error handler is disabled
643
        $customerGroup = $this->helper->getDefaultCustomerGroup();
644
645
        if (!empty($product->priceRanges)) {
646
            $this->setPriceRange($article, $detail, $product->priceRanges, $customerGroup);
647
648
            return;
649
        }
650
651
        $id = $this->manager->getConnection()->fetchColumn(
652
            'SELECT id FROM `s_articles_prices`
653
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
654
            [$customerGroup->getKey(), 1, 'beliebig', $article->getId(), $detail->getId()]
655
        );
656
657
        // todo@sb: test update prices
658
        if ($id > 0) {
659
            $this->manager->getConnection()->executeQuery(
660
                'UPDATE `s_articles_prices` SET `price` = ?, `baseprice` = ? WHERE `id` = ?',
661
                [$product->price, $product->purchasePrice, $id]
662
            );
663
        } else {
664
            $this->manager->getConnection()->executeQuery(
665
                'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `baseprice`)
666
              VALUES (?, 1, "beliebig", ?, ?, ?, ?);',
667
                [$customerGroup->getKey(), $article->getId(), $detail->getId(), $product->price, $product->purchasePrice]
668
            );
669
        }
670
    }
671
672
    /**
673
     * @param ProductModel $article
674
     * @param DetailModel $detail
675
     * @param array $priceRanges
676
     * @param Group $group
677
     * @throws \Doctrine\DBAL\ConnectionException
678
     * @throws \Exception
679
     */
680
    private function setPriceRange(ProductModel $article, DetailModel $detail, array $priceRanges, Group $group)
681
    {
682
        $this->manager->getConnection()->beginTransaction();
683
684
        try {
685
            // We always delete the prices,
686
            // because we can not know which record is update
687
            $this->manager->getConnection()->executeQuery(
688
                'DELETE FROM `s_articles_prices` WHERE `articleID` = ? AND `articledetailsID` = ?',
689
                [$article->getId(), $detail->getId()]
690
            );
691
692
            /** @var PriceRange $priceRange */
693
            foreach ($priceRanges as $priceRange) {
694
                $priceTo = $priceRange->to == PriceRange::ANY ? 'beliebig' : $priceRange->to;
695
696
                //todo: maybe batch insert if possible?
697
                $this->manager->getConnection()->executeQuery(
698
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`)
699
                      VALUES (?, ?, ?, ?, ?, ?);',
700
                    [
701
                        $group->getKey(),
702
                        $priceRange->from,
703
                        $priceTo,
704
                        $article->getId(),
705
                        $detail->getId(),
706
                        $priceRange->price
707
                    ]
708
                );
709
            }
710
            $this->manager->getConnection()->commit();
711
        } catch (\Exception $e) {
712
            $this->manager->getConnection()->rollBack();
713
            throw new \Exception($e->getMessage());
714
        }
715
    }
716
717
    /**
718
     * Adds translation record for given article
719
     *
720
     * @param ProductModel $article
721
     * @param Product $sdkProduct
722
     */
723
    private function addArticleTranslations(ProductModel $article, Product $sdkProduct)
724
    {
725
        /** @var \Shopware\Connect\Struct\Translation $translation */
726
        foreach ($sdkProduct->translations as $key => $translation) {
727
            /** @var \Shopware\Models\Shop\Locale $locale */
728
            $locale = $this->getLocaleRepository()->findOneBy(['locale' => LocaleMapper::getShopwareLocale($key)]);
729
            /** @var \Shopware\Models\Shop\Shop $shop */
730
            $shop = $this->getShopRepository()->findOneBy(['locale' => $locale]);
731
            if (!$shop) {
732
                continue;
733
            }
734
735
            $this->productTranslationsGateway->addArticleTranslation($translation, $article->getId(), $shop->getId());
736
        }
737
    }
738
739
    /**
740
     * dsadsa
741
     * @return \Shopware\Components\Model\ModelRepository
742
     */
743
    private function getLocaleRepository()
744
    {
745
        if (!$this->localeRepository) {
746
            $this->localeRepository = $this->manager->getRepository('Shopware\Models\Shop\Locale');
747
        }
748
749
        return $this->localeRepository;
750
    }
751
752
    private function getShopRepository()
753
    {
754
        if (!$this->shopRepository) {
755
            $this->shopRepository = $this->manager->getRepository('Shopware\Models\Shop\Shop');
756
        }
757
758
        return $this->shopRepository;
759
    }
760
761
    /**
762
     * Delete product or product variant with given shopId and sourceId.
763
     *
764
     * Only the combination of both identifies a product uniquely. Do NOT
765
     * delete products just by their sourceId.
766
     *
767
     * You might receive delete requests for products, which are not available
768
     * in your shop. Just ignore them.
769
     *
770
     * @param string $shopId
771
     * @param string $sourceId
772
     * @return void
773
     */
774
    public function delete($shopId, $sourceId)
775
    {
776
        $detail = $this->helper->getArticleDetailModelByProduct(new Product([
777
            'shopId' => $shopId,
778
            'sourceId' => $sourceId,
779
        ]));
780
        if ($detail === null) {
781
            return;
782
        }
783
784
        $this->deleteDetail($detail);
785
    }
786
787
    public function deleteDetail(DetailModel $detailModel)
788
    {
789
        $this->eventManager->notify(
790
            'Connect_Merchant_Delete_Product_Before',
791
            [
792
                'subject' => $this,
793
                'articleDetail' => $detailModel
794
            ]
795
        );
796
797
798
        $article = $detailModel->getArticle();
799
        $isMainVariant = $detailModel->getKind() === 1;
800
        // Not sure why, but the Attribute can be NULL
801
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
802
        $this->manager->remove($detailModel);
803
804
        if ($attribute) {
805
            $this->manager->remove($attribute);
806
        }
807
808
        if (count($details = $article->getDetails()) === 1) {
809
            $details->clear();
810
            $this->manager->remove($article);
811
        }
812
813
        // if removed variant is main variant
814
        // find first variant which is not main and mark it
815
        if ($isMainVariant) {
816
            /** @var \Shopware\Models\Article\Detail $variant */
817
            foreach ($article->getDetails() as $variant) {
818
                if ($variant->getId() != $detailModel->getId()) {
819
                    $variant->setKind(1);
820
                    $article->setMainDetail($variant);
821
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
822
                    $connectAttribute->setIsMainVariant(true);
823
                    $this->manager->persist($connectAttribute);
824
                    $this->manager->persist($article);
825
                    $this->manager->persist($variant);
826
                    break;
827
                }
828
            }
829
        }
830
831
        // Do not remove flush. It's needed when remove article,
832
        // because duplication of ordernumber. Even with remove before
833
        // persist calls mysql throws exception "Duplicate entry"
834
        $this->manager->flush();
835
    }
836
837
    /**
838
     * Get array of update info for the known fields
839
     *
840
     * @param $model
841
     * @param $detail
842
     * @param $attribute
843
     * @param $product
844
     * @return array
845
     */
846
    public function getUpdateFields($model, $detail, $attribute, $product)
847
    {
848
        // This also defines the flags of these fields
849
        $fields = $this->helper->getUpdateFlags();
850
        $flagsByName = array_flip($fields);
851
852
        $flag = 0;
853
        $output = [];
854
        foreach ($fields as $key => $field) {
855
            // Don't handle the imageInitialImport flag
856
            if ($field == 'imageInitialImport') {
857
                continue;
858
            }
859
860
            // If this is a new product
861
            if (!$model->getId() && $field == 'image' && !$this->config->getConfig('importImagesOnFirstImport', false)) {
862
                $output[$field] = false;
863
                $flag |= $flagsByName['imageInitialImport'];
864
                continue;
865
            }
866
867
            $updateAllowed = $this->isFieldUpdateAllowed($field, $model, $attribute);
868
            $output[$field] = $updateAllowed;
869
            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...
870
                $flag |= $key;
871
            }
872
        }
873
874
        return [$output, $flag];
875
    }
876
877
    /**
878
     * Determine if a given field has changed
879
     *
880
     * @param $field
881
     * @param ProductModel $model
882
     * @param DetailModel $detail
883
     * @param Product $product
884
     * @return bool
885
     */
886
    public function hasFieldChanged($field, ProductModel $model, DetailModel $detail, Product $product)
887
    {
888
        switch ($field) {
889
            case 'shortDescription':
890
                return $model->getDescription() != $product->shortDescription;
891
            case 'longDescription':
892
                return $model->getDescriptionLong() != $product->longDescription;
893
            case 'additionalDescription':
894
                return $detail->getAttribute()->getConnectProductDescription() != $product->additionalDescription;
895
            case 'name':
896
                return $model->getName() != $product->title;
897
            case 'image':
898
                return count($model->getImages()) != count($product->images);
899
            case 'price':
900
                $prices = $detail->getPrices();
901
                if (empty($prices)) {
902
                    return true;
903
                }
904
                $price = $prices->first();
905
                if (!$price) {
906
                    return true;
907
                }
908
909
                return $prices->first()->getPrice() != $product->price;
910
        }
911
912
        throw new \InvalidArgumentException('Unrecognized field');
913
    }
914
915
    /**
916
     * Helper method to determine if a given $fields may/must be updated.
917
     * This method will check for the model->id in order to determine, if it is a new entity. Therefore
918
     * this method cannot be used after the model in question was already flushed.
919
     *
920
     * @param $field
921
     * @param $model ProductModel
922
     * @param $attribute ConnectAttribute
923
     * @throws \RuntimeException
924
     * @return bool|null
925
     */
926
    public function isFieldUpdateAllowed($field, ProductModel $model, ConnectAttribute $attribute)
927
    {
928
        $allowed = [
929
            'ShortDescription',
930
            'LongDescription',
931
            'AdditionalDescription',
932
            'Image',
933
            'Price',
934
            'Name',
935
        ];
936
937
        // Always allow updates for new models
938
        if (!$model->getId()) {
939
            return true;
940
        }
941
942
        $field = ucfirst($field);
943
        $attributeGetter = 'getUpdate' . $field;
944
        $configName = 'overwriteProduct' . $field;
945
946
        if (!in_array($field, $allowed)) {
947
            throw new \RuntimeException("Unknown update field {$field}");
948
        }
949
950
        $attributeValue = $attribute->$attributeGetter();
951
952
953
954
        // If the value is 'null' or 'inherit', the behaviour will be inherited from the global configuration
955
        // Once we have a supplier based configuration, we need to take it into account here
956
        if ($attributeValue == null || $attributeValue == 'inherit') {
957
            return $this->config->getConfig($configName, true);
958
        }
959
960
        return $attributeValue == 'overwrite';
961
    }
962
963
    /**
964
     * Read product attributes mapping and set to shopware attribute model
965
     *
966
     * @param AttributeModel $detailAttribute
967
     * @param Product $product
968
     * @return AttributeModel
969
     */
970
    private function applyMarketplaceAttributes(AttributeModel $detailAttribute, Product $product)
971
    {
972
        $detailAttribute->setConnectReference($product->sourceId);
973
        $detailAttribute->setConnectArticleShipping($product->shipping);
974
        //todo@sb: check if connectAttribute matches position of the marketplace attribute
975
        array_walk($product->attributes, function ($value, $key) use ($detailAttribute) {
976
            $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...
977
            if (strlen($shopwareAttribute) > 0) {
978
                $setter = 'set' . ucfirst($shopwareAttribute);
979
                $detailAttribute->$setter($value);
980
            }
981
        });
982
983
        return $detailAttribute;
984
    }
985
986
    /**
987
     * @param $vendor
988
     * @return Supplier
989
     */
990
    private function createSupplier($vendor)
991
    {
992
        $supplier = new Supplier();
993
994
        if (is_array($vendor)) {
995
            $supplier->setName($vendor['name']);
996
            $supplier->setDescription($vendor['description']);
997
            if (array_key_exists('url', $vendor) && $vendor['url']) {
998
                $supplier->setLink($vendor['url']);
999
            }
1000
1001
            $supplier->setMetaTitle($vendor['page_title']);
1002
1003
            if (array_key_exists('logo_url', $vendor) && $vendor['logo_url']) {
1004
                $this->imageImport->importImageForSupplier($vendor['logo_url'], $supplier);
1005
            }
1006
        } else {
1007
            $supplier->setName($vendor);
1008
        }
1009
1010
        //sets supplier attributes
1011
        $attr = new \Shopware\Models\Attribute\ArticleSupplier();
1012
        $attr->setConnectIsRemote(true);
1013
1014
        $supplier->setAttribute($attr);
1015
1016
        return $supplier;
1017
    }
1018
1019
    /**
1020
     * Set detail purchase price with plain SQL
1021
     * Entity usage throws exception when error handlers are disabled
1022
     *
1023
     * @param DetailModel $detail
1024
     * @param float $purchasePrice
1025
     * @param Group $defaultGroup
1026
     * @throws \Doctrine\DBAL\DBALException
1027
     */
1028
    private function setPurchasePrice(DetailModel $detail, $purchasePrice, Group $defaultGroup)
1029
    {
1030
        if (method_exists($detail, 'setPurchasePrice')) {
1031
            $this->manager->getConnection()->executeQuery(
1032
                    'UPDATE `s_articles_details` SET `purchaseprice` = ? WHERE `id` = ?',
1033
                    [$purchasePrice, $detail->getId()]
1034
                );
1035
        } else {
1036
            $id = $this->manager->getConnection()->fetchColumn(
1037
                'SELECT id FROM `s_articles_prices`
1038
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1039
                [$defaultGroup->getKey(), 1, 'beliebig', $detail->getArticleId(), $detail->getId()]
1040
            );
1041
1042
            if ($id > 0) {
1043
                $this->manager->getConnection()->executeQuery(
1044
                    'UPDATE `s_articles_prices` SET `baseprice` = ? WHERE `id` = ?',
1045
                    [$purchasePrice, $id]
1046
                );
1047
            } else {
1048
                $this->manager->getConnection()->executeQuery(
1049
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `baseprice`)
1050
              VALUES (?, 1, "beliebig", ?, ?, ?);',
1051
                    [$defaultGroup->getKey(), $detail->getArticleId(), $detail->getId(), $purchasePrice]
1052
                );
1053
            }
1054
        }
1055
    }
1056
1057
    public function update($shopId, $sourceId, ProductUpdate $product)
1058
    {
1059
        // find article detail id
1060
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1061
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1062
            [$sourceId, $shopId]
1063
        );
1064
1065
        $this->eventManager->notify(
1066
            'Connect_Merchant_Update_GeneralProductInformation',
1067
            [
1068
                'subject' => $this,
1069
                'shopId' => $shopId,
1070
                'sourceId' => $sourceId,
1071
                'articleDetailId' => $articleDetailId
1072
            ]
1073
        );
1074
1075
        // update purchasePriceHash, offerValidUntil and purchasePrice in connect attribute
1076
        $this->manager->getConnection()->executeUpdate(
1077
            'UPDATE s_plugin_connect_items SET purchase_price_hash = ?, offer_valid_until = ?, purchase_price = ?
1078
            WHERE source_id = ? AND shop_id = ?',
1079
            [
1080
                $product->purchasePriceHash,
1081
                $product->offerValidUntil,
1082
                $product->purchasePrice,
1083
                $sourceId,
1084
                $shopId,
1085
            ]
1086
        );
1087
1088
        // update stock in article detail
1089
        // update prices
1090
        // if purchase price is stored in article detail
1091
        // update it together with stock
1092
        // since shopware 5.2
1093
        if (method_exists('Shopware\Models\Article\Detail', 'getPurchasePrice')) {
1094
            $this->manager->getConnection()->executeUpdate(
1095
                'UPDATE s_articles_details SET instock = ?, purchaseprice = ? WHERE id = ?',
1096
                [$product->availability, $product->purchasePrice, $articleDetailId]
1097
            );
1098
        } else {
1099
            $this->manager->getConnection()->executeUpdate(
1100
                'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1101
                [$product->availability, $articleDetailId]
1102
            );
1103
        }
1104
        $this->manager->getConnection()->executeUpdate(
1105
            "UPDATE s_articles_prices SET price = ?, baseprice = ? WHERE articledetailsID = ? AND pricegroup = 'EK'",
1106
            [$product->price, $product->purchasePrice, $articleDetailId]
1107
        );
1108
    }
1109
1110
    public function changeAvailability($shopId, $sourceId, $availability)
1111
    {
1112
        // find article detail id
1113
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1114
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1115
            [$sourceId, $shopId]
1116
        );
1117
1118
        $this->eventManager->notify(
1119
            'Connect_Merchant_Update_GeneralProductInformation',
1120
            [
1121
                'subject' => $this,
1122
                'shopId' => $shopId,
1123
                'sourceId' => $sourceId,
1124
                'articleDetailId' => $articleDetailId
1125
            ]
1126
        );
1127
1128
        // update stock in article detail
1129
        $this->manager->getConnection()->executeUpdate(
1130
            'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1131
            [$availability, $articleDetailId]
1132
        );
1133
    }
1134
1135
    /**
1136
     * @inheritDoc
1137
     */
1138
    public function makeMainVariant($shopId, $sourceId, $groupId)
1139
    {
1140
        //find article detail which should be selected as main one
1141
        $newMainDetail = $this->helper->getConnectArticleDetailModel($sourceId, $shopId);
1142
        if (!$newMainDetail) {
1143
            return;
1144
        }
1145
1146
        /** @var \Shopware\Models\Article\Article $article */
1147
        $article = $newMainDetail->getArticle();
1148
1149
        $this->eventManager->notify(
1150
            'Connect_Merchant_Update_ProductMainVariant_Before',
1151
            [
1152
                'subject' => $this,
1153
                'shopId' => $shopId,
1154
                'sourceId' => $sourceId,
1155
                'articleId' => $article->getId(),
1156
                'articleDetailId' => $newMainDetail->getId()
1157
            ]
1158
        );
1159
1160
        // replace current main detail with new one
1161
        $currentMainDetail = $article->getMainDetail();
1162
        $currentMainDetail->setKind(2);
1163
        $newMainDetail->setKind(1);
1164
        $article->setMainDetail($newMainDetail);
1165
1166
        $this->manager->persist($newMainDetail);
1167
        $this->manager->persist($currentMainDetail);
1168
        $this->manager->persist($article);
1169
        $this->manager->flush();
1170
    }
1171
}
1172