Completed
Push — master ( 7bc757...23ad32 )
by Tim
13s queued 11s
created

UrlRewriteObserver::createObserver()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 5
cts 5
cp 1
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Product\UrlRewrite\Observers\UrlRewriteObserver
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2016 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/techdivision/import-product-url-rewrite
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Product\UrlRewrite\Observers;
22
23
use TechDivision\Import\Utils\StoreViewCodes;
24
use TechDivision\Import\Subjects\SubjectInterface;
25
use TechDivision\Import\Observers\ObserverFactoryInterface;
26
use TechDivision\Import\Product\Utils\VisibilityKeys;
27
use TechDivision\Import\Product\Observers\AbstractProductImportObserver;
28
use TechDivision\Import\Product\UrlRewrite\Utils\ColumnKeys;
29
use TechDivision\Import\Product\UrlRewrite\Utils\MemberNames;
30
use TechDivision\Import\Product\UrlRewrite\Utils\CoreConfigDataKeys;
31
use TechDivision\Import\Product\UrlRewrite\Services\ProductUrlRewriteProcessorInterface;
32
33
/**
34
 * Observer that creates/updates the product's URL rewrites.
35
 *
36
 * @author    Tim Wagner <[email protected]>
37
 * @copyright 2016 TechDivision GmbH <[email protected]>
38
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
39
 * @link      https://github.com/techdivision/import-product-url-rewrite
40
 * @link      http://www.techdivision.com
41
 */
