Completed
Pull Request — master (#162)
by
unknown
17:12 queued 07:16
created

UrlKeyObserver::process()   C

Complexity

Conditions 11
Paths 15

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
dl 0
loc 64
ccs 0
cts 32
cp 0
rs 6.6387
c 0
b 0
f 0
cc 11
nc 15
nop 0
crap 132

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\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 the URL key column has a
122
        // value, if yes, use the value from the column
123
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
124
            $urlKey =  $this->getValue(ColumnKeys::URL_KEY);
125
        } else {
126
            // query whether or not the column `url_key` has a value
127
            if ($product &&
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...
128
                $this->getSubject()->getConfiguration()->hasParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME) &&
129
                !$this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME, true)
130
            ) {
131
                // product already exists and NO new URL key
132
                // has been specified in column `url_key`, so
133
                // we stop processing here
134
                return;
135
            }
136
137
            // initialize the URL key with the converted name
138
            $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
139
        }
140
141
        // load the URL paths of the categories
142
        // found in the column `categories`
143
        $urlPaths = $this->getUrlPaths();
144
145
        // iterate over the found URL paths and try to find a unique URL key
146
        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...
147
            // try to make the URL key unique for the given URL path
148
            $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...
149
            // if the URL key is NOT the same as the passed one or with the parent URL path
150
            // it can NOT be used, so we've to persist it temporarily and try it again for
151
            // all the other URL paths until we found one that works with every URL path
152
            if ($urlKey !== $proposedUrlKey) {
153
                // temporarily persist the URL key
154
                $urlKey = $proposedUrlKey;
155
                // reset the counter and restart the
156
                // iteration with the first URL path
157
                $i = 0;
158
            }
159
        }
160
161
        // set the unique URL key for further processing
162
        $this->setValue(ColumnKeys::URL_KEY, $urlKey);
163
    }
164
165
    /**
166
     * Extract's the category from the comma separeted list of categories from
167
     * the column `categories` and return's an array with their URL paths.
168
     *
169
     * @return string[] Array with the URL paths of the categories found in column `categories`
170
     * @todo The list has to be exended by the URL paths of the parent categories that has the anchor flag been acitvated
171
     */
172
    protected function getUrlPaths()
173
    {
174
175
        // initialize the array for the URL paths of the cateogries
176
        $urlPaths = array();
177
178
        // extract the categories from the column `categories`
179
        $categories = $this->getValue(ColumnKeys::CATEGORIES, array(), array($this, 'explode'));
180
181
        // iterate of the found categories, load
182
        // their URL path and add it the array
183
        foreach ($categories as $path) {
184
            $category = $this->getCategoryByPath($path);
185
            $urlPaths[] = $category[MemberNames::URL_PATH];
186
        }
187
188
        // return the array with the categories URL paths
189
        return $urlPaths;
190
    }
191
192
    /**
193
     * Temporarily persist's the IDs of the passed product.
194
     *
195
     * @param array $product The product to temporarily persist the IDs for
196
     *
197
     * @return void
198
     */
199
    protected function setIds(array $product)
200
    {
201
        $this->setLastEntityId(isset($product[MemberNames::ENTITY_ID]) ? $product[MemberNames::ENTITY_ID] : null);
202
    }
203
204
    /**
205
     * Set's the ID of the product that has been created recently.
206
     *
207
     * @param string $lastEntityId The entity ID
208
     *
209
     * @return void
210
     */
211
    protected function setLastEntityId($lastEntityId)
212
    {
213
        $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...
214
    }
215
216
    /**
217
     * Load's and return's the product with the passed SKU.
218
     *
219
     * @param string $sku The SKU of the product to load
220
     *
221
     * @return array The product
222
     */
223
    protected function loadProduct($sku)
224
    {
225
        return $this->getProductBunchProcessor()->loadProduct($sku);
226
    }
227
228
    /**
229
     * Return's the category with the passed path.
230
     *
231
     * @param string $path The path of the category to return
232
     *
233
     * @return array The category
234
     */
235
    protected function getCategoryByPath($path)
236
    {
237
        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...
238
    }
239
240
    /**
241
     * Returns the URL key utility instance.
242
     *
243
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
244
     */
245
    protected function getUrlKeyUtil()
246
    {
247
        return $this->urlKeyUtil;
248
    }
249
250
    /**
251
     * Make's the passed URL key unique by adding the next number to the end.
252
     *
253
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject The subject to make the URL key unique for
254
     * @param string                                                    $urlKey  The URL key to make unique
255
     *
256
     * @return string The unique URL key
257
     */
258
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, $urlKey)
259
    {
260
        return $this->getUrlKeyUtil()->makeUnique($subject, $urlKey);
261
    }
262
}
263