Completed
Push — master ( 23ad32...40a934 )
by Tim
01:28
created

UrlRewriteUpdateObserver::prepareUrlRewrites()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 11
cts 11
cp 1
rs 9.536
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
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 of the default store view (as we're in the
62
        // default row and does not have a store view code), because we need
63
        // that to handle the default product URL rewrite
64 1
        $rootCategory = $this->getRootCategory();
65
66
        // create redirect URL rewrites for the existing URL rewrites
67 1
        foreach ($this->existingUrlRewrites as $existingUrlRewrite) {
68
            // query whether or not 301 redirects have to be created, so don't create redirects
69
            // if the product is NOT visible or the rewrite history has been deactivated
70 1
            if ($this->isVisible() && $this->getSubject()->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_SAVE_REWRITES_HISTORY, true)) {
71
                // initialize the data with the URL rewrites new 301 configuration
72 1
                $attr = array(MemberNames::REDIRECT_TYPE => 301);
73
74
                // initialize the category with the root category
75 1
                $category = $rootCategory;
76
77
                // load the metadata from the existing URL rewrite
78 1
                $metadata = $this->getMetadata($existingUrlRewrite);
79
80
                // query whether or not the URL key of the existing URL rewrite has changed
81 1
                if (is_array($metadata) && isset($metadata[UrlRewriteObserver::CATEGORY_ID])) {
82 1
                    if (isset($this->urlRewrites[$metadata[UrlRewriteObserver::CATEGORY_ID]])) {
83
                        try {
84
                            // if yes, try to load the original category and OVERRIDE the default category
85 1
                            $category = $this->getCategory($metadata[UrlRewriteObserver::CATEGORY_ID], $this->getValue(ColumnKeys::STORE_VIEW_CODE));
86
                        } catch (\Exception $e) {
87
                            // if the old category can NOT be loaded, remove the
88
                            // category ID from the URL rewrites metadata
89
                            $attr[MemberNames::METADATA] = null;
90
91
                            // finally log a warning that the old category is not available ony more
92
                            $this->getSubject()
93
                                 ->getSystemLogger()
94
                                 ->warning(
95
                                     sprintf(
96
                                         'Category with ID "%d" is not longer available for URL rewrite with ID "%d"',
97
                                         $metadata[UrlRewriteObserver::CATEGORY_ID],
98
                                         $existingUrlRewrite[MemberNames::URL_REWRITE_ID]
99
                                     )
100
                                 );
101
                        }
102
                    }
103
                }
104
105
                // load target path/metadata for the actual category
106 1
                $targetPath = $this->prepareRequestPath($category);
107
108
                // skip update of URL rewrite, if nothing to change
109 1
                if ($targetPath === $existingUrlRewrite[MemberNames::TARGET_PATH] &&
110 1
                    301 === (int)$existingUrlRewrite[MemberNames::REDIRECT_TYPE]) {
111
                    // stop processing the URL rewrite
112
                    continue;
113
                }
114
115
                // skip update of URL rewrite, if resulting new target path EQUALS old request path
116 1
                if ($targetPath === $existingUrlRewrite[MemberNames::REQUEST_PATH]) {
117
                    // finally log a warning that the old category is not available ony more
118
                    $this->getSubject()
119
                         ->getSystemLogger()
120
                         ->warning(
121
                             sprintf(
122
                                 'New target path "%s" eqals request path for URL rewrite with ID "%d"',
123
                                 $existingUrlRewrite[MemberNames::REQUEST_PATH],
124
                                 $existingUrlRewrite[MemberNames::URL_REWRITE_ID]
125
                             )
126
                         );
127
128
                    // stop processing the URL rewrite
129
                    continue;
130
                }
131
132
                // set the target path
133 1
                $attr[MemberNames::TARGET_PATH] = $targetPath;
134
135
                // merge and return the prepared URL rewrite
136 1
                $existingUrlRewrite = $this->mergeEntity($existingUrlRewrite, $attr);
137
138
                // create the URL rewrite
139 1
                $this->persistUrlRewrite($existingUrlRewrite);
140
            } else {
141
                // query whether or not the URL rewrite has to be removed
142
                if ($this->getSubject()->getConfiguration()->hasParam(ConfigurationKeys::CLEAN_UP_URL_REWRITES) &&
143
                    $this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::CLEAN_UP_URL_REWRITES)
144
                ) {
145
                    // delete the existing URL rewrite
146
                    $this->deleteUrlRewrite(
147
                        array(MemberNames::URL_REWRITE_ID => $existingUrlRewrite[MemberNames::URL_REWRITE_ID]),
148
                        SqlStatementKeys::DELETE_URL_REWRITE
149
                    );
150
151
                    // log a message, that old URL rewrites have been cleaned-up
152
                    $this->getSubject()
153
                         ->getSystemLogger()
154
                         ->warning(
155
                             sprintf(
156
                                 'Cleaned-up URL rewrite "%s" for product with SKU "%s"',
157
                                 $existingUrlRewrite[MemberNames::REQUEST_PATH],
158 1
                                 $this->getValue(ColumnKeys::SKU)
159
                             )
160
                         );
161
                }
162
            }
