ProductBuilder   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Test Coverage

Coverage 87.07%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 138
dl 0
loc 344
ccs 128
cts 147
cp 0.8707
rs 9.6
c 8
b 0
f 1
wmc 35

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __clone() 0 3 1
A __construct() 0 18 1
A aSimpleProduct() 0 37 1
A withName() 0 9 2
A withStatus() 0 9 2
A withPrice() 0 5 1
A withSku() 0 5 1
A withStockQty() 0 5 1
A withData() 0 7 1
A withWebsiteIds() 0 5 1
A withWeight() 0 5 1
A withVisibility() 0 9 2
A withTaxClassId() 0 5 1
A aVirtualProduct() 0 6 1
A withCategoryIds() 0 5 1
A withCustomAttributes() 0 11 3
A withBackorders() 0 5 1
A withIsInStock() 0 5 1
A build() 0 12 4
A createProduct() 0 22 4
A isTransactionException() 0 8 2
A buildWithoutSave() 0 9 2
1
<?php
2
declare(strict_types=1);
3
4
namespace TddWizard\Fixtures\Catalog;
5
6
use Exception;
7
use Magento\Catalog\Api\Data\ProductInterface;
8
use Magento\Catalog\Api\Data\ProductWebsiteLinkInterfaceFactory;
9
use Magento\Catalog\Api\ProductRepositoryInterface;
10
use Magento\Catalog\Api\ProductWebsiteLinkRepositoryInterface;
11
use Magento\Catalog\Model\Product;
12
use Magento\Catalog\Model\Product\Attribute\Source\Status;
13
use Magento\Catalog\Model\Product\Type;
14
use Magento\Catalog\Model\Product\Visibility;
15
use Magento\CatalogInventory\Api\Data\StockItemInterface;
16
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
17
use Magento\Indexer\Model\IndexerFactory;
18
use Magento\TestFramework\Helper\Bootstrap;
0 ignored issues
show
Bug introduced by
The type Magento\TestFramework\Helper\Bootstrap was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Throwable;
20
21
/**
22
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
23
 * @SuppressWarnings(PHPMD.UnusedPrivateField)
24
 */