42
class UrlRewriteObserver extends AbstractProductImportObserver implements ObserverFactoryInterface
43
{
44
45
    /**
46
     * The entity type to load the URL rewrites for.
47
     *
48
     * @var string
49
     */
50
    const ENTITY_TYPE = 'product';
51
52
    /**
53
     * The key for the category in the metadata.
54
     *
55
     * @var string
56
     */
57
    const CATEGORY_ID = 'category_id';
58
59
    /**
60
     * The URL key from the CSV file column that has to be processed by the observer.
61
     *
62
     * @var string
63
     */
64
    protected $urlKey;
65
66
    /**
67
     * The actual category ID to process.
68
     *
69
     * @var integer
70
     */
71
    protected $categoryId;
72
73
    /**
74
     * The actual entity ID to process.
75
     *
76
     * @var integer
77
     */
78
    protected $entityId;
79
80
    /**
81
     * The ID of the recently created URL rewrite.
82
     *
83
     * @var integer
84
     */
85
    protected $urlRewriteId;
86
87
    /**
88
     * The array with the URL rewrites that has to be created.
89
     *
90
     * @var array
91
     */
92
    protected $urlRewrites = array();
93
94
    /**
95
     * The array with the category IDs related with the product.
96
     *
97
     * @var array
98
     */
99
    protected $productCategoryIds = array();
100
101
    /**
102
     * The array with the root categories.
103
     *
104
     * @var array
105
     */
106
    protected $rootCategories = array();
107
108
    /**
109
     * The product bunch processor instance.
110
     *
111
     * @var \TechDivision\Import\Product\UrlRewrite\Services\ProductUrlRewriteProcessorInterface
112 5
     */
113
    protected $productUrlRewriteProcessor;
114 5
115 5
    /**
116
     * Initialize the observer with the passed product URL rewrite processor instance.
117
     *
118
     * @param \TechDivision\Import\Product\UrlRewrite\Services\ProductUrlRewriteProcessorInterface $productUrlRewriteProcessor The product URL rewrite processor instance
119
     */
120
    public function __construct(ProductUrlRewriteProcessorInterface $productUrlRewriteProcessor)
121
    {
122 3
        $this->productUrlRewriteProcessor = $productUrlRewriteProcessor;
123
    }
124 3
125
    /**
126
     * Will be invoked by the observer visitor when a factory has been defined to create the observer instance.
127
     *
128
     * @param \TechDivision\Import\Subjects\SubjectInterface $subject The subject instance
129
     *
130
     * @return \TechDivision\Import\Observers\ObserverInterface The observer instance
131
     */
132
    public function createObserver(SubjectInterface $subject)
133
    {
134
135
        // load the root categories
136 3
        $rootCategories = $subject->getRootCategories();
0 ignored issues
show
Bug introduced by
The method getRootCategories() does not seem to exist on object<TechDivision\Impo...jects\SubjectInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
137
138
        // initialize the array with the root categories
139
        // by using the entity ID as index
140 3
        foreach ($rootCategories as $rootCategory) {
141 3
            $this->rootCategories[(int) $rootCategory[MemberNames::ENTITY_ID]] = $rootCategory;
142
        }
143
144 3
        // return the initialized instance
145 3
        return $this;
146
    }
147
148
    /**
149
     * Return's the product bunch processor instance.
150
     *
151
     * @return \TechDivision\Import\Product\Services\ProductBunchProcessorInterface The product bunch processor instance
152
     */
153
    protected function getProductUrlRewriteProcessor()
154
    {
155
        return $this->productUrlRewriteProcessor;
156
    }
157
158
    /**
159 3
     * Will be invoked by the action on the events the listener has been registered for.
160 3
     *
161
     * @param \TechDivision\Import\Subjects\SubjectInterface $subject The subject instance
162
     *
163
     * @return array The modified row
164
     * @throws \Exception Is thrown, if the product is not available or no URL key has been specified
165
     * @see \TechDivision\Import\Observers\ObserverInterface::handle()
166
     */
167
    public function handle(SubjectInterface $subject)
168
    {
169
170
        // initialize the row
171
        $this->setSubject($subject);
172
        $this->setRow($subject->getRow());
173
174 3
        // try to load the entity ID for the product with the passed SKU
175
        if ($product = $this->loadProduct($sku = $this->getValue(ColumnKeys::SKU))) {
176
            $this->setLastEntityId($this->entityId = $product[MemberNames::ENTITY_ID]);
177 3 View Code Duplication
        } else {
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...
178
            // prepare a log message
179
            $message = sprintf('Product with SKU "%s" can\'t be loaded to create URL rewrites', $sku);
180 3
            // query whether or not we're in debug mode
181
            if ($this->getSubject()->isDebugMode()) {
182
                $this->getSubject()->getSystemLogger()->warning($message);
183
                return $this->getRow();
184
            } else {
185
                throw new \Exception($this->appendExceptionSuffix($message));
186
            }
187
        }
188
189
        // try to load the URL key
190
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
191
            $this->urlKey = $this->getValue(ColumnKeys::URL_KEY);
192 View Code Duplication
        } else {
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...
193
            // prepare a log message
194
            $message = sprintf('Can\'t find a value in column "url_key" for product with SKU "%s"', $sku);
195
            // query whether or not we're in debug mode
196
            if ($this->getSubject()->isDebugMode()) {
197 3
                $this->getSubject()->getSystemLogger()->warning($message);
198
                return $this->getRow();
199
            } else {
200
                throw new \Exception($this->appendExceptionSuffix($message));
201
            }
202
        }
203
204
        // initialize the store view code
205
        $this->getSubject()->prepareStoreViewCode();
206
207
        // load the store view - if no store view has been set we use the admin store view as default
208
        $storeViewCode = $this->getSubject()->getStoreViewCode(StoreViewCodes::ADMIN);
209
210
        // query whether or not the row has already been processed
211 View Code Duplication
        if ($this->storeViewHasBeenProcessed($sku, $storeViewCode)) {
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...
212
            // log a message
213
            $this->getSubject()
214 3
                ->getSystemLogger()
215
                ->debug(
216
                    sprintf(
217
                        'URL rewrites for SKU "%s" + store view code "%s" has already been processed',
218
                        $sku,
219
                        $storeViewCode
220
                    )
221
                );
222
223
            // return without creating any rewrites
224
            return $this->getRow();
225
        };
226
227
        // stop processing as we don't want to create URL rewrites for the admin store view
228 View Code Duplication
        if ($storeViewCode === StoreViewCodes::ADMIN) {
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...
229
            // log a message and return
230
            $this->getSubject()
231 3
                 ->getSystemLogger()
232 3
                 ->debug(
233
                     sprintf(
234
                         'Store with code "%s" is not active, no URL rewrites will be created for product with SKU "%s"',
235
                         $storeViewCode,
236 3
                         $sku
237
                     )
238
                 );
239 3
240
            // return without creating any rewrites
241
            return $this->getRow();
242
        }
243
244
        // stop processing if the store is NOT active
245 View Code Duplication
        if (!$this->getSubject()->storeIsActive($storeViewCode)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method storeIsActive() does only exist in the following implementations of said interface: TechDivision\Import\Prod...jects\UrlRewriteSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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...
246
            // log a message and return
247 3
            $this->getSubject()
248
                 ->getSystemLogger()
249
                 ->debug(
250
                     sprintf(
251 3
                         'Store with code "%s" is not active, no URL rewrites will be created for product with SKU "%s"',
252
                         $storeViewCode,
253
                         $sku
254 3
                     )
255
                 );
256 3
257
            // return without creating any rewrites
258 3
            return $this->getRow();
259
        }
260
261
        // only map the visibility for the product row related to the default store view
262 3
        if (!$this->hasBeenProcessed($sku)) {
263
            $this->addEntityIdVisibilityIdMapping($this->getValue(ColumnKeys::VISIBILITY));
264
        }
265
266
        // process the functionality and return the row
267
        $this->process();
268 3
269 3
        // return the processed row
270
        return $this->getRow();
271
    }
