Completed
Push — master ( 477d2b...3ad691 )
by Tim
02:42 queued 01:09
created

UrlKeyAndPathObserver::process()   D

Complexity

Conditions 17
Paths 90

Size

Total Lines 94

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 94
rs 4.3042
c 0
b 0
f 0
cc 17
nc 90
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TechDivision\Import\Category\Observers\UrlKeyAndPathObserver
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 2019 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-category
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Category\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\Category\Utils\ConfigurationKeys;
29
use TechDivision\Import\Category\Utils\ColumnKeys;
30
use TechDivision\Import\Category\Utils\MemberNames;
31
use TechDivision\Import\Category\Services\CategoryBunchProcessorInterface;
32
use TechDivision\Import\Utils\Generators\GeneratorInterface;
33
34
/**
35
 * Observer that extracts the URL key/path from the category path
36
 * and adds them as two new columns with the their values.
37
 *
38
 * @author    Tim Wagner <[email protected]>
39
 * @copyright 2019 TechDivision GmbH <[email protected]>
40
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
41
 * @link      https://github.com/techdivision/import-category
42
 * @link      http://www.techdivision.com
43
 */
44
class UrlKeyAndPathObserver extends AbstractCategoryImportObserver
45
{
46
47
    /**
48
     * The trait that provides string => URL key conversion functionality.
49
     *
50
     * @var \TechDivision\Import\Utils\Filter\UrlKeyFilterTrait
51
     */
52
    use UrlKeyFilterTrait;
53
54
    /**
55
     * The URL key utility instance.
56
     *
57
     * @var \TechDivision\Import\Utils\UrlKeyUtilInterface
58
     */
59
    protected $urlKeyUtil;
60
61
    /**
62
     * The category bunch processor instance.
63
     *
64
     * @var \TechDivision\Import\Category\Services\CategoryBunchProcessorInterface
65
     */
66
    protected $categoryBunchProcessor;
67
68
    /**
69
     * The reverse sequence generator instance.
70
     *
71
     * @var \TechDivision\Import\Utils\Generators\GeneratorInterface
72
     */
73
    protected $reverseSequenceGenerator;
74
75
    /**
76
     * Initialize the observer with the passed product bunch processor instance.
77
     *
78
     * @param \TechDivision\Import\Category\Services\CategoryBunchProcessorInterface $categoryBunchProcessor   The category bunch processor instance
79
     * @param \Zend\Filter\FilterInterface                                           $convertLiteralUrlFilter  The URL filter instance
80
     * @param \TechDivision\Import\Utils\UrlKeyUtilInterface                         $urlKeyUtil               The URL key utility instance
81
     * @param \TechDivision\Import\Utils\Generators\GeneratorInterface               $reverseSequenceGenerator The reverse sequence generator instance
82
     */
83
    public function __construct(
84
        CategoryBunchProcessorInterface $categoryBunchProcessor,
85
        FilterInterface $convertLiteralUrlFilter,
86
        UrlKeyUtilInterface $urlKeyUtil,
87
        GeneratorInterface $reverseSequenceGenerator
88
    ) {
89
90
        // set the processor and the URL filter instance
91
        $this->categoryBunchProcessor = $categoryBunchProcessor;
92
        $this->convertLiteralUrlFilter = $convertLiteralUrlFilter;
93
        $this->urlKeyUtil = $urlKeyUtil;
94
        $this->reverseSequenceGenerator = $reverseSequenceGenerator;
95
    }
96
97
    /**
98
     * Process the observer's business logic.
99
     *
100
     * @return void
101
     */
102
    protected function process()
103
    {
104
105
        // initialize the URL key, the entity and the category
106
        $urlKey = null;
107
        $entity = null;
108
        $category = array();
109
110
        // prepare the store view code
111
        $this->prepareStoreViewCode();
112
113
        // set the entity ID for the category with the passed path
114
        try {
115
            $entity = $this->getCategoryByPath($path = $this->getValue(ColumnKeys::PATH));
116
            $this->setIds($category = $entity);
117
        } catch (\Exception $e) {
118
            $this->setIds(array());
119
            $category[MemberNames::ENTITY_ID] = $this->getReverseSequenceGenerator()->generate();
0 ignored issues
show
Bug introduced by
The call to generate() misses a required argument $entity.

This check looks for function calls that miss required arguments.

Loading history...
120
        }
121
122
        // query whether or not the URL key column has a value
123
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
124
            $urlKey = $this->getValue(ColumnKeys::URL_KEY);
125
        } else {
126
            // query whether or not the existing category `url_key` should be re-created from the category name
127
            if (is_array($entity) && !$this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME, true)) {
128
                // if the category already exists and NO re-creation from the category name has to
129
                // be done, load the original `url_key`from the category and use that to proceed
130
                $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...
131
            }
132
133
            // try to load the value from column `name` if URL key is still
134
            // empty, because we need it to process the the rewrites later on
135
            if ($urlKey === null || $urlKey === '' && $this->hasValue(ColumnKeys::NAME)) {
136
                $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
137
            }
138
        }
