Completed
Push — pac-264--pdo-exception ( cb452d...c8cc02 )
by
unknown
16:40
created

UrlKeyObserver::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
3
/**
4
 * TechDivision\Import\Product\Observers\UrlKeyObserver
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
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Product\Observers;
22
23
use TechDivision\Import\Product\Utils\ConfigurationKeys;
24
use Zend\Filter\FilterInterface;
25
use TechDivision\Import\Utils\StoreViewCodes;
26
use TechDivision\Import\Product\Utils\MemberNames;
27
use TechDivision\Import\Product\Utils\ColumnKeys;
28
use TechDivision\Import\Utils\Filter\UrlKeyFilterTrait;
29
use TechDivision\Import\Product\Services\ProductBunchProcessorInterface;
30
use TechDivision\Import\Utils\UrlKeyUtilInterface;
31
use TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface;
32
33
/**
34
 * Observer that extracts the URL key from the product name and adds a two new columns
35
 * with the their values.
36
 *
37
 * @author    Tim Wagner <[email protected]>
38
 * @copyright 2016 TechDivision GmbH <[email protected]>
39
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
40
 * @link      https://github.com/techdivision/import-product
41
 * @link      http://www.techdivision.com
42
 */
43
class UrlKeyObserver extends AbstractProductImportObserver
44
{
45
46
    /**
47
     * The trait that provides string => URL key conversion functionality.
48
     *
49
     * @var \TechDivision\Import\Utils\Filter\UrlKeyFilterTrait
50
     */
51
    use UrlKeyFilterTrait;
52
53
    /**
54
     * The URL key utility instance.
55
     *
56
     * @var \TechDivision\Import\Utils\UrlKeyUtilInterface
57
     */
58
    protected $urlKeyUtil;
59
60
    /**
61
     * The product bunch processor instance.
62
     *
63
     * @var \TechDivision\Import\Product\Services\ProductBunchProcessorInterface
64
     */
65
    protected $productBunchProcessor;
66
67
    /**
68
     * Initialize the observer with the passed product bunch processor and filter instance.
69
     *
70
     * @param \TechDivision\Import\Product\Services\ProductBunchProcessorInterface $productBunchProcessor   The product bunch processor instance
71
     * @param \Zend\Filter\FilterInterface                                         $convertLiteralUrlFilter The URL filter instance
72
     * @param \TechDivision\Import\Utils\UrlKeyUtilInterface                       $urlKeyUtil              The URL key utility instance
73
     */
74
    public function __construct(
75
        ProductBunchProcessorInterface $productBunchProcessor,
76
        FilterInterface $convertLiteralUrlFilter,
77
        UrlKeyUtilInterface $urlKeyUtil
78
    ) {
79
        $this->productBunchProcessor = $productBunchProcessor;
80
        $this->convertLiteralUrlFilter = $convertLiteralUrlFilter;
81
        $this->urlKeyUtil = $urlKeyUtil;
82
    }
83
84
    /**
85
     * Return's the product bunch processor instance.
86
     *
87
     * @return \TechDivision\Import\Product\Services\ProductBunchProcessorInterface The product bunch processor instance
88
     */
89
    protected function getProductBunchProcessor()
90
    {
91
        return $this->productBunchProcessor;
92
    }
93
94
    /**
95
     * Process the observer's business logic.
96
     *
97
     * @return void
98
     * @throws \Exception Is thrown, if either column "url_key" or "name" have a value set
99
     */
100
    protected function process()
101
    {
102
103
        // prepare the store view code
104
        $this->getSubject()->prepareStoreViewCode();
105
106
        // throw an exception, that the URL key can not be initialized and we're in admin store view
107
        if ($this->getSubject()->getStoreViewCode(StoreViewCodes::ADMIN) === StoreViewCodes::ADMIN
108
            && $this->hasValue(ColumnKeys::URL_KEY) === false
109
            && $this->hasValue(ColumnKeys::NAME) === false
110
        ) {
111
            throw new \Exception('Can\'t initialize the URL key because either columns "url_key" or "name" have a value set for default store view');
112
        }
113
114
        // set the entity ID for the product with the passed SKU
115
        if ($product = $this->loadProduct($this->getValue(ColumnKeys::SKU))) {
116
            $this->setIds($product);
117
        } else {
118
            $this->setIds(array());
119
        }
120
121
        // query whether or not an existing product `url_key` should recalc from product name
122
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
123
            $urlKey =  $this->getValue(ColumnKeys::URL_KEY);
124
        } else {
125
            // product already exists and NO recalc from product key,
126
            // so we search origin url_key from product
127
            $urlKey = $this->loadUrlKey(
128
                $this->getSubject(),
0 ignored issues
show
Documentation introduced by
$this->getSubject() is of type object<TechDivision\Impo...jects\SubjectInterface>, but the function expects a object<TechDivision\Impo...yAwareSubjectInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
129
                $this->getPrimaryKey()
130
            );
131
132
            // query whether or not a product name is available
133
            if (!$urlKey) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlKey of type string|null is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
134
                // initialize the URL key with the converted name
135
                $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
136
            }
137
        }
138
139
        // load the URL paths of the categories
140
        // found in the column `categories`
141
        $urlPaths = $this->getUrlPaths();
142
143
        // iterate over the found URL paths and try to find a unique URL key
