Completed
Push — master ( a17149...dc458e )
by Tim
01:49
created

UrlRewriteUpdateObserver::process()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 113

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 49.5039

Importance

Changes 0
Metric Value
dl 0
loc 113
ccs 20
cts 50
cp 0.4
rs 5.2933
c 0
b 0
f 0
cc 13
nc 13
nop 0
crap 49.5039

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\UrlRewrite\Observers\UrlRewriteUpdateObserver
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-url-rewrite
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Product\UrlRewrite\Observers;
22
23
use TechDivision\Import\Utils\StoreViewCodes;
24
use TechDivision\Import\Product\Utils\CoreConfigDataKeys;
25
use TechDivision\Import\Product\UrlRewrite\Utils\MemberNames;
26
use TechDivision\Import\Product\UrlRewrite\Utils\ColumnKeys;
27
use TechDivision\Import\Product\UrlRewrite\Utils\ConfigurationKeys;
28
use TechDivision\Import\Product\UrlRewrite\Utils\SqlStatementKeys;
29
30
/**
31
 * Observer that creates/updates the product's URL rewrites.
32
 *
33
 * @author    Tim Wagner <[email protected]>
34
 * @copyright 2016 TechDivision GmbH <[email protected]>
35
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
36
 * @link      https://github.com/techdivision/import-product-url-rewrite
37
 * @link      http://www.techdivision.com
38
 */
39
class UrlRewriteUpdateObserver extends UrlRewriteObserver
40
{
41
42
    /**
43
     * Array with the existing URL rewrites of the actual product.
44
     *
45
     * @var array
46
     */
47
    protected $existingUrlRewrites = array();
48
49
    /**
50
     * Process the observer's business logic.
51
     *
52
     * @return void
53
     * @see \TechDivision\Import\Product\UrlRewrite\Observers\UrlRewriteObserver::process()
54
     */
55 1
    protected function process()
56
    {
57
58
        // process the new URL rewrites first
59 1
        parent::process();
60
61
        // load the root category
62 1
        $rootCategory = $this->getRootCategory();
63
64
        // create redirect URL rewrites for the existing URL rewrites
65 1
        foreach ($this->existingUrlRewrites as $existingUrlRewrite) {
66
            // if the URL rewrite has been created manually
67 1
            if ((integer) $existingUrlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
68
                // do NOTHING, because someone really WANTED to create THIS redirect
69
                continue;
70
            }
71
72
            // query whether or not 301 redirects have to be created, so don't create redirects
73
            // if the product is NOT visible or the rewrite history has been deactivated
74 1
            if ($this->isVisible() && $this->getSubject()->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_SAVE_REWRITES_HISTORY, true)) {
75
                // if the URL rewrite already IS a redirect
76 1
                if ((integer) $existingUrlRewrite[MemberNames::REDIRECT_TYPE] !== 0) {
77
                    // do NOT create another redirect or update the actual one
78
                    continue;
79
                }
80
81
                // initialize the data with the URL rewrites new 301 configuration
82 1
                $attr = array(MemberNames::REDIRECT_TYPE => 301);
83
84
                // initialize the category with the root category
85 1
                $category = $rootCategory;
86
87
                // load the metadata from the existing URL rewrite
88 1
                $metadata = $this->getMetadata($existingUrlRewrite);
89
90
                // query whether or not the URL key of the existing URL rewrite has changed
91 1
                if (is_array($metadata) && isset($metadata[UrlRewriteObserver::CATEGORY_ID])) {
92 1
                    if (isset($this->urlRewrites[$metadata[UrlRewriteObserver::CATEGORY_ID]])) {
93
                        try {
94
                            // if yes, try to load the original category and OVERRIDE the default category
95 1
                            $category = $this->getCategory($metadata[UrlRewriteObserver::CATEGORY_ID], $this->getValue(ColumnKeys::STORE_VIEW_CODE));
96
                        } catch (\Exception $e) {
97
                            // if the old category can NOT be loaded, remove the
98
                            // category ID from the URL rewrites metadata
99
                            $attr[MemberNames::METADATA] = null;
100
101
                            // finally log a warning that the old category is not available ony more
102
                            $this->getSubject()
103
                                 ->getSystemLogger()
104
                                 ->warning(
105
                                     sprintf(
106
                                         'Category with ID "%d" is not longer available for URL rewrite with ID "%d"',
107
                                         $metadata[UrlRewriteObserver::CATEGORY_ID],
108
                                         $existingUrlRewrite[MemberNames::URL_REWRITE_ID]
109
                                     )
110
                                 );
111
                        }
112
                    }
113
                }
114
115
                // load target path/metadata for the actual category
116 1
                $targetPath = $this->prepareRequestPath($category);
117
118
                // skip update of URL rewrite, if resulting new target path EQUALS old request path
119 1
                if ($targetPath === $existingUrlRewrite[MemberNames::REQUEST_PATH]) {
120
                    // finally log a warning that the old category is not available ony more
121
                    $this->getSubject()
122
                         ->getSystemLogger()
123
                         ->warning(
124
                             sprintf(
125
                                 'New target path "%s" eqals request path for URL rewrite with ID "%d"',
126
                                 $existingUrlRewrite[MemberNames::REQUEST_PATH],
127
                                 $existingUrlRewrite[MemberNames::URL_REWRITE_ID]
128
                             )
129
                         );
130
131
                    // stop processing the URL rewrite
132
                    continue;
133
                }
134
135
                // set the target path
136 1
                $attr[MemberNames::TARGET_PATH] = $targetPath;
137
138
                // merge and return the prepared URL rewrite
139 1
                $existingUrlRewrite = $this->mergeEntity($existingUrlRewrite, $attr);
140
141
                // create the URL rewrite
142 1
                $this->persistUrlRewrite($existingUrlRewrite);
143
            } else {
144
                // query whether or not the URL rewrite has to be removed
145
                if ($this->getSubject()->getConfiguration()->hasParam(ConfigurationKeys::CLEAN_UP_URL_REWRITES) &&
146
                    $this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::CLEAN_UP_URL_REWRITES)
147
                ) {
148
                    // delete the existing URL rewrite
149
                    $this->deleteUrlRewrite(
150
                        array(MemberNames::URL_REWRITE_ID => $existingUrlRewrite[MemberNames::URL_REWRITE_ID]),
151
                        SqlStatementKeys::DELETE_URL_REWRITE
152
                    );
153
154
                    // log a message, that old URL rewrites have been cleaned-up
155
                    $this->getSubject()
156
                         ->getSystemLogger()
157
                         ->warning(
158
                             sprintf(
159
                                 'Cleaned-up URL rewrite "%s" for product with SKU "%s"',
160
                                 $existingUrlRewrite[MemberNames::REQUEST_PATH],
161 1
                                 $this->getValue(ColumnKeys::SKU)
162
                             )
163
                         );
164
                }
165
            }
166
        }