272
273 2
    /**
274 2
     * Process the observer's business logic.
275
     *
276
     * @return void
277
     */
278 2
    protected function process()
279
    {
280
281
        // prepare the URL rewrites
282
        $this->prepareUrlRewrites();
283
284
        // iterate over the categories and create the URL rewrites
285
        foreach ($this->urlRewrites as $categoryId => $urlRewrite) {
286
            // initialize and persist the URL rewrite
287
            if ($urlRewrite = $this->initializeUrlRewrite($urlRewrite)) {
288
                // initialize URL rewrite and catagory ID
289
                $this->categoryId = $categoryId;
0 ignored issues
show
Documentation Bug introduced by
It seems like $categoryId can also be of type string. However, the property $categoryId is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
290
291 3
                try {
292
                    // persist the URL rewrite
293
                    $this->urlRewriteId = $this->persistUrlRewrite($urlRewrite);
0 ignored issues
show
Documentation Bug introduced by
The property $urlRewriteId was declared of type integer, but $this->persistUrlRewrite($urlRewrite) is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
294
295
                    /*
296
                     * Attention! Stop processing, if this is a root category, because Magento needs explicitly
297
                     * NO URL rewrite product category relation to render canonical and meta og:url tag!
298
                     */
299
                    if ($this->isRootCategory($this->getCategory($categoryId))) {
300 2
                        continue;
301
                    }
302 2
303
                    // initialize and persist the URL rewrite product => category relation
304
                    $urlRewriteProductCategory = $this->initializeUrlRewriteProductCategory(
305
                        $this->prepareUrlRewriteProductCategoryAttributes()
306
                    );
307
308
                    // persist the URL rewrite product category relation
309
                    $this->persistUrlRewriteProductCategory($urlRewriteProductCategory);
310
                } catch (\Exception $e) {
311
                    // query whether or not debug mode has been enabled
312 View Code Duplication
                    if ($this->getSubject()->isDebugMode()) {
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...
313 1
                        $this->getSubject()
314
                             ->getSystemLogger()
315 1
                             ->warning($this->getSubject()->appendExceptionSuffix($e->getMessage()));
316
                    } else {
317
                        throw $e;
318
                    }
319
                }
320
            }
321
        }
322
    }
323 3
324
    /**
325
     * Initialize the category product with the passed attributes and returns an instance.
326
     *
327 3
     * @param array $attr The category product attributes
328 3
     *
329
     * @return array The initialized category product
330
     */
331
    protected function initializeUrlRewrite(array $attr)
332 3
    {
333
        return $attr;
334
    }
335
336
    /**
337
     * Initialize the URL rewrite product => category relation with the passed attributes
338
     * and returns an instance.
339
     *
340
     * @param array $attr The URL rewrite product => category relation attributes
341
     *
342
     * @return array The initialized URL rewrite product => category relation
343
     */
344
    protected function initializeUrlRewriteProductCategory($attr)
345
    {
346
        return $attr;
347
    }
348 3
349
    /**
350
     * Prepare's the URL rewrites that has to be created/updated.
351 3
     *
352
     * @return void
353
     */
354 3
    protected function prepareUrlRewrites()
