Completed
Push — master ( 2df636...70dc22 )
by Fabian
16s queued 10s
created

ProductBuilder   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Test Coverage

Coverage 92.91%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 133
dl 0
loc 331
ccs 131
cts 141
cp 0.9291
rs 9.76
c 8
b 0
f 1
wmc 33

21 Methods

Rating   Name   Duplication   Size   Complexity  
A withName() 0 9 2
A withStatus() 0 9 2
A build() 0 12 4
A withPrice() 0 5 1
A withSku() 0 5 1
A createProduct() 0 22 4
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 isTransactionException() 0 8 2
A withCategoryIds() 0 5 1
A withCustomAttributes() 0 11 3
A withBackorders() 0 5 1
A withIsInStock() 0 5 1
A __clone() 0 3 1
A __construct() 0 18 1
A aVirtualProduct() 0 6 1
A aSimpleProduct() 0 37 1
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 30
    public function __clone()
103
    {
104 30
        $this->product = clone $this->product;
105 30
    }
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
    /**
148
     * @param mixed[] $data
149
     * @return ProductBuilder
150
     */
151
    public function withData(array $data): ProductBuilder
152
    {
153
        $builder = clone $this;
154
155
        $builder->product->addData($data);
156
157
        return $builder;
158
    }
159
160 7
    public function withSku(string $sku): ProductBuilder
161
    {
162 7
        $builder = clone $this;
163 7
        $builder->product->setSku($sku);
164 7
        return $builder;
165
    }
166
167 2
    public function withName(string $name, int $storeId = null): ProductBuilder
168
    {
169 2
        $builder = clone $this;
170 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...
171 1
            $builder->storeSpecificValues[$storeId][ProductInterface::NAME] = $name;
172
        } else {
173 2
            $builder->product->setName($name);
174
        }
175 2
        return $builder;
176
    }
177
178
    /**
179
     * @param int $status
180
     * @param int|null $storeId Pass store ID to set value for specific store.
181
     *                          Attention: Status is configured per website, will affect all stores of the same website
182
     * @return ProductBuilder
183
     */
184 2
    public function withStatus(int $status, $storeId = null): ProductBuilder
185
    {
186 2
        $builder = clone $this;
187 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...
188 1
            $builder->storeSpecificValues[$storeId][ProductInterface::STATUS] = $status;
189
        } else {
190 2
            $builder->product->setStatus($status);
191
        }
192 2
        return $builder;
193
    }
194
195 2
    public function withVisibility(int $visibility, int $storeId = null): ProductBuilder
196
    {
197 2
        $builder = clone $this;
198 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...
199 1
            $builder->storeSpecificValues[$storeId][ProductInterface::VISIBILITY] = $visibility;
200
        } else {
201 2
            $builder->product->setVisibility($visibility);
202
        }
203 2
        return $builder;
204
    }
205
206
    /**
207
     * @param int[] $websiteIds
208
     * @return ProductBuilder
209
     */
210 2
    public function withWebsiteIds(array $websiteIds): ProductBuilder
211
    {
212 2
        $builder = clone $this;
213 2
        $builder->websiteIds = $websiteIds;
214 2
        return $builder;
215
    }
216
217
    /**
218
     * @param int[] $categoryIds
219
     * @return ProductBuilder
220
     */
221
    public function withCategoryIds(array $categoryIds): ProductBuilder
222
    {
223
        $builder = clone $this;
224
        $builder->categoryIds = $categoryIds;
225
        return $builder;
226
    }
227
228 3
    public function withPrice(float $price): ProductBuilder
229
    {
230 3
        $builder = clone $this;
231 3
        $builder->product->setPrice($price);
232 3
        return $builder;
233
    }
234
235 1
    public function withTaxClassId(int $taxClassId): ProductBuilder
236
    {
237 1
        $builder = clone $this;
238 1
        $builder->product->setData('tax_class_id', $taxClassId);
239 1
        return $builder;
240
    }
241
242 1
    public function withIsInStock(bool $inStock): ProductBuilder