167 1
    }
168
169
    /**
170
     * Remove's the passed URL rewrite from the existing one's.
171
     *
172
     * @param array $urlRewrite The URL rewrite to remove
173
     *
174
     * @return void
175
     */
176
    protected function removeExistingUrlRewrite(array $urlRewrite)
177
    {
178
179
        // load request path
180
        $requestPath = $urlRewrite[MemberNames::REQUEST_PATH];
181
182
        // query whether or not the URL rewrite exists and remove it, if available
183
        if (isset($this->existingUrlRewrites[$requestPath])) {
184
            unset($this->existingUrlRewrites[$requestPath]);
185
        }
186
    }
187
188
    /**
189
     * Prepare's the URL rewrites that has to be created/updated.
190
     *
191
     * @return void
192
     * @see \TechDivision\Import\Product\UrlRewrite\Observers\UrlRewriteObserver::prepareUrlRewrites()
193
     */
194 1
    protected function prepareUrlRewrites()
195
    {
196
197
        // (re-)initialize the array for the existing URL rewrites
198 1
        $this->existingUrlRewrites = array();
199
200
        // prepare the new URL rewrites first
201 1
        parent::prepareUrlRewrites();
202
203
        // load the store ID to use
204 1
        $storeId = $this->getSubject()->getRowStoreId();
0 ignored issues
show
Bug introduced by
The method getRowStoreId() does not exist on TechDivision\Import\Subjects\SubjectInterface. Did you maybe mean getRow()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
205
206
        // load the existing URL rewrites of the actual entity
207 1
        $existingUrlRewrites = $this->getUrlRewritesByEntityTypeAndEntityIdAndStoreId(
208 1
            UrlRewriteObserver::ENTITY_TYPE,
209 1
            $this->entityId,
210 1
            $storeId
211
        );
212
213
        // prepare the existing URL rewrites to improve searching them by request path
214 1
        foreach ($existingUrlRewrites as $existingUrlRewrite) {
215 1
            $this->existingUrlRewrites[$existingUrlRewrite[MemberNames::REQUEST_PATH]] = $existingUrlRewrite;
216
        }
217 1
    }
218
219
    /**
220
     * Initialize the category product with the passed attributes and returns an instance.
221
     *
222
     * @param array $attr The category product attributes
223
     *
224
     * @return array The initialized category product
225
     */
226 1
    protected function initializeUrlRewrite(array $attr)
227
    {
228
229
        // load the category ID of the passed URL rewrite entity
230 1
        $categoryId = $this->getCategoryIdFromMetadata($attr);
231
232
        // iterate over the available URL rewrites to find the one that matches the category ID
233 1
        foreach ($this->existingUrlRewrites as $urlRewrite) {
234
            // compare the category IDs AND the request path
235 1
            if ($categoryId === $this->getCategoryIdFromMetadata($urlRewrite) &&
236 1
                $attr[MemberNames::REQUEST_PATH] === $urlRewrite[MemberNames::REQUEST_PATH]
237
            ) {
238
                // if a URL rewrite has been found, do NOT create OR keep an existing redirect
239
                $this->removeExistingUrlRewrite($urlRewrite);
240
241
                // if the found URL rewrite has been created manually
242
                if ((integer) $urlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
243
                    // do NOT update it nor create another redirect
244
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type of the parent method TechDivision\Import\Prod...r::initializeUrlRewrite of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
245
                }
246
247
                // if the found URL rewrite has been autogenerated, then update it
248 1
                return $this->mergeEntity($urlRewrite, $attr);
249
            }
250
        }
251
252
        // simple return the attributes
253 1
        return $attr;
254
    }