139
140
        // stop processing, if no URL key is available
141
        if ($urlKey === null || $urlKey === '') {
142
            // throw an exception, that the URL key can not be
143
            // initialized and we're in the default store view
144
            if ($this->getStoreViewCode(StoreViewCodes::ADMIN) === StoreViewCodes::ADMIN) {
145
                throw new \Exception(sprintf('Can\'t initialize the URL key for category "%s" because columns "url_key" or "name" have a value set for default store view', $path));
146
            }
147
            // stop processing, because we're in a store
148
            // view row and a URL key is not mandatory
149
            return;
150
        }
151
152
        // load ID of the actual store view
153
        $storeId = $this->getRowStoreId(StoreViewCodes::ADMIN);
154
155
        // explode the path into the category names
156
        if ($categories = $this->explode($this->getValue(ColumnKeys::PATH), '/')) {
157
            // initialize the array for the category paths
158
            $categoryPaths = array();
159
            // iterate over the parent category names and try
160
            // to load the categories to build the URL path
161
            for ($i = sizeof($categories) - 1; $i > 1; $i--) {
162
                try {
163
                    // prepare the expected category name
164
                    $categoryPath = implode('/', array_slice($categories, 0, $i));
165
                    // load the existing category and prepend the URL key the array with the category URL keys
166
                    $existingCategory = $this->getCategoryByPkAndStoreId($this->mapPath($categoryPath), $storeId);
167
                    // query whether or not an URL key is available or not
168
                    if (isset($existingCategory[MemberNames::URL_KEY])) {
169
                        array_unshift($categoryPaths, $existingCategory[MemberNames::URL_KEY]);
170
                    } else {
171
                        $this->getSystemLogger()->debug(sprintf('Can\'t find URL key for category "%s"', $categoryPath));
172
                    }
173
                } catch (\Exception $e) {
174
                    $this->getSystemLogger()->debug(sprintf('Can\'t load parent category "%s"', $categoryPath));
175
                }
176
            }
177
        }
178
179
        // update the URL key with the unique value
180
        $this->setValue(
181
            ColumnKeys::URL_KEY,
182
            $urlKey = $this->makeUnique($this->getSubject(), $category, $urlKey, sizeof($categoryPaths) > 0 ? array(implode('/', $categoryPaths)) : array())
0 ignored issues
show
Bug introduced by
The variable $categoryPaths does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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...
183
        );
184
185
        // finally, append the URL key as last element to the path
186
        array_push($categoryPaths, $urlKey);
187
188
        // create the virtual column for the URL path
189
        if ($this->hasHeader(ColumnKeys::URL_PATH) === false) {
190
            $this->addHeader(ColumnKeys::URL_PATH);
191
        }
192
193
        // set the URL path
194
        $this->setValue(ColumnKeys::URL_PATH, implode('/', $categoryPaths));
195
    }
196
197
    /**
198
     * Return the primary key member name.
199
     *
200
     * @return string The primary key member name
201
     */
202
    protected function getPkMemberName()
203
    {
204
        return MemberNames::ENTITY_ID;
205
    }
206
207
    /**
208
     * Returns the category bunch processor instance.
209
     *
210
     * @return \TechDivision\Import\Category\Services\CategoryBunchProcessorInterface The category bunch processor instance
211
     */
212
    protected function getCategoryBunchProcessor()
213
    {
214
        return $this->categoryBunchProcessor;
215
    }