243
    {
244 1
        $builder = clone $this;
245 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

245
        $builder->product->getExtensionAttributes()->/** @scrutinizer ignore-call */ getStockItem()->setIsInStock($inStock);
Loading history...
246 1
        return $builder;
247
    }
248
249 1
    public function withStockQty(float $qty): ProductBuilder
250
    {
251 1
        $builder = clone $this;
252 1
        $builder->product->getExtensionAttributes()->getStockItem()->setQty($qty);
253 1
        return $builder;
254
    }
255
256 1
    public function withBackorders(float $backorders) : ProductBuilder
257
    {
258 1
        $builder = clone $this;
259 1
        $builder->product->getExtensionAttributes()->getStockItem()->setBackorders($backorders);
260 1
        return $builder;
261
    }
262
263 1
    public function withWeight(float $weight): ProductBuilder
264
    {
265 1
        $builder = clone $this;
266 1
        $builder->product->setWeight($weight);
267 1
        return $builder;
268
    }
269
270
    /**
271
     * @param mixed[] $values
272
     * @param int|null $storeId
273
     * @return ProductBuilder
274
     */
275 2
    public function withCustomAttributes(array $values, int $storeId = null): ProductBuilder
276
    {
277 2
        $builder = clone $this;
278 2
        foreach ($values as $code => $value) {
279 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...
280 1
                $builder->storeSpecificValues[$storeId][$code] = $value;
281
            } else {
282 2
                $builder->product->setCustomAttribute($code, $value);
283
            }
284
        }
285 2
        return $builder;
286
    }
287
288
    /**
289
     * @return ProductInterface
290
     * @throws Exception
291
     */
292 30
    public function build(): ProductInterface
293
    {
294
        try {
295 30
            $product = $this->createProduct();
296 29
            $this->indexerFactory->create()->load('cataloginventory_stock')->reindexRow($product->getId());
297 29
            return $product;
298 1
        } catch (Exception $e) {
299 1
            $e->getPrevious();
300 1
            if (self::isTransactionException($e) || self::isTransactionException($e->getPrevious())) {
301
                throw IndexFailed::becauseInitiallyTriggeredInTransaction($e);
302
            }
303 1
            throw $e;
304
        }
305
    }
306
307
    /**
308
     * @return ProductInterface
309
     * @throws Exception
310
     */
311 30
    private function createProduct(): ProductInterface
312
    {
313 30
        $builder = clone $this;
314 30
        if (!$builder->product->getSku()) {
315 24
            $builder->product->setSku(sha1(uniqid('', true)));
316
        }
317 30
        $builder->product->setCustomAttribute('url_key', $builder->product->getSku());
318 30
        $builder->product->setData('category_ids', $builder->categoryIds);
319 30
        $product = $builder->productRepository->save($builder->product);
320 30
        foreach ($builder->websiteIds as $websiteId) {
321 30
            $websiteLink = $builder->websiteLinkFactory->create();
322 30
            $websiteLink->setWebsiteId($websiteId)->setSku($product->getSku());
323 30
            $builder->websiteLinkRepository->save($websiteLink);
324
        }
325 29
        foreach ($builder->storeSpecificValues as $storeId => $values) {
326
            /** @var Product $storeProduct */
327 1
            $storeProduct = clone $product;
328 1
            $storeProduct->setStoreId($storeId);
329 1
            $storeProduct->addData($values);
330 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

330
            /** @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...
331
        }
332 29
        return $product;
333
    }
334
335
    /**
336
     * @param Throwable|null $exception
337
     * @return bool
338
     */
339 1
    private static function isTransactionException($exception): bool
340
    {
341 1
        if ($exception === null) {
342
            return false;
343
        }
344 1
        return (bool) preg_match(
345 1
            '{please retry transaction|DDL statements are not allowed in transactions}i',
346 1
            $exception->getMessage()
347
        );
348
    }
349
350 3
    public static function aVirtualProduct(): ProductBuilder
351
    {
352 3
        $builder = self::aSimpleProduct();
353 3
        $builder->product->setName('Virtual Product');
354 3
        $builder->product->setTypeId(Type::TYPE_VIRTUAL);
355 3
        return $builder;
356
    }
357
}
358