Completed
Pull Request — master (#74)
by Tim
03:23
created

UrlRewriteUpdateObserver::prepareUrlRewrites()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 10
cts 10
cp 1
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 9
nc 3
nop 0
crap 3
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
     * Return's the URL rewrite for the passed store ID and request path.
47
     *
48
     * @param integer $storeId     The store ID to return the URL rewrite for
49
     * @param string  $requestPath The request path to return the URL rewrite for
50
     *
51
     * @return array|null The URL rewrite
52
     */
53 1
    protected function getExistingUrlRewrite($storeId, $requestPath)
54
    {
55 1
        if (isset($this->existingUrlRewrites[$storeId][$requestPath])) {
56
            return $this->existingUrlRewrites[$storeId][$requestPath];
57
        }
58 1
    }
59
60
    /**
61
     * Remove's the passed URL rewrite from the existing one's.
62
     *
63
     * @param array $urlRewrite The URL rewrite to remove
64
     *
65
     * @return void
66
     */
67
    protected function removeExistingUrlRewrite(array $urlRewrite)
68
    {
69
70
        // load store ID and request path
71
        $storeId = (integer) $urlRewrite[MemberNames::STORE_ID];
72
        $requestPath = $urlRewrite[MemberNames::REQUEST_PATH];
73
74
        // query whether or not the URL rewrite exists and remove it, if available
75
        if (isset($this->existingUrlRewrites[$storeId][$requestPath])) {
76
            unset($this->existingUrlRewrites[$storeId][$requestPath]);
77
        }
78
    }
79
80
    /**
81
     * Process the observer's business logic.
82
     *
83
     * @return void
84
     * @see \TechDivision\Import\Product\Observers\UrlRewriteObserver::process()
85
     */
86 1
    protected function process()
87
    {
88
89
        // process the new URL rewrites first
90 1
        parent::process();
91
92
        // load the root category
93 1
        $rootCategory = $this->getRootCategory();
94
95
        // create redirect URL rewrites for the existing URL rewrites
96 1
        foreach ($this->existingUrlRewrites as $existingUrlRewrites) {
97 1
            foreach ($existingUrlRewrites as $existingUrlRewrite) {
98
                // if the URL rewrite has been created manually
99 1
                if ((integer) $existingUrlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
100
                    // do NOT create another redirect
101
                    continue;
102
                }
103
104
                // if the URL rewrite already IS a redirect
105 1
                if ((integer) $existingUrlRewrite[MemberNames::REDIRECT_TYPE] !== 0) {
106
                    // do NOT create another redirect
107
                    continue;
108
                }
109
110
                // load the metadata from the existing URL rewrite
111 1
                $metadata = $this->getMetadata($existingUrlRewrite);
112
113
                // initialize the category with the root category
114 1
                $category = $rootCategory;
115
116
                // query whether or not, the existing URL rewrite has been replaced
117 1
                if (isset($this->urlRewrites[$metadata['category_id']])) {
118
                    // if yes, load the category of the original one
119 1
                    $category = $this->getCategory($metadata['category_id']);
120
                }
121
122
                // load target path/metadata for the actual category
123 1
                $targetPath = $this->prepareRequestPath($category);
124 1
                $metadata = serialize($this->prepareMetadata($category));
125
126
                // override data with the 301 configuration
127
                $attr = array(
128 1
                    MemberNames::IS_AUTOGENERATED => 0,
129 1
                    MemberNames::REDIRECT_TYPE    => 301,
130 1
                    MemberNames::METADATA         => $metadata,
131 1
                    MemberNames::TARGET_PATH      => $targetPath,
132
                );
133
134
                // merge and return the prepared URL rewrite
135 1
                $existingUrlRewrite = $this->mergeEntity($existingUrlRewrite, $attr);
136
137
                // create the URL rewrite
138 1
                $this->persistUrlRewrite($existingUrlRewrite);
139
            }
140
        }
141 1
    }
142
143
    /**
144
     * Prepare's the URL rewrites that has to be created/updated.
145
     *
146
     * @return void
147
     * @see \TechDivision\Import\Product\Observers\UrlRewriteObserver::prepareUrlRewrites()
148
     */
149 1
    protected function prepareUrlRewrites()
150
    {
151
152
        // prepare the new URL rewrites first
153 1
        parent::prepareUrlRewrites();
154
155
        // query whether or not 301 redirects have to be created
156 1
        if ($this->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_SAVE_REWRITES_HISTORY, true)) {
0 ignored issues
show
Deprecated Code introduced by
The method TechDivision\Import\Obse...er::getCoreConfigData() has been deprecated with message: Will be removed with version 1.0.0, use subject method instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
157
            // (re-)initialize the array for the existing URL rewrites
158 1
            $this->existingUrlRewrites = array();
159
160
            // load the existing URL rewrites of the actual entity
161 1
            $existingUrlRewrites = $this->getUrlRewritesByEntityTypeAndEntityId(UrlRewriteObserver::ENTITY_TYPE, $this->getLastEntityId());
0 ignored issues
show
Deprecated Code introduced by
The method TechDivision\Import\Obse...rver::getLastEntityId() has been deprecated with message: Will be removed with version 1.0.0, use subject method instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
162
163
            // prepare the existing URL rewrites to improve searching them by store ID/request path
164 1
            foreach ($existingUrlRewrites as $existingUrlRewrite) {
165
                // load store ID and request path from the existing URL rewrite
166 1
                $storeId = (integer) $existingUrlRewrite[MemberNames::STORE_ID];
167 1
                $requestPath = $existingUrlRewrite[MemberNames::REQUEST_PATH];
168
169
                // append the URL rewrite with its store ID/request path
170 1
                $this->existingUrlRewrites[$storeId][$requestPath] = $existingUrlRewrite;
171
            }
172
        }
173 1
    }