144
        for ($i = 0; $i < sizeof($urlPaths); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function sizeof() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
145
            // try to make the URL key unique for the given URL path
146
            $proposedUrlKey = $this->makeUnique($this->getSubject(), $urlKey, $urlPaths[$i]);
0 ignored issues
show
Documentation introduced by
$this->getSubject() is of type object<TechDivision\Impo...jects\SubjectInterface>, but the function expects a object<TechDivision\Impo...yAwareSubjectInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Unused Code introduced by
The call to UrlKeyObserver::makeUnique() has too many arguments starting with $urlPaths[$i].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
147
            // if the URL key is NOT the same as the passed one or with the parent URL path
148
            // it can NOT be used, so we've to persist it temporarily and try it again for
149
            // all the other URL paths until we found one that works with every URL path
150
            if ($urlKey !== $proposedUrlKey) {
151
                // temporarily persist the URL key
152
                $urlKey = $proposedUrlKey;
153
                // reset the counter and restart the
154
                // iteration with the first URL path
155
                $i = 0;
156
            }
157
        }
158
159
        // set the unique URL key for further processing
160
        $this->setValue(ColumnKeys::URL_KEY, $urlKey);
161
    }
162
163
    /**
164
     * Extract's the category from the comma separeted list of categories from
165
     * the column `categories` and return's an array with their URL paths.
166
     *
167
     * @return string[] Array with the URL paths of the categories found in column `categories`
168
     * @todo The list has to be exended by the URL paths of the parent categories that has the anchor flag been acitvated
169
     */
170
    protected function getUrlPaths()
171
    {
172
173
        // initialize the array for the URL paths of the cateogries
174
        $urlPaths = array();
175
176
        // extract the categories from the column `categories`
177
        $categories = $this->getValue(ColumnKeys::CATEGORIES, array(), array($this, 'explode'));
178
179
        // iterate of the found categories, load
180
        // their URL path and add it the array
181
        foreach ($categories as $path) {
182
            $category = $this->getCategoryByPath($path);
183
            $urlPaths[] = $category[MemberNames::URL_PATH];
184
        }
185
186
        // return the array with the categories URL paths
187
        return $urlPaths;
188
    }
189
190
    /**
191
     * Temporarily persist's the IDs of the passed product.
192
     *
193
     * @param array $product The product to temporarily persist the IDs for
194
     *
195
     * @return void
196
     */
197
    protected function setIds(array $product)
198
    {
199
        $this->setLastEntityId(isset($product[MemberNames::ENTITY_ID]) ? $product[MemberNames::ENTITY_ID] : null);
200
    }
201
202
    /**
203
     * Set's the ID of the product that has been created recently.
204
     *
205
     * @param string $lastEntityId The entity ID
206
     *
207
     * @return void
208
     */
209
    protected function setLastEntityId($lastEntityId)
210
    {
211
        $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\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...
212
    }
213
214
    /**
215
     * Return's the PK to of the product.
216
     *
217
     * @return integer The PK to create the relation with
218
     */
219
    protected function getPrimaryKey()
220
    {
221
        $this->getSubject()->getLastEntityId();
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 getLastEntityId() does only exist in the following implementations of said interface: TechDivision\Import\Observers\EntitySubjectImpl, TechDivision\Import\Plugins\ExportableSubjectImpl, TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, 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...
222
    }
223
224
    /**
225
     * Load's and return's the product with the passed SKU.
226
     *
227
     * @param string $sku The SKU of the product to load
228
     *
229
     * @return array The product
230
     */
231
    protected function loadProduct($sku)
232
    {
233
        return $this->getProductBunchProcessor()->loadProduct($sku);
234
    }
235
236
    /**
237
     * Load's and return's the url_key with the passed primary ID.
238
     *
239
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject      The subject to load the URL key
240
     * @param int                                                       $primaryKeyId The ID from product
241
     *
242
     * @return string|null url_key or null
243
     */
244
    protected function loadUrlKey(UrlKeyAwareSubjectInterface $subject, $primaryKeyId)
245
    {
246
        return $this->getUrlKeyUtil()->loadUrlKey($subject, $primaryKeyId);
0 ignored issues
show
Bug introduced by
The method loadUrlKey() does not seem to exist on object<TechDivision\Impo...ls\UrlKeyUtilInterface>.

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...
247
    }
248
249
    /**
250
     * Return's the category with the passed path.
251
     *
252
     * @param string $path The path of the category to return
253
     *
254
     * @return array The category
255
     */
256
    protected function getCategoryByPath($path)
257
    {
258
        return $this->getSubject()->getCategoryByPath($path);
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.

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...
259
    }
260
261
    /**
262
     * Returns the URL key utility instance.
263
     *
264
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
265
     */
266
    protected function getUrlKeyUtil()
267
    {
268
        return $this->urlKeyUtil;
269
    }
270
271
    /**
272
     * Make's the passed URL key unique by adding the next number to the end.
273
     *
274
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject The subject to make the URL key unique for
275
     * @param string                                                    $urlKey  The URL key to make unique
276
     *
277
     * @return string The unique URL key
278
     */
279
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, $urlKey)
280
    {
281
        return $this->getUrlKeyUtil()->makeUnique($subject, $urlKey);
282
    }
283
}
284