ProductToShop::createPropertyGroup()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
1
<?php
2
/**
3
 * (c) shopware AG <[email protected]>
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace ShopwarePlugins\Connect\Components;
9
10
use Shopware\Bundle\SearchBundle\Sorting\ReleaseDateSorting;
11
use Shopware\Connect\Gateway;
12
use Shopware\Components\Model\CategoryDenormalization;
13
use Shopware\Connect\ProductToShop as ProductToShopBase;
14
use Shopware\Connect\Struct\OrderStatus;
15
use Shopware\Connect\Struct\Product;
16
use Shopware\Models\Article\Article as ProductModel;
17
use Shopware\Models\Article\Configurator\Option;
18
use Shopware\Models\Order\Status;
19
use Shopware\Models\Article\Detail as DetailModel;
20
use Shopware\Models\Attribute\Article as AttributeModel;
21
use Shopware\Components\Model\ModelManager;
22
use Shopware\Connect\Struct\PriceRange;
23
use Shopware\Connect\Struct\ProductUpdate;
24
use Shopware\CustomModels\Connect\ProductStreamAttribute;
25
use Shopware\Models\Customer\Group;
26
use Shopware\Connect\Struct\Property;
27
use Shopware\Models\ProductStream\ProductStream;
28
use Shopware\Models\Property\Group as PropertyGroup;
29
use Shopware\Models\Property\Option as PropertyOption;
30
use Shopware\Models\Property\Value as PropertyValue;
31
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamRepository;
32
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamService;
33
use ShopwarePlugins\Connect\Components\Translations\LocaleMapper;
34
use ShopwarePlugins\Connect\Components\Gateway\ProductTranslationsGateway;
35
use ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway;
36
use ShopwarePlugins\Connect\Components\Utils\UnitMapper;
37
use Shopware\CustomModels\Connect\Attribute as ConnectAttribute;
38
use Shopware\Models\Article\Supplier;
39
use Shopware\Models\Tax\Tax;
40
use Shopware\Models\Article\Configurator\Set;
41
42
/**
43
 * The interface for products imported *from* connect *to* the local shop
44
 *
45
 * @category  Shopware
46
 * @package   Shopware\Plugins\SwagConnect
47
 */