163
        }
164 1
    }
165
166
    /**
167
     * Remove's the passed URL rewrite from the existing one's.
168
     *
169
     * @param array $urlRewrite The URL rewrite to remove
170
     *
171
     * @return void
172
     */
173
    protected function removeExistingUrlRewrite(array $urlRewrite)
174
    {
175
176
        // load request path
177
        $requestPath = $urlRewrite[MemberNames::REQUEST_PATH];
178
179
        // query whether or not the URL rewrite exists and remove it, if available
180
        if (isset($this->existingUrlRewrites[$requestPath])) {
181
            unset($this->existingUrlRewrites[$requestPath]);
182
        }
183
    }
184
185
    /**
186
     * Prepare's the URL rewrites that has to be created/updated.
187
     *
188
     * @return void
189
     * @see \TechDivision\Import\Product\UrlRewrite\Observers\UrlRewriteObserver::prepareUrlRewrites()
190
     */
191 1
    protected function prepareUrlRewrites()
192
    {
193
194
        // (re-)initialize the array for the existing URL rewrites
195 1
        $this->existingUrlRewrites = array();
196
197
        // prepare the new URL rewrites first
198 1
        parent::prepareUrlRewrites();
199
200
        // load the store ID to use
201 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...
202
203
        // load the existing URL rewrites of the actual entity
204 1
        $existingUrlRewrites = $this->getUrlRewritesByEntityTypeAndEntityIdAndStoreId(
205 1
            UrlRewriteObserver::ENTITY_TYPE,
206 1
            $this->entityId,
207 1
            $storeId
208
        );
209
210
        // prepare the existing URL rewrites to improve searching them by request path
211 1
        foreach ($existingUrlRewrites as $existingUrlRewrite) {
212 1
            $this->existingUrlRewrites[$existingUrlRewrite[MemberNames::REQUEST_PATH]] = $existingUrlRewrite;
213
        }
214 1
    }
215
216
    /**
217
     * Initialize the category product with the passed attributes and returns an instance.
218
     *
219
     * @param array $attr The category product attributes
220
     *
221
     * @return array The initialized category product
222
     */
223 1
    protected function initializeUrlRewrite(array $attr)
224
    {
225
226
        // load the category ID of the passed URL rewrite entity
227 1
        $categoryId = $this->getCategoryIdFromMetadata($attr);
228
229
        // iterate over the available URL rewrites to find the one that matches the category ID
230 1
        foreach ($this->existingUrlRewrites as $urlRewrite) {
231
            // compare the category IDs AND the request path
232 1
            if ($categoryId === $this->getCategoryIdFromMetadata($urlRewrite) &&
233 1
                $attr[MemberNames::REQUEST_PATH] === $urlRewrite[MemberNames::REQUEST_PATH]
234
            ) {
235
                // if a URL rewrite has been found, do NOT create OR keep an existing redirect
236
                $this->removeExistingUrlRewrite($urlRewrite);
237
238
                // if the found URL rewrite has been autogenerated, then update it
239 1
                return $this->mergeEntity($urlRewrite, $attr);
240
            }
241
        }
242
243
        // simple return the attributes
244 1
        return $attr;
245
    }
246
247
    /**
248
     * Extracts the category ID of the passed URL rewrite entity, if available, and return's it.
249
     *
250
     * @param array $attr The URL rewrite entity to extract and return the category ID for
251
     *
252
     * @return integer|null The category ID if available, else NULL
253
     */
