Completed
Push — pac-264--pdo-exception ( 132ece )
by Tim
09:00
created

UrlKeyObserver::getUrlPaths()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
cc 2
nc 2
nop 0
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\Product\Utils\MemberNames;
26
use TechDivision\Import\Product\Utils\ColumnKeys;
27
use TechDivision\Import\Utils\Filter\UrlKeyFilterTrait;
28
use TechDivision\Import\Product\Services\ProductBunchProcessorInterface;
29
use TechDivision\Import\Utils\UrlKeyUtilInterface;
30
use TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface;
31
32
/**
33
 * Observer that extracts the URL key from the product name and adds a two new columns
34
 * with the their values.
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
40
 * @link      http://www.techdivision.com
41
 */
42
class UrlKeyObserver extends AbstractProductImportObserver
43
{
44
45
    /**
46
     * The trait that provides string => URL key conversion functionality.
47
     *
48
     * @var \TechDivision\Import\Utils\Filter\UrlKeyFilterTrait
49
     */
50
    use UrlKeyFilterTrait;
51
52
    /**
53
     * The URL key utility instance.
54
     *
55
     * @var \TechDivision\Import\Utils\UrlKeyUtilInterface
56
     */
57
    protected $urlKeyUtil;
58
59
    /**
60
     * The product bunch processor instance.
61
     *
62
     * @var \TechDivision\Import\Product\Services\ProductBunchProcessorInterface
63
     */
64
    protected $productBunchProcessor;
65
66
    /**
67
     * Initialize the observer with the passed product bunch processor and filter instance.
68
     *
69
     * @param \TechDivision\Import\Product\Services\ProductBunchProcessorInterface $productBunchProcessor   The product bunch processor instance
70
     * @param \Zend\Filter\FilterInterface                                         $convertLiteralUrlFilter The URL filter instance
71
     * @param \TechDivision\Import\Utils\UrlKeyUtilInterface                       $urlKeyUtil              The URL key utility instance
72
     */
73
    public function __construct(
74
        ProductBunchProcessorInterface $productBunchProcessor,
75
        FilterInterface $convertLiteralUrlFilter,
76
        UrlKeyUtilInterface $urlKeyUtil
77
    ) {
78
        $this->productBunchProcessor = $productBunchProcessor;
79
        $this->convertLiteralUrlFilter = $convertLiteralUrlFilter;
80
        $this->urlKeyUtil = $urlKeyUtil;
81
    }
82
83
    /**
84
     * Return's the product bunch processor instance.
85
     *
86
     * @return \TechDivision\Import\Product\Services\ProductBunchProcessorInterface The product bunch processor instance
87
     */
88
    protected function getProductBunchProcessor()
89
    {
90
        return $this->productBunchProcessor;
91
    }
92
93
    /**
94
     * Process the observer's business logic.
95
     *
96
     * @return void
97
     * @throws \Exception Is thrown, if either column "url_key" or "name" have a value set
98
     * @todo See PAC-307
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 column `url_key` has a value
122
        if ($product && $this->hasValue(ColumnKeys::URL_KEY) === false) {
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
            // @todo See PAC-307
124
            // product already exists and NO new URL key
125
            // has been specified in column `url_key`, so
126
            // we stop processing here
127
128
            return;
129
        }
130
131
        // query whether or not the URL key column has a
132
        // value, if yes, use the value from the column
133
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
134
            $urlKey =  $this->getValue(ColumnKeys::URL_KEY);
135
        } else {
136
            // initialize the URL key with the converted name
137
            $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
138
        }
139
140
        // load the URL paths of the categories
141
        // found in the column `categories`
142
        $urlPaths = $this->getUrlPaths();
143
144
        // iterate over the found URL paths and try to find a unique URL key
145
        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...
146
            // try to make the URL key unique for the given URL path
147
            $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...
148
            // if the URL key is NOT the same as the passed one or with the parent URL path
149
            // it can NOT be used, so we've to persist it temporarily and try it again for
150
            // all the other URL paths until we found one that works with every URL path
151
            if ($urlKey !== $proposedUrlKey) {
152
                // temporarily persist the URL key
153
                $urlKey = $proposedUrlKey;
154
                // reset the counter and restart the
155
                // iteration with the first URL path
156
                $i = 0;
157
            }
158
        }
159
160
        // set the unique URL key for further processing
161
        $this->setValue(ColumnKeys::URL_KEY, $urlKey);
162
    }
163
164
    /**
165
     * Extract's the category from the comma separeted list of categories from
166
     * the column `categories` and return's an array with their URL paths.
167
     *
168
     * @return string[] Array with the URL paths of the categories found in column `categories`
169
     * @todo The list has to be exended by the URL paths of the parent categories that has the anchor flag been acitvated
170
     */
171
    protected function getUrlPaths()
172
    {
173
174
        // initialize the array for the URL paths of the cateogries
175
        $urlPaths = array();
176
177
        // extract the categories from the column `categories`
178
        $categories = $this->getValue(ColumnKeys::CATEGORIES, array(), array($this, 'explode'));
179
180
        // iterate of the found categories, load
181
        // their URL path and add it the array
182
        foreach ($categories as $path) {
183
            $category = $this->getCategoryByPath($path);
184
            $urlPaths[] = $category[MemberNames::URL_PATH];
185
        }
186
187
        // return the array with the categories URL paths
188
        return $urlPaths;
189
    }
190
191
    /**
192
     * Temporarily persist's the IDs of the passed product.
193
     *
194
     * @param array $product The product to temporarily persist the IDs for
195
     *
196
     * @return void
197
     */
198
    protected function setIds(array $product)
199
    {
200
        $this->setLastEntityId(isset($product[MemberNames::ENTITY_ID]) ? $product[MemberNames::ENTITY_ID] : null);
201
    }
202
203
    /**
204
     * Set's the ID of the product that has been created recently.
205
     *
206
     * @param string $lastEntityId The entity ID
207
     *
208
     * @return void
209
     */
210
    protected function setLastEntityId($lastEntityId)
211
    {
212
        $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...
213
    }
214
215
    /**
216
     * Load's and return's the product with the passed SKU.
217
     *
218
     * @param string $sku The SKU of the product to load
219
     *
220
     * @return array The product
221
     */
222
    protected function loadProduct($sku)
223
    {
224
        return $this->getProductBunchProcessor()->loadProduct($sku);
225
    }
226
227
    /**
228
     * Return's the category with the passed path.
229
     *
230
     * @param string $path The path of the category to return
231
     *
232
     * @return array The category
233
     */
234
    protected function getCategoryByPath($path)
235
    {
236
        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...
237
    }
238
239
    /**
240
     * Returns the URL key utility instance.
241
     *
242
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
243
     */
244
    protected function getUrlKeyUtil()
245
    {
246
        return $this->urlKeyUtil;
247
    }
248
249
    /**
250
     * Make's the passed URL key unique by adding the next number to the end.
251
     *
252
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject The subject to make the URL key unique for
253
     * @param string                                                    $urlKey  The URL key to make unique
254
     *
255
     * @return string The unique URL key
256
     */
257
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, $urlKey)
258
    {
259
        return $this->getUrlKeyUtil()->makeUnique($subject, $urlKey);
260
    }
261
}
262