355
    {
356
357 3
        // (re-)initialize the array for the URL rewrites and the product category IDs
358
        $this->urlRewrites = array();
359
        $this->productCategoryIds = array();
360 2
361
        // do NOT create new URL rewrites, if the product is NOT visible (any more), BUT
362 2
        // handle existing URL rewrites, e. g. to remove and clean up the URL rewrites
363
        if (!$this->isVisible()) {
364 2
             // log a message
365
             $this->getSubject()
366
                 ->getSystemLogger()
367
                 ->debug(
368
                     sprintf(
369
                         'Product with SKU "%s" is not visible, so no URL rewrites will be created',
370
                         $this->getValue(ColumnKeys::SKU)
371
                     )
372
                 );
373
374
             // return without creating any rewrites
375
             return;
376
        }
377
378 3
        // load the root category of the default store view (as we're in the
379
        // default row and does not have a store view code), because we need
380 3
        // that to create the default product URL rewrite
381
        $rootCategory = $this->getRootCategory();
382
383 3
        // at least, add the root category ID to the category => product relations
384
        $this->productCategoryIds[] = $rootCategory[MemberNames::ENTITY_ID];
385 3
386
        // load the store view code from the appropriate column
387
        $storeViewCode = $this->getValue(ColumnKeys::STORE_VIEW_CODE);
388
389
        // load the category paths from the import file
390
        $paths = $this->getValue(ColumnKeys::CATEGORIES, array(), array($this, 'explode'));
391
392
        // append the category => product relations found
393
        foreach ($paths as $path) {
394
            try {
395
                // try to load the category for the given path
396
                $category = $this->getCategoryByPath($path, $storeViewCode);
397
                // resolve the product's categories recursively
398 2
                $this->resolveCategoryIds($category[MemberNames::ENTITY_ID], true, $storeViewCode);
399
            } catch (\Exception $e) {
400
                // query whether or not debug mode has been enabled
401 View Code Duplication
                if ($this->getSubject()->isDebugMode()) {
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...
402 2
                    $this->getSubject()
403 2
                         ->getSystemLogger()
404
                         ->warning($this->getSubject()->appendExceptionSuffix($e->getMessage()));
405
                } else {
406
                    throw $e;
407 2
                }
408
            }
409
        }
410 2
411
        // initialize the member varialbe with the category ID
412
        // and prepare the attributes for each URL rewrite
413 2
        foreach ($this->productCategoryIds as $this->categoryId) {
414
            $this->urlRewrites[$this->categoryId] = $this->prepareAttributes($storeViewCode);
415
        }
416 2
    }
417 2
418
    /**
419 2
     * Resolve's the parent categories of the category with the passed ID and relate's
420
     * it with the product with the passed ID, if the category is top level OR has the
421
     * anchor flag set.
422
     *
423
     * @param integer $categoryId    The ID of the category to resolve the parents
424
     * @param boolean $topLevel      TRUE if the passed category has top level, else FALSE
425
     * @param string  $storeViewCode The store view code to resolve the category IDs for
426
     *
427
     * @return void
428
     */
429 4
    protected function resolveCategoryIds($categoryId, $topLevel = false, $storeViewCode = StoreViewCodes::ADMIN)
430
    {
431
432
        // load the data of the category with the passed ID
433 4
        $category = $this->getCategory($categoryId, $storeViewCode);
434 2
435
        // return immediately if this is a root category
436
        if ($this->isRootCategory($category)) {
437
            return;
438 4
        }
439
440
        // create the product category relation for the current category
441 4
        $this->createProductCategoryRelation($category, $topLevel);
442 1
443
        // try to resolve the parent category IDs
444
        $this->resolveCategoryIds($category[MemberNames::PARENT_ID], false, $storeViewCode);
445
    }
446 3
447 3
    /**
448 3
     * Adds the entity product relation if necessary.
449
     *
450
     * @param array   $category The category to create the relation for
451 2
     * @param boolean $topLevel Whether or not the category has top level
452 2
     *
453 2
     * @return void
454 2
     */
455 2
    protected function createProductCategoryRelation($category, $topLevel)