254 1
    protected function getCategoryIdFromMetadata(array $attr)
255
    {
256
257
        // load the metadata of the passed URL rewrite entity
258 1
        $metadata = $this->getMetadata($attr);
259
260
        // return the category ID from the metadata
261 1
        return (integer) $metadata[UrlRewriteObserver::CATEGORY_ID];
262
    }
263
264
    /**
265
     * Initialize the URL rewrite product => category relation with the passed attributes
266
     * and returns an instance.
267
     *
268
     * @param array $attr The URL rewrite product => category relation attributes
269
     *
270
     * @return array|null The initialized URL rewrite product => category relation
271
     */
272 1
    protected function initializeUrlRewriteProductCategory($attr)
273
    {
274
275
        // try to load the URL rewrite product category relation
276 1
        if ($urlRewriteProductCategory = $this->loadUrlRewriteProductCategory($attr[MemberNames::URL_REWRITE_ID])) {
277
            return $this->mergeEntity($urlRewriteProductCategory, $attr);
278
        }
279
280
        // simple return the URL rewrite product category
281 1
        return $attr;
282
    }
283
284
    /**
285
     * Return's the unserialized metadata of the passed URL rewrite. If the
286
     * metadata doesn't contain a category ID, the category ID of the root
287
     * category will be added.
288
     *
289
     * @param array $urlRewrite The URL rewrite to return the metadata for
290
     *
291
     * @return array The metadata of the passed URL rewrite
292
     */
293 1
    protected function getMetadata($urlRewrite)
294
    {
295
296
        // initialize the array with the metaddata
297 1
        $metadata = array();
298
299
        // try to unserialize the metadata from the passed URL rewrite
300 1
        if (isset($urlRewrite[MemberNames::METADATA])) {
301 1
            $metadata = json_decode($urlRewrite[MemberNames::METADATA], true);
302
        }
303
304
        // query whether or not a category ID has been found
305 1
        if (isset($metadata[UrlRewriteObserver::CATEGORY_ID])) {
306
            // if yes, return the metadata
307 1
            return $metadata;
308
        }
309
310
        // if not, append the ID of the root category
311 1
        $rootCategory = $this->getRootCategory();
312 1
        $metadata[UrlRewriteObserver::CATEGORY_ID] = (integer) $rootCategory[MemberNames::ENTITY_ID];
313
314
        // and return the metadata
315 1
        return $metadata;
316
    }
317
318
    /**
319
     * Return's the category with the passed ID.
320
     *
321
     * @param integer $categoryId    The ID of the category to return
322
     * @param string  $storeViewCode The store view code of the category to return, defaults to "admin"
323
     *
324
     * @return array The category data
325
     */
326 1
    protected function getCategory($categoryId, $storeViewCode = StoreViewCodes::ADMIN)
327
    {
328 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...
329
    }
330
331
    /**
332
     * Return's the URL rewrites for the passed URL entity type and ID.
333
     *
334
     * @param string  $entityType The entity type to load the URL rewrites for
335
     * @param integer $entityId   The entity ID to load the URL rewrites for
336
     * @param integer $storeId    The store ID to load the URL rewrites for
337
     *
338
     * @return array The URL rewrites
339
     */
340 1
    protected function getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId)
341
    {
342 1
        return $this->getProductUrlRewriteProcessor()->getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId);
343
    }
344
345
    /**
346
     * Return's the URL rewrite product category relation for the passed
347
     * URL rewrite ID.
348
     *
349
     * @param integer $urlRewriteId The URL rewrite ID to load the URL rewrite product category relation for
350
     *
351
     * @return array|false The URL rewrite product category relations
352
     */
353 1
    protected function loadUrlRewriteProductCategory($urlRewriteId)
354
    {
355 1
        return $this->getProductUrlRewriteProcessor()->loadUrlRewriteProductCategory($urlRewriteId);
356
    }
357
358
    /**
359
     * Delete's the URL rewrite with the passed attributes.
360
     *
361
     * @param array       $row  The attributes of the entity to delete
362
     * @param string|null $name The name of the prepared statement that has to be executed
363
     *
364
     * @return void
365
     */
366
    protected function deleteUrlRewrite($row, $name = null)
367
    {
368
        $this->getProductUrlRewriteProcessor()->deleteUrlRewrite($row, $name);
369
    }
370
}
371