Completed
Push — pac-264--pdo-exception ( c8cc02...5bd829 )
by Tim
07:45
created

UrlKeyObserver::loadUrlKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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 Zend\Filter\FilterInterface;
24
use TechDivision\Import\Utils\StoreViewCodes;
25
use TechDivision\Import\Utils\UrlKeyUtilInterface;
26
use TechDivision\Import\Utils\Filter\UrlKeyFilterTrait;
27
use TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface;
28
use TechDivision\Import\Product\Utils\MemberNames;
29
use TechDivision\Import\Product\Utils\ColumnKeys;
30
use TechDivision\Import\Product\Utils\ConfigurationKeys;
31
use TechDivision\Import\Product\Services\ProductBunchProcessorInterface;
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
        // initialize the URL key and the product
104
        $urlKey = null;
105
        $product = array();
0 ignored issues
show
Unused Code introduced by
$product is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
106
107
        // prepare the store view code
108
        $this->getSubject()->prepareStoreViewCode();
109
110
        // set the entity ID for the product with the passed SKU
111
        if ($product = $this->loadProduct($sku = $this->getValue(ColumnKeys::SKU))) {
112
            $this->setIds($product);
113
        } else {
114
            $this->setIds(array());
115
        }
116
117
        // query whether or not the URL key column has a value
118
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
119
            $urlKey = $this->getValue(ColumnKeys::URL_KEY);
120
        } else {
121
            // query whether or not the existing product `url_key` should be re-created from the product name
122
            if ($product && !$this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME, true)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $product of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
123
                // if the product already exists and NO re-creation from the product name has to
124
                // be done, load the original `url_key`from the product and use that to proceed
125
                $urlKey = $this->loadUrlKey($this->getSubject(), $this->getPrimaryKey());
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...
126
            }
127
128
            // try to load the value from column `name` if URL key is still
129
            // empty, because we need it to process the the rewrites later on
130
            if ($urlKey === null || $urlKey === '' && $this->hasValue(ColumnKeys::NAME)) {
131
                $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
132
            }
133
        }
134
135
        // stop processing, if no URL key is available
136
        if ($urlKey === null || $urlKey === '') {
137
            // throw an exception, that the URL key can not be
138
            // initialized and we're in the default store view
139
            if ($this->getSubject()->getStoreViewCode(StoreViewCodes::ADMIN) === StoreViewCodes::ADMIN) {
140
                throw new \Exception(sprintf('Can\'t initialize the URL key for product "%s" because columns "url_key" or "name" have a value set for default store view', $sku));
141
            }
142
            // stop processing, because we're in a store
143
            // view row and a URL key is not mandatory
144
            return;
145
        }
146
147
        // try to load the URL paths of the categories found in the column
148
        // `categories, if not available, simply make the URL key unique
149
        if (sizeof($urlPaths = $this->getUrlPaths()) === 0) {
150
            $urlKey = $this->makeUnique($this->getSubject(), $urlKey);
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...
151
        } else {
152
            // otherwise iterate over the found URL
153
            // paths and try to find a unique URL key
154
            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...
155
                // try to make the URL key unique for the given URL path
156
                $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...
157
                // if the URL key is NOT the same as the passed one or with the parent URL path
158
                // it can NOT be used, so we've to persist it temporarily and try it again for
159
                // all the other URL paths until we found one that works with every URL path
160
                if ($urlKey !== $proposedUrlKey) {
161
                    // temporarily persist the URL key
162
                    $urlKey = $proposedUrlKey;
163
                    // reset the counter and restart the
164
                    // iteration with the first URL path
165
                    $i = 0;
166
                }
167
            }
168
        }
169
170
        // set the unique URL key for further processing
171
        $this->setValue(ColumnKeys::URL_KEY, $urlKey);
172
    }
173
174
    /**
175
     * Extract's the category from the comma separeted list of categories from
176
     * the column `categories` and return's an array with their URL paths.
177
     *
178
     * @return string[] Array with the URL paths of the categories found in column `categories`
179
     * @todo The list has to be exended by the URL paths of the parent categories that has the anchor flag been acitvated
180
     */
181
    protected function getUrlPaths()
182
    {
183
184
        // initialize the array for the URL paths of the cateogries
185
        $urlPaths = array();
186
187
        // extract the categories from the column `categories`
188
        $categories = $this->getValue(ColumnKeys::CATEGORIES, array(), array($this, 'explode'));
189
190
        // iterate of the found categories, load
191
        // their URL path and add it the array
192
        foreach ($categories as $path) {
193
            $category = $this->getCategoryByPath($path);
194
            $urlPaths[] = $category[MemberNames::URL_PATH];
195
        }
196
197
        // return the array with the categories URL paths
198
        return $urlPaths;
199
    }
200
201
    /**
202
     * Temporarily persist's the IDs of the passed product.
203
     *
204
     * @param array $product The product to temporarily persist the IDs for
205
     *
206
     * @return void
207
     */
208
    protected function setIds(array $product)
209
    {
210
        $this->setLastEntityId(isset($product[MemberNames::ENTITY_ID]) ? $product[MemberNames::ENTITY_ID] : null);
211
    }
212
213
    /**
214
     * Set's the ID of the product that has been created recently.
215
     *
216
     * @param string $lastEntityId The entity ID
217
     *
218
     * @return void
219
     */
220
    protected function setLastEntityId($lastEntityId)
221
    {
222
        $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...
223
    }
224
225
    /**
226
     * Return's the PK to of the product.
227
     *
228
     * @return integer The PK to create the relation with
229
     */
230
    protected function getPrimaryKey()
231
    {
232
        $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...
233
    }
234
235
    /**
236
     * Load's and return's the product with the passed SKU.
237
     *
238
     * @param string $sku The SKU of the product to load
239
     *
240
     * @return array The product
241
     */
242
    protected function loadProduct($sku)
243
    {
244
        return $this->getProductBunchProcessor()->loadProduct($sku);
245
    }
246
247
    /**
248
     * Load's and return's the url_key with the passed primary ID.
249
     *
250
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject      The subject to load the URL key
251
     * @param int                                                       $primaryKeyId The ID from product
252
     *
253
     * @return string|null url_key or null
254
     */
255
    protected function loadUrlKey(UrlKeyAwareSubjectInterface $subject, $primaryKeyId)
256
    {
257
        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...
258
    }
259
260
    /**
261
     * Return's the category with the passed path.
262
     *
263
     * @param string $path The path of the category to return
264
     *
265
     * @return array The category
266
     */
267
    protected function getCategoryByPath($path)
268
    {
269
        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...
270
    }
271
272
    /**
273
     * Returns the URL key utility instance.
274
     *
275
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
276
     */
277
    protected function getUrlKeyUtil()
278
    {
279
        return $this->urlKeyUtil;
280
    }
281
282
    /**
283
     * Make's the passed URL key unique by adding the next number to the end.
284
     *
285
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject The subject to make the URL key unique for
286
     * @param string                                                    $urlKey  The URL key to make unique
287
     *
288
     * @return string The unique URL key
289
     */
290
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, $urlKey)
291
    {
292
        return $this->getUrlKeyUtil()->makeUnique($subject, $urlKey);
293
    }
294
}
295