456 2
    {
457
458
        // query whether or not the product has already been related
459 2
        if (in_array($category[MemberNames::ENTITY_ID], $this->productCategoryIds)) {
460
            return;
461
        }
462
463
        // load the backend configuration value for whether or not the catalog product rewrites should be generated
464
        $generateCategoryRewrites = $this->getGenerateCategoryProductRewritesOptionValue();
465
466 4
        // abort if generating product categories is disabled and category is not root
467
        if ($generateCategoryRewrites === false && $this->isRootCategory($category) === false) {
468 4
            return;
469 4
        }
470 4
471
        // create relation if the category is top level or has the anchor flag set
472
        if ($topLevel || (integer) $category[MemberNames::IS_ANCHOR] === 1) {
473
            $this->productCategoryIds[] = $category[MemberNames::ENTITY_ID];
474
            return;
475
        }
476
477
        $this->getSubject()
478
            ->getSystemLogger()
479
            ->debug(
480
                sprintf(
481 3
                    'Don\'t create URL rewrite for category "%s" because of missing anchor flag',
482
                    $category[MemberNames::PATH]
483
                )
484
            );
485 3
    }
486
487
    /**
488 3
     * Returns the option value for whether or not to generate product catalog rewrites as well.
489
     *
490
     * @return bool
491 3
     */
492 3
    protected function getGenerateCategoryProductRewritesOptionValue()
493 3
    {
494
        return (bool) $this->getSubject()->getCoreConfigData(
495
            CoreConfigDataKeys::CATALOG_SEO_GENERATE_CATEGORY_PRODUCT_REWRITES,
496 3
            true
497
        );
498 3
    }
499 3
500 3
    /**
501 3
     * Prepare the attributes of the entity that has to be persisted.
502 3
     *
503 3
     * @param string $storeViewCode The store view code to prepare the attributes for
504 3
     *
505 3
     * @return array The prepared attributes
506 3
     */
507
    protected function prepareAttributes($storeViewCode)
508
    {
509
510
        // load the store ID to use
511
        $storeId = $this->getSubject()->getRowStoreId();
0 ignored issues
show
Bug introduced by
The method getRowStoreId() does not exist on TechDivision\Import\Subjects\SubjectInterface. Did you maybe mean getRow()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
512
513
        // load the category to create the URL rewrite for
514
        $category = $this->getCategory($this->categoryId, $storeViewCode);
515
516 2
        // initialize the values
517
        $metadata = $this->prepareMetadata($category);
518
        $targetPath = $this->prepareTargetPath($category);
519
        $requestPath = $this->prepareRequestPath($category);
520 2
521
        // return the prepared URL rewrite
522 2
        return $this->initializeEntity(
523 2
            array(
524 2
                MemberNames::ENTITY_TYPE      => UrlRewriteObserver::ENTITY_TYPE,
525
                MemberNames::ENTITY_ID        => $this->entityId,
526
                MemberNames::REQUEST_PATH     => $requestPath,
527
                MemberNames::TARGET_PATH      => $targetPath,
528
                MemberNames::REDIRECT_TYPE    => 0,
529
                MemberNames::STORE_ID         => $storeId,
530
                MemberNames::DESCRIPTION      => null,
531
                MemberNames::IS_AUTOGENERATED => 1,
532
                MemberNames::METADATA         => $metadata ? json_encode($metadata) : null
533
            )
534
        );
535
    }
536 3
537
    /**
538
     * Prepare's the URL rewrite product => category relation attributes.
539
     *
540 3
     * @return array The prepared attributes
541 3
     */
542
    protected function prepareUrlRewriteProductCategoryAttributes()
543 2
    {
544
545
        // return the prepared product
546
        return $this->initializeEntity(
547 3
            array(
548
                MemberNames::PRODUCT_ID     => $this->entityId,
549
                MemberNames::CATEGORY_ID    => $this->categoryId,
550
                MemberNames::URL_REWRITE_ID => $this->urlRewriteId
551
            )
552
        );
553
    }
554
555
    /**
556
     * Prepare's the target path for a URL rewrite.
557
     *
558 3
     * @param array $category The categroy with the URL path
559
     *
560
     * @return string The target path
561
     */
562 3
    protected function prepareTargetPath(array $category)
563
    {
564
565 3
        // query whether or not, the category is the root category
566 3
        if ($this->isRootCategory($category)) {
567
            $targetPath = sprintf('catalog/product/view/id/%d', $this->entityId);
568
        } else {
569 2
            $targetPath = sprintf('catalog/product/view/id/%d/category/%d', $this->entityId, $category[MemberNames::ENTITY_ID]);
570 2
        }
571
572
        // return the target path
573
        return $targetPath;
574
    }