48
class ProductToShop implements ProductToShopBase
49
{
50
    const RELATION_TYPE_RELATED = 'relationships';
51
    const RELATION_TYPE_SIMILAR = 'similar';
52
53
    /**
54
     * @var Helper
55
     */
56
    private $helper;
57
58
    /**
59
     * @var ModelManager
60
     */
61
    private $manager;
62
63
    /**
64
     * @var \ShopwarePlugins\Connect\Components\Config
65
     */
66
    private $config;
67
68
    /**
69
     * @var ImageImport
70
     */
71
    private $imageImport;
72
73
    /**
74
     * @var \ShopwarePlugins\Connect\Components\VariantConfigurator
75
     */
76
    private $variantConfigurator;
77
78
    /**
79
     * @var MarketplaceGateway
80
     */
81
    private $marketplaceGateway;
82
83
    /**
84
     * @var ProductTranslationsGateway
85
     */
86
    private $productTranslationsGateway;
87
88
    /**
89
     * @var \Shopware\Models\Shop\Repository
90
     */
91
    private $shopRepository;
92
93
    private $localeRepository;
94
95
    /**
96
     * @var CategoryResolver
97
     */
98
    private $categoryResolver;
99
100
    /**
101
     * @var \Shopware\Connect\Gateway
102
     */
103
    private $connectGateway;
104
105
    /**
106
     * @var \Enlight_Event_EventManager
107
     */
108
    private $eventManager;
109
110
    /**
111
     * @var CategoryDenormalization
112
     */
113
    private $categoryDenormalization;
114
115
    /**
116
     * @param Helper $helper
117
     * @param ModelManager $manager
118
     * @param ImageImport $imageImport
119
     * @param \ShopwarePlugins\Connect\Components\Config $config
120
     * @param VariantConfigurator $variantConfigurator
121
     * @param \ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway $marketplaceGateway
122
     * @param ProductTranslationsGateway $productTranslationsGateway
123
     * @param CategoryResolver $categoryResolver
124
     * @param Gateway $connectGateway
125
     * @param \Enlight_Event_EventManager $eventManager
126
     * @param CategoryDenormalization $categoryDenormalization
127
     */
128
    public function __construct(
129
        Helper $helper,
130
        ModelManager $manager,
131
        ImageImport $imageImport,
132
        Config $config,
133
        VariantConfigurator $variantConfigurator,
134
        MarketplaceGateway $marketplaceGateway,
135
        ProductTranslationsGateway $productTranslationsGateway,
136
        CategoryResolver $categoryResolver,
137
        Gateway $connectGateway,
138
        \Enlight_Event_EventManager $eventManager,
139
        CategoryDenormalization $categoryDenormalization
140
    ) {
141
        $this->helper = $helper;
142
        $this->manager = $manager;
143
        $this->config = $config;
144
        $this->imageImport = $imageImport;
145
        $this->variantConfigurator = $variantConfigurator;
146
        $this->marketplaceGateway = $marketplaceGateway;
147
        $this->productTranslationsGateway = $productTranslationsGateway;
148
        $this->categoryResolver = $categoryResolver;
149
        $this->connectGateway = $connectGateway;
150
        $this->eventManager = $eventManager;
151
        $this->categoryDenormalization = $categoryDenormalization;
152
    }
153
154
    /**
155
     * Start transaction
156
     *
157
     * Starts a transaction, which includes all insertOrUpdate and delete
158
     * operations, as well as the revision updates.
159
     *
160
     * @return void
161
     */
162
    public function startTransaction()
163
    {
164
        $this->manager->getConnection()->beginTransaction();
165
    }
166
167
    /**
168
     * Commit transaction
169
     *
170
     * Commits the transactions, once all operations are queued.
171
     *
172
     * @return void
173
     */
174
    public function commit()
175
    {
176
        $this->manager->getConnection()->commit();
177
    }
178
179
    /**
180
     * Import or update given product
181
     *
182
     * Store product in your shop database as an external product. The
183
     * associated sourceId
184
     *
185
     * @param Product $product
186
     */
187
    public function insertOrUpdate(Product $product)
188
    {
189
        /** @var Product $product */
190
        $product = $this->eventManager->filter(
191
            'Connect_ProductToShop_InsertOrUpdate_Before',
192
            $product
193
        );
194
195
        // todo@dn: Set dummy values and make product inactive
196
        if (empty($product->title) || empty($product->vendor)) {
197
            return;
198
        }
199
200
        $number = $this->generateSKU($product);
201
202
        $detail = $this->helper->getArticleDetailModelByProduct($product);
203
        $detail = $this->eventManager->filter(
204
            'Connect_Merchant_Get_Article_Detail_After',
205
            $detail,
206
            [
207
                'product' => $product,
208
                'subject' => $this
209
            ]
210
        );
211
212
        $isMainVariant = false;
213
        if ($detail === null) {
214
            $active = $this->config->getConfig('activateProductsAutomatically', false) ? true : false;
215
216
            $model = $this->getSWProductModel($product, $active, $isMainVariant);
217
218
            $detail = $this->generateNewDetail($product, $model);
219
        } else {
220
            /** @var ProductModel $model */
221
            $model = $detail->getArticle();
222
            // fix for isMainVariant flag
223
            // in connect attribute table
224
            $mainDetail = $model->getMainDetail();
225
            $isMainVariant = $this->checkIfMainVariant($detail, $mainDetail);
226
            $this->variantConfigurator->configureVariantAttributes($product, $detail);
227
            $this->updateConfiguratorSetTypeFromProduct($model, $product);
228
229
            $this->cleanUpConfiguratorSet($model, $product);
230
        }
231
232
        $detail->setNumber($number);
233
234
        $detailAttribute = $this->getOrCreateAttributeModel($detail, $model);
235
236
        $connectAttribute = $this->helper->getConnectAttributeByModel($detail) ?: new ConnectAttribute;
237
        // configure main variant and groupId
238
        if ($isMainVariant === true) {
239
            $connectAttribute->setIsMainVariant(true);
240
        }
241
        $connectAttribute->setGroupId($product->groupId);
242
243
        list($updateFields, $flag) = $this->getUpdateFields($model, $detail, $connectAttribute, $product);
244
        $this->setPropertiesForNewProducts($updateFields, $model, $detailAttribute, $product);
245
246
        $this->saveVat($product, $model);
247
248
        $this->applyProductProperties($model, $product);
249
250
        $detailAttribute = $this->applyMarketplaceAttributes($detailAttribute, $product);
251
252
        $this->setConnectAttributesFromProduct($connectAttribute, $product);
253
254
        // store product categories to connect attribute
255
        $connectAttribute->setCategory($product->categories);
256
257
        $connectAttribute->setLastUpdateFlag($flag);
258
259
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
260
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
261
262
        $this->updateDetailFromProduct($detail, $product);
263
264
        // some shops have feature "sell not in stock",
265
        // then end customer should be able to by the product with stock = 0
266
        $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
267
        if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
268
            $model->setLastStock(false);
269
        } else {
270
            $model->setLastStock(true);
271
        }
272
273
        $this->detailSetUnit($detail, $product, $detailAttribute);
274
275
        $this->detailSetAttributes($detail, $product);
276
277
        $this->connectAttributeSetLastUpdate($connectAttribute, $product);
278
279
        if ($model->getMainDetail() === null) {
280
            $model->setMainDetail($detail);
281
        }
282
283
        if ($detail->getAttribute() === null) {
284
            $detail->setAttribute($detailAttribute);
285
            $detailAttribute->setArticle($model);
286
        }
287
288
        $connectAttribute->setArticle($model);
289
        $connectAttribute->setArticleDetail($detail);
290
291
        $this->eventManager->notify(
292
            'Connect_Merchant_Saving_ArticleAttribute_Before',
293
            [
294
                'subject' => $this,
295
                'connectAttribute' => $connectAttribute
296
            ]
297
        );
298
299
        //article has to be flushed
300
        $this->manager->persist($model);
301
        $this->manager->persist($connectAttribute);
302
        $this->manager->persist($detail);
303
        $this->manager->flush();
304
305
        $this->categoryResolver->storeRemoteCategories($product->categories, $model->getId(), $product->shopId);
306
        $categories = $this->categoryResolver->resolve($product->categories, $product->shopId, $product->stream);
307
        if (count($categories) > 0) {
308
            $detailAttribute->setConnectMappedCategory(true);
309
        }
310
311
        $this->manager->persist($detailAttribute);
312
        $this->manager->flush();
313
314
        $this->categoryDenormalization($model, $categories);
315
316
        $defaultCustomerGroup = $this->helper->getDefaultCustomerGroup();
317
        // Only set prices, if fixedPrice is active or price updates are configured
318
        if (count($detail->getPrices()) == 0 || $connectAttribute->getFixedPrice() || $updateFields['price']) {
319
            $this->setPrice($model, $detail, $product);
320
        }
321
        // If the price is not being update, update the purchasePrice anyway
322
        $this->setPurchasePrice($detail, $product->purchasePrice, $defaultCustomerGroup);
323
324
        $this->manager->clear();
325
326
        $this->addArticleTranslations($model, $product);
327
328
        if ($isMainVariant || $product->groupId === null) {
329
            $this->applyCrossSelling($model->getId(), $product);
330
        }
331
332
        //clear cache for that article
333
        $this->helper->clearArticleCache($model->getId());
334
335
        if ($updateFields['image']) {
336
            // Reload the model in order to not to work an the already flushed model
337
            $model = $this->helper->getArticleModelByProduct($product);
338
            // import only global images for article
339
            $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 337 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...
340
            if ($updateFields['mainImage'] && isset($product->images[0])) {
341
                $this->imageImport->importMainImage($product->images[0], $model->getId());
342
            }
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
397
        $this->removeOptions($detailModel, $article);
398
399
        // Not sure why, but the Attribute can be NULL
400
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
401
        $this->manager->remove($detailModel);
402
403
        if ($attribute) {
404
            $this->manager->remove($attribute);
405
        }
406
407
        // if removed variant is main variant
408
        // find first variant which is not main and mark it
409
        if ($detailModel->getKind() === 1) {
410
            /** @var \Shopware\Models\Article\Detail $variant */
411
            foreach ($article->getDetails() as $variant) {
412
                if ($variant->getId() != $detailModel->getId()) {
413
                    $variant->setKind(1);
414
                    $article->setMainDetail($variant);
415
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
416
                    if (!$connectAttribute) {
417
                        continue;
418
                    }
419
                    $connectAttribute->setIsMainVariant(true);
420
                    $this->manager->persist($connectAttribute);
421
                    $this->manager->persist($article);
422
                    $this->manager->persist($variant);
423
                    break;
424
                }
425
            }
426
        }
427
428
        if (count($details = $article->getDetails()) === 1) {
429
            $details->clear();
430
            $this->manager->remove($article);
431
        }
432
433
        //save category Ids before flush
434
        $oldCategoryIds = array_map(function ($category) {
435
            return $category->getId();
436
        }, $article->getCategories()->toArray());
437
438
        // Do not remove flush. It's needed when remove article,
439
        // because duplication of ordernumber. Even with remove before
440
        // persist calls mysql throws exception "Duplicate entry"
441
        $this->manager->flush();
442
        // always clear entity manager, because $article->getDetails() returns
443
        // more than 1 detail, but all of them were removed except main one.
444
        $this->manager->clear();
445
446
        // call this after flush because article has to be deleted that this works
447
        if (count($oldCategoryIds) > 0) {
448
            $this->categoryResolver->deleteEmptyConnectCategories($oldCategoryIds);
449
        }
450
    }
451
452
    /**
453
     * @param DetailModel $detailModel
454
     * @param $article
455
     */
456
    private function removeOptions(DetailModel $detailModel, $article)
457
    {
458
        $configSet = $article->getConfiguratorSet();
459
        $options = $detailModel->getConfiguratorOptions();
460
        if($options && $configSet) {
461
            $configSet->setOptions($options);
462
            $this->manager->persist($configSet);
463
        }
464
    }
465
466
    /**
467
     * @param Product $product
468
     * @param $active
469
     * @param $isMainVariant
470
     * @return null|Article
471
     */
472
    private function getSWProductModel(Product $product, $active, &$isMainVariant)
473
    {
474
        if ($product->groupId !== null) {
475
            $model = $this->helper->getArticleByRemoteProduct($product);
476
            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...
477
                $model = $this->helper->createProductModel($product);
478
                $model->setActive($active);
479
                $isMainVariant = true;
480
            }
481
        } else {
482
            $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
483
            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...
484
                $model = $this->helper->createProductModel($product);
485
                $model->setActive($active);
486
            }
487
        }