216
217
    /**
218
     * Returns the URL key utility instance.
219
     *
220
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
221
     */
222
    protected function getUrlKeyUtil()
223
    {
224
        return $this->urlKeyUtil;
225
    }
226
227
    /**
228
     * Returns the reverse sequence generator instance.
229
     *
230
     * @return \TechDivision\Import\Utils\Generators\GeneratorInterface The reverse sequence generator
231
     */
232
    protected function getReverseSequenceGenerator() : GeneratorInterface
233
    {
234
        return $this->reverseSequenceGenerator;
235
    }
236
237
    /**
238
     * Return's the category with the passed path.
239
     *
240
     * @param string $path The path of the category to return
241
     *
242
     * @return array The category
243
     * @throws \Exception Is thrown, if the requested category is not available
244
     */
245
    protected function getCategoryByPath($path)
246
    {
247
        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\Cate...AbstractCategorySubject, TechDivision\Import\Category\Subjects\BunchSubject, TechDivision\Import\Cate...ts\SortableBunchSubject.

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...
248
    }
249
250
    /**
251
     * Returns the category with the passed primary key and the attribute values for the passed store ID.
252
     *
253
     * @param string  $pk      The primary key of the category to return
254
     * @param integer $storeId The store ID of the category values
255
     *
256
     * @return array|null The category data
257
     */
258
    protected function getCategoryByPkAndStoreId($pk, $storeId)
259
    {
260
        return $this->getCategoryBunchProcessor()->getCategoryByPkAndStoreId($pk, $storeId);
261
    }
262
263
    /**
264
     * Temporarily persist's the IDs of the passed category.
265
     *
266
     * @param array $category The category to temporarily persist the IDs for
267
     *
268
     * @return void
269
     */
270
    protected function setIds(array $category)
271
    {
272
        $this->setLastEntityId(isset($category[MemberNames::ENTITY_ID]) ? $category[MemberNames::ENTITY_ID] : null);
273
    }
274
275
    /**
276
     * Set's the ID of the category that has been created recently.
277
     *
278
     * @param string $lastEntityId The entity ID
279
     *
280
     * @return void
281
     */
282
    protected function setLastEntityId($lastEntityId)
283
    {
284
        $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\Cate...AbstractCategorySubject, TechDivision\Import\Category\Subjects\BunchSubject, TechDivision\Import\Cate...ts\SortableBunchSubject, TechDivision\Import\Plugins\ExportableSubjectImpl, 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...
285
    }
286
287
    /**
288
     * Return's the PK to of the product.
289
     *
290
     * @return integer The PK to create the relation with
291
     */
292
    protected function getPrimaryKey()
293
    {
294
        $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\Cate...AbstractCategorySubject, TechDivision\Import\Category\Subjects\BunchSubject, TechDivision\Import\Cate...ts\SortableBunchSubject, TechDivision\Import\Observers\EntitySubjectImpl, TechDivision\Import\Plugins\ExportableSubjectImpl, 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...
295
    }
296
297
    /**
298
     * Load's and return's the url_key with the passed primary ID.
299
     *
300
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject      The subject to load the URL key
301
     * @param int                                                       $primaryKeyId The ID from category
302
     *
303
     * @return string|null url_key or null
304
     */
305
    protected function loadUrlKey(UrlKeyAwareSubjectInterface $subject, $primaryKeyId)
306
    {
307
        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...
308
    }
309
310
    /**
311
     * Make's the passed URL key unique by adding the next number to the end.
312
     *
313
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject  The subject to make the URL key unique for
314
     * @param array                                                     $entity   The entity to make the URL key unique for
315
     * @param string                                                    $urlKey   The URL key to make unique
316
     * @param array                                                     $urlPaths The URL paths to make unique
317
     *
318
     * @return string The unique URL key
319
     */
320
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, array $entity, string $urlKey, array $urlPaths = array())
321
    {
322
        return $this->getUrlKeyUtil()->makeUnique($subject, $entity, $urlKey, $urlPaths);
0 ignored issues
show
Documentation introduced by
$entity is of type array, but the function expects a string.

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 UrlKeyUtilInterface::makeUnique() has too many arguments starting with $urlKey.

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...
323
    }
324
}
325