575
576
    /**
577
     * Prepare's the request path for a URL rewrite or the target path for a 301 redirect.
578
     *
579
     * @param array $category The categroy with the URL path
580
     *
581
     * @return string The request path
582
     * @throws \RuntimeException Is thrown, if the passed category has no or an empty value for attribute "url_path"
583
     */
584
    protected function prepareRequestPath(array $category)
585
    {
586
587
        // load the product URL suffix to use
588
        $urlSuffix = $this->getSubject()->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_PRODUCT_URL_SUFFIX, '.html');
589
590
        // query whether or not, the category is the root category
591
        if ($this->isRootCategory($category)) {
592
            return sprintf('%s%s', $this->urlKey, $urlSuffix);
593 3
        } else {
594
            // query whether or not the category's "url_path" attribute, necessary to create a valid "request_path", is available
595
            if (isset($category[MemberNames::URL_PATH]) && $category[MemberNames::URL_PATH]) {
596
                return sprintf('%s/%s%s', $category[MemberNames::URL_PATH], $this->urlKey, $urlSuffix);
597 3
            }
598
        }
599
600 3
        // throw an exception if the category's "url_path" attribute is NOT available
601 3
        throw new \RuntimeException(
602
            $this->appendExceptionSuffix(
603
                sprintf(
604
                    'Can\'t find mandatory attribute "%s" for category ID "%d", necessary to build a valid "request_path"',
605 2
                    MemberNames::URL_PATH,
606
                    $category[MemberNames::ENTITY_ID]
607
                )
608 2
            )
609
        );
610
    }
611
612
    /**
613
     * Prepare's the URL rewrite's metadata with the passed category values.
614
     *
615
     * @param array $category The category used for preparation
616 3
     *
617
     * @return array|null The metadata
618 3
     */
619
    protected function prepareMetadata(array $category)
620
    {
621
622
        // initialize the metadata
623
        $metadata = array();
624
625
        // query whether or not, the passed category IS the root category
626
        if ($this->isRootCategory($category)) {
627
            return;
628
        }
629
630 3
        // if not, set the category ID in the metadata
631
        $metadata[UrlRewriteObserver::CATEGORY_ID] = (string) $category[MemberNames::ENTITY_ID];
632 3
633
        // return the metadata
634
        return $metadata;
635
    }
636
637
    /**
638
     * Query whether or not the actual entity is visible.
639
     *
640
     * @return boolean TRUE if the entity is visible, else FALSE
641 3
     */
642
    protected function isVisible()
643 3
    {
644
        return $this->getEntityIdVisibilityIdMapping() !== VisibilityKeys::VISIBILITY_NOT_VISIBLE;
645
    }
646
647
    /**
648
     * Return's the visibility for the passed entity ID, if it already has been mapped. The mapping will be created
649
     * by calling <code>\TechDivision\Import\Product\Subjects\BunchSubject::getVisibilityIdByValue</code> which will
650
     * be done by the <code>\TechDivision\Import\Product\Callbacks\VisibilityCallback</code>.
651
     *
652
     * @return integer The visibility ID
653 3
     * @throws \Exception Is thrown, if the entity ID has not been mapped
654
     * @see \TechDivision\Import\Product\Subjects\BunchSubject::getVisibilityIdByValue()
655
     */
656
    protected function getEntityIdVisibilityIdMapping()
657 3
    {
658
        return $this->getSubject()->getEntityIdVisibilityIdMapping();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getEntityIdVisibilityIdMapping() does only exist in the following implementations of said interface: TechDivision\Import\Prod...jects\UrlRewriteSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
659
    }
660 3
661
    /**
662
     * Return's the root category for the actual view store.
663
     *
664
     * @return array The store's root category
665
     * @throws \Exception Is thrown if the root category for the passed store code is NOT available
666
     */
667
    protected function getRootCategory()
668
    {
669
        return $this->getSubject()->getRootCategory();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getRootCategory() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Prod...jects\UrlRewriteSubject, TechDivision\Import\Subjects\AbstractEavSubject, TechDivision\Import\Subjects\AbstractSubject, TechDivision\Import\Subjects\MoveFilesSubject, TechDivision\Import\Subjects\ValidatorSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
670
    }