174
175
    /**
176
     * Initialize the category product with the passed attributes and returns an instance.
177
     *
178
     * @param array $attr The category product attributes
179
     *
180
     * @return array The initialized category product
181
     */
182 1
    protected function initializeUrlRewrite(array $attr)
183
    {
184
185
        // load store ID and request path
186 1
        $storeId = $attr[MemberNames::STORE_ID];
187 1
        $requestPath = $attr[MemberNames::REQUEST_PATH];
188
189
        // try to load the URL rewrite for the store ID and request path
190 1
        if ($urlRewrite = $this->getExistingUrlRewrite($storeId, $requestPath)) {
191
            // if a URL rewrite has been found, do NOT create a redirect
192
            $this->removeExistingUrlRewrite($urlRewrite);
193
194
            // if the found URL rewrite has been created manually
195
            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
            return $this->mergeEntity($urlRewrite, $attr);
202
        }
203
204
        // simple return the attributes
205 1
        return $attr;
206
    }
207
208
    /**
209
     * Initialize the URL rewrite product => category relation with the passed attributes
210
     * and returns an instance.
211
     *
212
     * @param array $attr The URL rewrite product => category relation attributes
213
     *
214
     * @return array|null The initialized URL rewrite product => category relation
215
     */
216 1
    protected function initializeUrlRewriteProductCategory($attr)
217
    {
218
219
        // initialize product and category ID
220 1
        $productId = $attr[MemberNames::PRODUCT_ID];
221 1
        $categoryId = $attr[MemberNames::CATEGORY_ID];
222
223
        // try to load the URL rewrite product category relation for the product/category ID
224 1
        if ($urlRewriteProductCategory = $this->loadUrlRewriteProductCategory($productId, $categoryId)) {
225
            return $this->mergeEntity($urlRewriteProductCategory, $attr);
226
        }
227
228
        // simple return the URL rewrite product category
229 1
        return $attr;
230
    }
231
232
    /**
233
     * Return's the unserialized metadata of the passed URL rewrite. If the
234
     * metadata doesn't contain a category ID, the category ID of the root
235
     * category will be added.
236
     *
237
     * @param array $urlRewrite The URL rewrite to return the metadata for
238
     *
239
     * @return array The metadata of the passed URL rewrite
240
     */
241 1
    protected function getMetadata($urlRewrite)
242
    {
243
244
        // initialize the array with the metaddata
245 1
        $metadata = array();
246
247
        // try to unserialize the metadata from the passed URL rewrite
248 1
        if (isset($urlRewrite[MemberNames::METADATA])) {
249 1
            $metadata = unserialize($urlRewrite[MemberNames::METADATA]);
250
        }
251
252
        // query whether or not a category ID has been found
253 1
        if (isset($metadata['category_id'])) {
254
            // if yes, return the metadata
255 1
            return $metadata;
256
        }
257
258
        // if not, append the ID of the root category
259 1
        $rootCategory = $this->getRootCategory();
260 1
        $metadata['category_id'] = $rootCategory[MemberNames::ENTITY_ID];
261
262
        // and return the metadata
263 1
        return $metadata;
264
    }
265
266
    /**
267
     * Return's the category with the passed ID.
268
     *
269
     * @param integer $categoryId The ID of the category to return
270
     *
271
     * @return array The category data
272
     */
273 1
    protected function getCategory($categoryId)
274
    {
275 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...
276
    }
277
278
    /**
279
     * Return's the URL rewrites for the passed URL entity type and ID.
280
     *
281
     * @param string  $entityType The entity type to load the URL rewrites for
282
     * @param integer $entityId   The entity ID to laod the rewrites for
283
     *
284
     * @return array The URL rewrites
285
     */
286 1
    protected function getUrlRewritesByEntityTypeAndEntityId($entityType, $entityId)
287
    {
288 1
        return $this->getProductBunchProcessor()->getUrlRewritesByEntityTypeAndEntityId($entityType, $entityId);
289
    }
290
291
    /**
292
     * Return's the URL rewrite product category relation for the passed
293
     * product and category ID.
294
     *
295
     * @param integer $productId  The product ID to load the URL rewrite product category relation for
296
     * @param integer $categoryId The category ID to load the URL rewrite product category relation for
297
     *
298
     * @return array|false The URL rewrite product category relations
299
     */
300 1
    protected function loadUrlRewriteProductCategory($productId, $categoryId)
301
    {
302 1
        return $this->getProductBunchProcessor()->loadUrlRewriteProductCategory($productId, $categoryId);
303
    }
304
}
305