255
256
    /**
257
     * Extracts the category ID of the passed URL rewrite entity, if available, and return's it.
258
     *
259
     * @param array $attr The URL rewrite entity to extract and return the category ID for
260
     *
261
     * @return integer|null The category ID if available, else NULL
262
     */
263 1
    protected function getCategoryIdFromMetadata(array $attr)
264
    {
265
266
        // load the metadata of the passed URL rewrite entity
267 1
        $metadata = $this->getMetadata($attr);
268
269
        // return the category ID from the metadata
270 1
        return (integer) $metadata[UrlRewriteObserver::CATEGORY_ID];
271
    }
272
273
    /**
274
     * Initialize the URL rewrite product => category relation with the passed attributes
275
     * and returns an instance.
276
     *
277
     * @param array $attr The URL rewrite product => category relation attributes
278
     *
279
     * @return array|null The initialized URL rewrite product => category relation
280
     */
281 1
    protected function initializeUrlRewriteProductCategory($attr)
282
    {
283
284
        // try to load the URL rewrite product category relation
285 1
        if ($urlRewriteProductCategory = $this->loadUrlRewriteProductCategory($attr[MemberNames::URL_REWRITE_ID])) {
286
            return $this->mergeEntity($urlRewriteProductCategory, $attr);
287
        }
288
289
        // simple return the URL rewrite product category
290 1
        return $attr;
291
    }
292
293
    /**
294
     * Return's the unserialized metadata of the passed URL rewrite. If the
295
     * metadata doesn't contain a category ID, the category ID of the root
296
     * category will be added.
297
     *
298
     * @param array $urlRewrite The URL rewrite to return the metadata for
299
     *
300
     * @return array The metadata of the passed URL rewrite
301
     */
302 1
    protected function getMetadata($urlRewrite)
303
    {
304
305
        // initialize the array with the metaddata
306 1
        $metadata = array();
307
308
        // try to unserialize the metadata from the passed URL rewrite
309 1
        if (isset($urlRewrite[MemberNames::METADATA])) {
310 1
            $metadata = json_decode($urlRewrite[MemberNames::METADATA], true);
311
        }
312
313
        // query whether or not a category ID has been found
314 1
        if (isset($metadata[UrlRewriteObserver::CATEGORY_ID])) {
315
            // if yes, return the metadata
316 1
            return $metadata;
317
        }
318
319
        // if not, append the ID of the root category
320 1
        $rootCategory = $this->getRootCategory();
321 1
        $metadata[UrlRewriteObserver::CATEGORY_ID] = (integer) $rootCategory[MemberNames::ENTITY_ID];
322
323
        // and return the metadata
324 1
        return $metadata;
325
    }
326
327
    /**
328
     * Return's the category with the passed ID.
329
     *
330
     * @param integer $categoryId    The ID of the category to return
331
     * @param string  $storeViewCode The store view code of the category to return, defaults to "admin"
332
     *
333
     * @return array The category data
334
     */
335 1
    protected function getCategory($categoryId, $storeViewCode = StoreViewCodes::ADMIN)
336
    {
337 1
        return $this->getSubject()->getCategory($categoryId, $storeViewCode);
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 getCategory() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Prod...jects\UrlRewriteSubject.

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...
338
    }
339
340
    /**
341
     * Return's the URL rewrites for the passed URL entity type and ID.
342
     *
343
     * @param string  $entityType The entity type to load the URL rewrites for
344
     * @param integer $entityId   The entity ID to load the URL rewrites for
345
     * @param integer $storeId    The store ID to load the URL rewrites for
346
     *
347
     * @return array The URL rewrites
348
     */
349 1
    protected function getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId)
350
    {
351 1
        return $this->getProductUrlRewriteProcessor()->getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId);
352
    }
353
354
    /**
355
     * Return's the URL rewrite product category relation for the passed
356
     * URL rewrite ID.
357
     *
358
     * @param integer $urlRewriteId The URL rewrite ID to load the URL rewrite product category relation for
359
     *
360
     * @return array|false The URL rewrite product category relations
361
     */
362 1
    protected function loadUrlRewriteProductCategory($urlRewriteId)
363
    {
364 1
        return $this->getProductUrlRewriteProcessor()->loadUrlRewriteProductCategory($urlRewriteId);
365
    }
366
367
    /**
368
     * Delete's the URL rewrite with the passed attributes.
369
     *
370
     * @param array       $row  The attributes of the entity to delete
371
     * @param string|null $name The name of the prepared statement that has to be executed
372
     *
373
     * @return void
374
     */
375
    protected function deleteUrlRewrite($row, $name = null)
376
    {
377
        $this->getProductUrlRewriteProcessor()->deleteUrlRewrite($row, $name);
378
    }
379
}
380