Completed
Pull Request — master (#79)
by Tim
02:50
created

UrlRewriteUpdateObserver::deleteUrlRewrite()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Product\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
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Product\Observers;
22
23
use TechDivision\Import\Product\Utils\MemberNames;
24
use TechDivision\Import\Product\Utils\CoreConfigDataKeys;
25
26
/**
27
 * Observer that creates/updates the product's URL rewrites.
28
 *
29
 * @author    Tim Wagner <[email protected]>
30
 * @copyright 2016 TechDivision GmbH <[email protected]>
31
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
32
 * @link      https://github.com/techdivision/import-product
33
 * @link      http://www.techdivision.com
34
 */
35
class UrlRewriteUpdateObserver extends UrlRewriteObserver
36
{
37
38
    /**
39
     * Array with the existing URL rewrites of the actual product.
40
     *
41
     * @var array
42
     */
43
    protected $existingUrlRewrites = array();
44
45
    /**
46
     * Process the observer's business logic.
47
     *
48
     * @return void
49
     * @see \TechDivision\Import\Product\Observers\UrlRewriteObserver::process()
50
     */
51 1
    protected function process()
52
    {
53
54
        // process the new URL rewrites first
55 1
        parent::process();
56
57
        // load the root category
58 1
        $rootCategory = $this->getRootCategory();
59
60
        // create redirect URL rewrites for the existing URL rewrites
61 1
        foreach ($this->existingUrlRewrites as $existingUrlRewrite) {
62
            // if the URL rewrite has been created manually
63 1
            if ((integer) $existingUrlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
64
                // do NOTHING, because someone really WANTED to create THIS redirect
65
                continue;
66
            }
67
68
            // query whether or not 301 redirects have to be created
69 1
            if ($this->getSubject()->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_SAVE_REWRITES_HISTORY, true)) {
70
                // if the URL rewrite already IS a redirect
71 1
                if ((integer) $existingUrlRewrite[MemberNames::REDIRECT_TYPE] !== 0) {
72
                    // do NOT create another redirect or update the actual one
73
                    continue;
74
                }
75
76
                // load the metadata from the existing URL rewrite
77 1
                $metadata = $this->getMetadata($existingUrlRewrite);
78
79
                // initialize the category with the root category
80 1
                $category = $rootCategory;
81
82
                // query whether or not, the existing URL rewrite has been replaced
83 1
                if (isset($this->urlRewrites[$metadata[UrlRewriteObserver::CATEGORY_ID]])) {
84
                    // if yes, load the category of the original one
85
                    $category = $this->getCategory($metadata[UrlRewriteObserver::CATEGORY_ID]);
86
                }
87
88
                // load target path/metadata for the actual category
89 1
                $targetPath = $this->prepareRequestPath($category);
90 1
                $metadata = serialize($this->prepareMetadata($category));
91
92
                // override data with the 301 configuration
93
                $attr = array(
94 1
                    MemberNames::IS_AUTOGENERATED => 0,
95 1
                    MemberNames::REDIRECT_TYPE    => 301,
96 1
                    MemberNames::METADATA         => $metadata,
97 1
                    MemberNames::TARGET_PATH      => $targetPath,
98
                );
99
100
                // merge and return the prepared URL rewrite
101 1
                $existingUrlRewrite = $this->mergeEntity($existingUrlRewrite, $attr);
102
103
                // create the URL rewrite
104 1
                $this->persistUrlRewrite($existingUrlRewrite);
105
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
106
            } else {
107
                // delete the existing URL rewrite
108 1
                $this->deleteUrlRewrite($existingUrlRewrite);
109
            }
110
        }
111 1
    }
112
113
    /**
114
     * Return's the URL rewrite for the passed request path.
115
     *
116
     * @param string $requestPath The request path to return the URL rewrite for
117
     *
118
     * @return array|null The URL rewrite
119
     */
120
    protected function getExistingUrlRewrite($requestPath)
121
    {
122
        if (isset($this->existingUrlRewrites[$requestPath])) {
123
            return $this->existingUrlRewrites[$requestPath];
124
        }
125
    }
126
127
    /**
128
     * Remove's the passed URL rewrite from the existing one's.
129
     *
130
     * @param array $urlRewrite The URL rewrite to remove
131
     *
132
     * @return void
133
     */
134 1
    protected function removeExistingUrlRewrite(array $urlRewrite)
135
    {
136
137
        // load request path
138 1
        $requestPath = $urlRewrite[MemberNames::REQUEST_PATH];
139
140
        // query whether or not the URL rewrite exists and remove it, if available
141 1
        if (isset($this->existingUrlRewrites[$requestPath])) {
142 1
            unset($this->existingUrlRewrites[$requestPath]);
143
        }
144 1
    }
145
146
    /**
147
     * Prepare's the URL rewrites that has to be created/updated.
148
     *
149
     * @return void
150
     * @see \TechDivision\Import\Product\Observers\UrlRewriteObserver::prepareUrlRewrites()
151
     */
152 1
    protected function prepareUrlRewrites()
153
    {
154
155
        // (re-)initialize the array for the existing URL rewrites
156 1
        $this->existingUrlRewrites = array();
157
158
        // prepare the new URL rewrites first
159 1
        parent::prepareUrlRewrites();
160
161
        // load the existing URL rewrites of the actual entity
162 1
        $existingUrlRewrites = $this->getUrlRewritesByEntityTypeAndEntityIdAndStoreId(
163 1
            UrlRewriteObserver::ENTITY_TYPE,
164 1
            $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\Observers\EntitySubjectImpl, 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...
165 1
            $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...
166
        );
167
168
        // prepare the existing URL rewrites to improve searching them by request path
169 1
        foreach ($existingUrlRewrites as $existingUrlRewrite) {
170 1
            $this->existingUrlRewrites[$existingUrlRewrite[MemberNames::REQUEST_PATH]] = $existingUrlRewrite;
171
        }
172 1
    }
173
174
    /**
175
     * Initialize the category product with the passed attributes and returns an instance.
176
     *
177
     * @param array $attr The category product attributes
178
     *
179
     * @return array The initialized category product
180
     */
181 1
    protected function initializeUrlRewrite(array $attr)
182
    {
183
184
        // load the category ID of the passed URL rewrite entity
185 1
        $categoryId = $this->getCategoryIdFromMetadata($attr);
186
187
        // iterate over the availabel URL rewrites to find the one that matches the category ID
188 1
        foreach ($this->existingUrlRewrites as $urlRewrite) {
189
            // compare the category IDs
190 1
            if ($categoryId === $this->getCategoryIdFromMetadata($urlRewrite)) {
191
                // if a URL rewrite has been found, do NOT create a redirect
192 1
                $this->removeExistingUrlRewrite($urlRewrite);
193
194
                // if the found URL rewrite has been created manually
195 1
                if ((integer) $urlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
196
                    // do NOT update it nor create a another redirect
197
                    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...
198
                }
199
200
                // if the found URL rewrite has been autogenerated, then update it
201 1
                return $this->mergeEntity($urlRewrite, $attr);
202
            }
203
        }
204
205
        // simple return the attributes
206 1
        return $attr;
207
    }
208
209
    /**
210
     * Extracts the category ID of the passed URL rewrite entity, if available, and return's it.
211
     *
212
     * @param array $attr The URL rewrite entity to extract and return the category ID for
213
     *
214
     * @return integer|null The category ID if available, else NULL
215
     */
216 1
    protected function getCategoryIdFromMetadata(array $attr)
217
    {
218
219
        // load the metadata of the passed URL rewrite entity
220 1
        $metadata = $this->getMetadata($attr);
221
222
        // return the category ID from the metadata
223 1
        return $metadata[UrlRewriteObserver::CATEGORY_ID];
224
    }
225
226
    /**
227
     * Initialize the URL rewrite product => category relation with the passed attributes
228
     * and returns an instance.
229
     *
230
     * @param array $attr The URL rewrite product => category relation attributes
231
     *
232
     * @return array|null The initialized URL rewrite product => category relation
233
     */
234 1
    protected function initializeUrlRewriteProductCategory($attr)
235
    {
236
237
        // try to load the URL rewrite product category relation
238 1
        if ($urlRewriteProductCategory = $this->loadUrlRewriteProductCategory($attr[MemberNames::URL_REWRITE_ID])) {
239
            return $this->mergeEntity($urlRewriteProductCategory, $attr);
240
        }
241
242
        // simple return the URL rewrite product category
243 1
        return $attr;
244
    }
245
246
    /**
247
     * Return's the unserialized metadata of the passed URL rewrite. If the
248
     * metadata doesn't contain a category ID, the category ID of the root
249
     * category will be added.
250
     *
251
     * @param array $urlRewrite The URL rewrite to return the metadata for
252
     *
253
     * @return array The metadata of the passed URL rewrite
254
     */
255 1
    protected function getMetadata($urlRewrite)
256
    {
257
258
        // initialize the array with the metaddata
259 1
        $metadata = array();
260
261
        // try to unserialize the metadata from the passed URL rewrite
262 1
        if (isset($urlRewrite[MemberNames::METADATA])) {
263 1
            $metadata = unserialize($urlRewrite[MemberNames::METADATA]);
264
        }
265
266
        // query whether or not a category ID has been found
267 1
        if (isset($metadata[UrlRewriteObserver::CATEGORY_ID])) {
268
            // if yes, return the metadata
269 1
            return $metadata;
270
        }
271
272
        // if not, append the ID of the root category
273 1
        $rootCategory = $this->getRootCategory();
274 1
        $metadata[UrlRewriteObserver::CATEGORY_ID] = $rootCategory[MemberNames::ENTITY_ID];
275
276
        // and return the metadata
277 1
        return $metadata;
278
    }
279
280
    /**
281
     * Return's the category with the passed ID.
282
     *
283
     * @param integer $categoryId The ID of the category to return
284
     *
285
     * @return array The category data
286
     */
287 1
    protected function getCategory($categoryId)
288
    {
289 1
        return $this->getSubject()->getCategory($categoryId);
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.

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...
290
    }
291
292
    /**
293
     * Return's the URL rewrites for the passed URL entity type and ID.
294
     *
295
     * @param string  $entityType The entity type to load the URL rewrites for
296
     * @param integer $entityId   The entity ID to load the URL rewrites for
297
     * @param integer $storeId    The store ID to load the URL rewrites for
298
     *
299
     * @return array The URL rewrites
300
     */
301 1
    public function getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId)
302
    {
303 1
        return $this->getProductBunchProcessor()->getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId);
304
    }
305
306
    /**
307
     * Return's the URL rewrite product category relation for the passed
308
     * URL rewrite ID.
309
     *
310
     * @param integer $urlRewriteId The URL rewrite ID to load the URL rewrite product category relation for
311
     *
312
     * @return array|false The URL rewrite product category relations
313
     */
314 1
    protected function loadUrlRewriteProductCategory($urlRewriteId)
315
    {
316 1
        return $this->getProductBunchProcessor()->loadUrlRewriteProductCategory($urlRewriteId);
317
    }
318
319
    /**
320
     * Delete's the URL rewrite with the passed attributes.
321
     *
322
     * @param array       $row  The attributes of the entity to delete
323
     * @param string|null $name The name of the prepared statement that has to be executed
324
     *
325
     * @return void
326
     */
327
    protected function deleteUrlRewrite($row, $name = null)
328
    {
329
        $this->getProductBunchProcessor()->removeUrlRewrite($row, $name);
0 ignored issues
show
Bug introduced by
The method removeUrlRewrite() does not seem to exist on object<TechDivision\Impo...unchProcessorInterface>.

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...
330
    }
331
}
332