488
489
        return $model;
490
    }
491
492
    /**
493
     * @param Product $product
494
     * @param $model
495
     * @return DetailModel
496
     */
497
    private function generateNewDetail(Product $product, $model)
498
    {
499
        $detail = new DetailModel();
500
        $detail->setActive($model->getActive());
501
        // added for 5.4 compatibility
502
        if (method_exists($detail, 'setLastStock')) {
503
            $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
504
            if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
505
                $detail->setLastStock(false);
506
            } else {
507
                $detail->setLastStock(true);
508
            }
509
        }
510
        $this->manager->persist($detail);
511
        $detail->setArticle($model);
512
        $model->getDetails()->add($detail);
513
        $this->variantConfigurator->configureVariantAttributes($product, $detail);
514
515
        return $detail;
516
    }
517
518
    /**
519
     * @param DetailModel $detail
520
     * @param DetailModel $mainDetail
521
     * @return bool
522
     */
523
    private function checkIfMainVariant(DetailModel $detail, DetailModel $mainDetail)
524
    {
525
        return $detail->getId() === $mainDetail->getId();
526
    }
527
528
    /**
529
     * @param ProductModel $model
530
     * @param Product $product
531
     */
532
    private function updateConfiguratorSetTypeFromProduct(ProductModel $model, Product $product)
533
    {
534
        $configSet = $model->getConfiguratorSet();
535
        if (!empty($product->variant) && $configSet instanceof Set) {
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Article\Configurator\Set does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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