25
class ProductBuilder
26
{
27
    /**
28
     * @var ProductRepositoryInterface
29
     */
30
    private $productRepository;
31
32
    /**
33
     * @var StockItemRepositoryInterface
34
     */
35
    private $stockItemRepository;
36
37
    /**
38
     * @var ProductWebsiteLinkRepositoryInterface
39
     */
40
    private $websiteLinkRepository;
41
42
    /**
43
     * @var ProductWebsiteLinkInterfaceFactory
44
     */
45
    private $websiteLinkFactory;
46
47
    /**
48
     * @var IndexerFactory
49
     */
50
    private $indexerFactory;
51
52
    /**
53
     * @var Product
54
     */
55
    protected $product;
56
57
    /**
58
     * @var int[]
59
     */
60
    private $websiteIds;
61
62
    /**
63
     * @var mixed[][]
64
     */
65
    private $storeSpecificValues;
66
67
    /**
68
     * @var int[]
69
     */
70
    private $categoryIds = [];
71
72
    /**
73
     * @param ProductRepositoryInterface $productRepository
74
     * @param StockItemRepositoryInterface $stockItemRepository
75
     * @param ProductWebsiteLinkRepositoryInterface $websiteLinkRepository
76
     * @param ProductWebsiteLinkInterfaceFactory $websiteLinkFactory
77
     * @param IndexerFactory $indexerFactory
78
     * @param Product $product
79
     * @param int[] $websiteIds
80
     * @param mixed[] $storeSpecificValues
81
     */
82 30
    final public function __construct(
83
        ProductRepositoryInterface $productRepository,
84
        StockItemRepositoryInterface $stockItemRepository,
85
        ProductWebsiteLinkRepositoryInterface $websiteLinkRepository,
86
        ProductWebsiteLinkInterfaceFactory $websiteLinkFactory,
87
        IndexerFactory $indexerFactory,
88
        Product $product,
89
        array $websiteIds,
90
        array $storeSpecificValues
91
    ) {
92 30
        $this->productRepository = $productRepository;
93 30
        $this->websiteLinkRepository = $websiteLinkRepository;
94 30
        $this->stockItemRepository = $stockItemRepository;
95 30
        $this->websiteLinkFactory = $websiteLinkFactory;
96 30
        $this->indexerFactory = $indexerFactory;
97 30
        $this->product = $product;
98 30
        $this->websiteIds = $websiteIds;
99 30
        $this->storeSpecificValues = $storeSpecificValues;
100 30
    }
101
102 29
    public function __clone()
103
    {
104 29
        $this->product = clone $this->product;
105 29
    }
106
107 30
    public static function aSimpleProduct(): ProductBuilder
108
    {
109 30
        $objectManager = Bootstrap::getObjectManager();
110
        /** @var Product $product */
111 30
        $product = $objectManager->create(ProductInterface::class);
112
113 30
        $product->setTypeId(Type::TYPE_SIMPLE)
114 30
                ->setAttributeSetId(4)
115 30
                ->setName('Simple Product')
116 30
                ->setPrice(10)
117 30
                ->setVisibility(Visibility::VISIBILITY_BOTH)
118 30
                ->setStatus(Status::STATUS_ENABLED);
119 30
        $product->addData(
120
            [
121 30
                'tax_class_id' => 1,
122
                'description' => 'Description',
123
            ]
124
        );
125
        /** @var StockItemInterface $stockItem */
126 30
        $stockItem = $objectManager->create(StockItemInterface::class);
127 30
        $stockItem->setManageStock(true)
128 30
                  ->setQty(100)
129 30
                  ->setIsQtyDecimal(false)
130 30
                  ->setIsInStock(true);
131 30
        $product->setExtensionAttributes(
132 30
            $product->getExtensionAttributes()->setStockItem($stockItem)
0 ignored issues
show
Bug introduced by
The method setStockItem() does not exist on Magento\Framework\Api\ExtensionAttributesInterface. It seems like you code against a sub-type of Magento\Framework\Api\ExtensionAttributesInterface such as Magento\Ui\Model\Bookmark. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

132
            $product->getExtensionAttributes()->/** @scrutinizer ignore-call */ setStockItem($stockItem)
Loading history...
133
        );
134
135 30
        return new static(
136 30
            $objectManager->create(ProductRepositoryInterface::class),
137 30
            $objectManager->create(StockItemRepositoryInterface::class),
138 30
            $objectManager->create(ProductWebsiteLinkRepositoryInterface::class),
139 30
            $objectManager->create(ProductWebsiteLinkInterfaceFactory::class),
140 30
            $objectManager->create(IndexerFactory::class),
141
            $product,
142 30
            [1],
143 30
            []
144
        );
145
    }
146
147 3
    public static function aVirtualProduct(): ProductBuilder
148
    {
149 3
        $builder = self::aSimpleProduct();
150 3
        $builder->product->setName('Virtual Product');
151 3
        $builder->product->setTypeId(Type::TYPE_VIRTUAL);
152 3
        return $builder;
153
    }
154
155
    /**
156
     * @param mixed[] $data
157
     * @return ProductBuilder
158
     */
159
    public function withData(array $data): ProductBuilder
160
    {
161
        $builder = clone $this;
162
163
        $builder->product->addData($data);
164
165
        return $builder;
166
    }
167
168 7
    public function withSku(string $sku): ProductBuilder
169
    {
170 7
        $builder = clone $this;
171 7
        $builder->product->setSku($sku);
172 7
        return $builder;
173
    }
174
175 2
    public function withName(string $name, int $storeId = null): ProductBuilder
176
    {
177 2
        $builder = clone $this;
178 2
        if ($storeId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $storeId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
179 1
            $builder->storeSpecificValues[$storeId][ProductInterface::NAME] = $name;
180
        } else {
181 2
            $builder->product->setName($name);
182
        }
183 2
        return $builder;
184
    }
185
186
    /**
187
     * @param int $status
188
     * @param int|null $storeId Pass store ID to set value for specific store.
189
     *                          Attention: Status is configured per website, will affect all stores of the same website
190
     * @return ProductBuilder
191
     */
192 2
    public function withStatus(int $status, $storeId = null): ProductBuilder
193
    {
194 2
        $builder = clone $this;
195 2
        if ($storeId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $storeId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
196 1
            $builder->storeSpecificValues[$storeId][ProductInterface::STATUS] = $status;
197
        } else {
198 2
            $builder->product->setStatus($status);
199
        }
200 2
        return $builder;
201
    }
202
203 2
    public function withVisibility(int $visibility, int $storeId = null): ProductBuilder
204
    {
205 2
        $builder = clone $this;
206 2
        if ($storeId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $storeId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
207 1
            $builder->storeSpecificValues[$storeId][ProductInterface::VISIBILITY] = $visibility;
208
        } else {
209 2
            $builder->product->setVisibility($visibility);
210
        }
211 2
        return $builder;
212
    }
213
214
    /**
215
     * @param int[] $websiteIds
216
     * @return ProductBuilder
217
     */
218 1
    public function withWebsiteIds(array $websiteIds): ProductBuilder
219
    {
220 1
        $builder = clone $this;
221 1
        $builder->websiteIds = $websiteIds;
222 1
        return $builder;
223
    }
224
225
    /**
226
     * @param int[] $categoryIds
227
     * @return ProductBuilder
228
     */
229
    public function withCategoryIds(array $categoryIds): ProductBuilder
230
    {
231
        $builder = clone $this;
232
        $builder->categoryIds = $categoryIds;
233
        return $builder;
234
    }
235
236 3
    public function withPrice(float $price): ProductBuilder
237
    {
238 3
        $builder = clone $this;
239 3
        $builder->product->setPrice($price);
240 3
        return $builder;
241
    }
242
243 1
    public function withTaxClassId(int $taxClassId): ProductBuilder
244
    {
245 1
        $builder = clone $this;
246 1
        $builder->product->setData('tax_class_id', $taxClassId);
247 1
        return $builder;
248
    }
249
250 1
    public function withIsInStock(bool $inStock): ProductBuilder
251
    {
252 1
        $builder = clone $this;
253 1
        $builder->product->getExtensionAttributes()->getStockItem()->setIsInStock($inStock);
0 ignored issues
show
Bug introduced by
The method getStockItem() does not exist on Magento\Framework\Api\ExtensionAttributesInterface. It seems like you code against a sub-type of Magento\Framework\Api\ExtensionAttributesInterface such as Magento\Ui\Model\Bookmark. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

253
        $builder->product->getExtensionAttributes()->/** @scrutinizer ignore-call */ getStockItem()->setIsInStock($inStock);
Loading history...
254 1
        return $builder;
255
    }
256
257 1
    public function withStockQty(float $qty): ProductBuilder
258
    {
259 1
        $builder = clone $this;
260 1
        $builder->product->getExtensionAttributes()->getStockItem()->setQty($qty);
261 1
        return $builder;
262
    }
263
264 1
    public function withBackorders(float $backorders) : ProductBuilder
265
    {
266 1
        $builder = clone $this;
267 1
        $builder->product->getExtensionAttributes()->getStockItem()->setBackorders($backorders);
268 1
        return $builder;
269
    }
270
271 1
    public function withWeight(float $weight): ProductBuilder
272
    {
273 1
        $builder = clone $this;
274 1
        $builder->product->setWeight($weight);
275 1
        return $builder;
276
    }
277
278
    /**
279
     * @param mixed[] $values
280
     * @param int|null $storeId
281
     * @return ProductBuilder
282
     */
283 2
    public function withCustomAttributes(array $values, int $storeId = null): ProductBuilder
284
    {
285 2
        $builder = clone $this;
286 2
        foreach ($values as $code => $value) {
287 2
            if ($storeId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $storeId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
288 1
                $builder->storeSpecificValues[$storeId][$code] = $value;
289
            } else {
290 2
                $builder->product->setCustomAttribute($code, $value);
291
            }
292
        }
293 2
        return $builder;
294
    }
295
296
    /**
297
     * @return ProductInterface
298
     * @throws Exception
299
     */
300 29
    public function build(): ProductInterface
301
    {
302
        try {
303 29
            $product = $this->createProduct();
304 29
            $this->indexerFactory->create()->load('cataloginventory_stock')->reindexRow($product->getId());
305 29
            return $product;
306
        } catch (Exception $e) {
307
            $e->getPrevious();
308
            if (self::isTransactionException($e) || self::isTransactionException($e->getPrevious())) {
309
                throw IndexFailed::becauseInitiallyTriggeredInTransaction($e);
310
            }
311
            throw $e;
312
        }
313
    }
314
315
    /**
316
     * @return ProductInterface
317
     */
318 1
    public function buildWithoutSave() : ProductInterface
319
    {
320 1
        if (!$this->product->getSku()) {
321 1
            $this->product->setSku(sha1(uniqid('', true)));
322
        }
323 1
        $this->product->setCustomAttribute('url_key', $this->product->getSku());
324 1
        $this->product->setData('category_ids', $this->categoryIds);
325
326 1
        return clone $this->product;
327
    }
328
329
    /**
330
     * @return ProductInterface
331
     * @throws Exception
332
     */
333 29
    private function createProduct(): ProductInterface
334
    {
335 29
        $builder = clone $this;
336 29
        if (!$builder->product->getSku()) {
337 23
            $builder->product->setSku(sha1(uniqid('', true)));
338
        }
339 29
        $builder->product->setCustomAttribute('url_key', $builder->product->getSku());
340 29
        $builder->product->setData('category_ids', $builder->categoryIds);
341 29
        $product = $builder->productRepository->save($builder->product);
342 29
        foreach ($builder->websiteIds as $websiteId) {
343 29
            $websiteLink = $builder->websiteLinkFactory->create();
344 29
            $websiteLink->setWebsiteId($websiteId)->setSku($product->getSku());
345 29
            $builder->websiteLinkRepository->save($websiteLink);
346
        }
347 29
        foreach ($builder->storeSpecificValues as $storeId => $values) {
348
            /** @var Product $storeProduct */
349 1
            $storeProduct = clone $product;
350 1
            $storeProduct->setStoreId($storeId);
351 1
            $storeProduct->addData($values);
352 1
            $storeProduct->save();
0 ignored issues
show
Deprecated Code introduced by
The function Magento\Framework\Model\AbstractModel::save() has been deprecated: 100.1.0 because entities must not be responsible for their own persistence. Service contracts should persist entities. Use resource model "save" to implement service contract persistence operations. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

352
            /** @scrutinizer ignore-deprecated */ $storeProduct->save();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
353
        }
354 29
        return $product;
355
    }
356
357
    /**
358
     * @param Throwable|null $exception
359
     * @return bool
360
     */
361
    private static function isTransactionException($exception): bool
362
    {
363
        if ($exception === null) {
364
            return false;
365
        }
366
        return (bool) preg_match(
367
            '{please retry transaction|DDL statements are not allowed in transactions}i',
368
            $exception->getMessage()
369
        );
370
    }
371
}
372