671 2
672
    /**
673 2
     * Return's TRUE if the passed category IS the root category, else FALSE.
674
     *
675
     * @param array $category The category to query
676
     *
677
     * @return boolean TRUE if the passed category IS the root category
678
     */
679
    protected function isRootCategory(array $category)
680
    {
681
        return isset($this->rootCategories[$category[MemberNames::ENTITY_ID]]);
682
    }
683
684 2
    /**
685
     * Return's the category with the passed path.
686 2
     *
687
     * @param string $path          The path of the category to return
688
     * @param string $storeViewCode The store view code of the category to return
689
     *
690
     * @return array The category
691
     */
692
    protected function getCategoryByPath($path, $storeViewCode = StoreViewCodes::ADMIN)
693
    {
694
        return $this->getSubject()->getCategoryByPath($path, $storeViewCode);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getCategoryByPath() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Prod...jects\UrlRewriteSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
695
    }
696 3
697
    /**
698 3
     * Return's the category with the passed ID.
699
     *
700
     * @param integer $categoryId    The ID of the category to return
701
     * @param string  $storeViewCode The store view code of category to return
702
     *
703
     * @return array The category data
704
     */
705
    protected function getCategory($categoryId, $storeViewCode = StoreViewCodes::ADMIN)
706
    {
707
        return $this->getSubject()->getCategory($categoryId, $storeViewCode);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getCategory() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Prod...jects\UrlRewriteSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
708 2
    }
709
710 2
    /**
711
     * Persist's the URL rewrite with the passed data.
712
     *
713
     * @param array $row The URL rewrite to persist
714
     *
715
     * @return string The ID of the persisted entity
716
     */
717
    protected function persistUrlRewrite($row)
718
    {
719
        return $this->getProductUrlRewriteProcessor()->persistUrlRewrite($row);
720
    }
721 3
722
    /**
723 3
     * Persist's the URL rewrite product => category relation with the passed data.
724
     *
725
     * @param array $row The URL rewrite product => category relation to persist
726
     *
727
     * @return void
728
     */
729
    protected function persistUrlRewriteProductCategory($row)
730
    {
731
        return $this->getProductUrlRewriteProcessor()->persistUrlRewriteProductCategory($row);
732
    }
733 3
734
    /**
735 3
     * Queries whether or not the passed SKU and store view code has already been processed.
736 3
     *
737
     * @param string $sku           The SKU to check been processed
738
     * @param string $storeViewCode The store view code to check been processed
739
     *
740
     * @return boolean TRUE if the SKU and store view code has been processed, else FALSE
741
     */
742
    protected function storeViewHasBeenProcessed($sku, $storeViewCode)
743
    {
744
        return $this->getSubject()->storeViewHasBeenProcessed($sku, $storeViewCode);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method storeViewHasBeenProcessed() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Prod...jects\UrlRewriteSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
745 3
    }
746
747 3
    /**
748 3
     * Add the entity ID => visibility mapping for the actual entity ID.
749
     *
750
     * @param string $visibility The visibility of the actual entity to map
751
     *
752
     * @return void
753
     */
754
    protected function addEntityIdVisibilityIdMapping($visibility)
755
    {
756
        $this->getSubject()->addEntityIdVisibilityIdMapping($visibility);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method addEntityIdVisibilityIdMapping() does only exist in the following implementations of said interface: TechDivision\Import\Prod...jects\UrlRewriteSubject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
757 3
    }
758
759 3
    /**
760
     * Set's the ID of the product that has been created recently.
761
     *
762
     * @param string $lastEntityId The entity ID
763
     *
764
     * @return void
765
     */
766
    protected function setLastEntityId($lastEntityId)
767
    {
768
        $this->getSubject()->setLastEntityId($lastEntityId);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method setLastEntityId() does only exist in the following implementations of said interface: TechDivision\Import\Plugins\ExportableSubjectImpl, TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Prod...jects\UrlRewriteSubject, TechDivision\Import\Subjects\ExportableTraitImpl.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
769
    }
770
771
    /**
772
     * Load's and return's the product with the passed SKU.
773
     *
774
     * @param string $sku The SKU of the product to load
775
     *
776
     * @return array The product
777
     */
778
    protected function loadProduct($sku)
779
    {
780
        return $this->getProductUrlRewriteProcessor()->loadProduct($sku);
781
    }
782
}
783