Completed
Push — master ( 4a09c7...cf5b29 )
by Tim
10s
created

UrlRewriteUpdateObserver::deleteUrlRewrite()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Category\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-category
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Category\Observers;
22
23
use TechDivision\Import\Category\Utils\MemberNames;
24
use TechDivision\Import\Category\Utils\CoreConfigDataKeys;
25
use TechDivision\Import\Category\Utils\ColumnKeys;
26
use TechDivision\Import\Category\Utils\ConfigurationKeys;
27
28
/**
29
 * Observer that creates/updates the category's URL rewrites.
30
 *
31
 * @author    Tim Wagner <[email protected]>
32
 * @copyright 2016 TechDivision GmbH <[email protected]>
33
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
34
 * @link      https://github.com/techdivision/import-category
35
 * @link      http://www.techdivision.com
36
 */
37
class UrlRewriteUpdateObserver extends UrlRewriteObserver
38
{
39
40
    /**
41
     * Array with the existing URL rewrites of the actual category.
42
     *
43
     * @var array
44
     */
45
    protected $existingUrlRewrites = array();
46
47
    /**
48
     * Return's the URL rewrite for the passed request path.
49
     *
50
     * @param string $requestPath The request path to return the URL rewrite for
51
     *
52
     * @return array|null The URL rewrite
53
     */
54
    protected function getExistingUrlRewrite($requestPath)
55
    {
56
        if (isset($this->existingUrlRewrites[$requestPath])) {
57
            return $this->existingUrlRewrites[$requestPath];
58
        }
59
    }
60
61
    /**
62
     * Remove's the passed URL rewrite from the existing one's.
63
     *
64
     * @param array $urlRewrite The URL rewrite to remove
65
     *
66
     * @return void
67
     */
68
    protected function removeExistingUrlRewrite(array $urlRewrite)
69
    {
70
71
        // load store ID and request path
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[$requestPath])) {
76
            unset($this->existingUrlRewrites[$requestPath]);
77
        }
78
    }
79
80
    /**
81
     * Process the observer's business logic.
82
     *
83
     * @return void
84
     */
85
    protected function process()
86
    {
87
88
        // process the new URL rewrites first
89
        parent::process();
90
91
        // create redirect URL rewrites for the existing URL rewrites
92
        foreach ($this->existingUrlRewrites as $existingUrlRewrites) {
93
            foreach ($existingUrlRewrites as $existingUrlRewrite) {
94
                // if the URL rewrite has been created manually
95
                if ((integer) $existingUrlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
96
                    // do NOT create another redirect
97
                    continue;
98
                }
99
100
                // query whether or not 301 redirects have to be created, so don't
101
                // create redirects if the the rewrite history has been deactivated
102
                if ($this->getSubject()->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_CATEGORY_URL_SUFFIX, true)) {
103
                    // if the URL rewrite already IS a redirect
104
                    if ((integer) $existingUrlRewrite[MemberNames::REDIRECT_TYPE] !== 0) {
105
                        // do NOT create another redirect
106
                        continue;
107
                    }
108
109
                    // if yes, load the category of the original one
110
                    $category = $this->getCategory($existingUrlRewrite[MemberNames::ENTITY_ID]);
111
112
                    // load target path/metadata for the actual category
113
                    $targetPath = $this->prepareRequestPath($category);
0 ignored issues
show
Unused Code introduced by
The call to UrlRewriteUpdateObserver::prepareRequestPath() has too many arguments starting with $category.

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...
114
115
                    // override data with the 301 configuration
116
                    $attr = array(
117
                        MemberNames::REDIRECT_TYPE    => 301,
118
                        MemberNames::TARGET_PATH      => $targetPath,
119
                    );
120
121
                    // merge and return the prepared URL rewrite
122
                    $existingUrlRewrite = $this->mergeEntity($existingUrlRewrite, $attr);
123
124
                    // create the URL rewrite
125
                    $this->persistUrlRewrite($existingUrlRewrite);
126
127
                } else {
128
                    // query whether or not the URL rewrite has to be removed
129
                    if ($this->getSubject()->getConfiguration()->hasParam(ConfigurationKeys::CLEAN_UP_URL_REWRITES) &&
130
                        $this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::CLEAN_UP_URL_REWRITES)
131
                    ) {
132
                        // delete the existing URL rewrite
133
                        $this->deleteUrlRewrite($existingUrlRewrite);
134
135
                        // log a message, that old URL rewrites have been cleaned-up
136
                        $this->getSubject()
137
                             ->getSystemLogger()
138
                             ->warning(
139
                                 sprintf(
140
                                     'Cleaned-up %d URL rewrite "%s" for category with path "%s"',
141
                                     $existingUrlRewrite[MemberNames::REQUEST_PATH],
142
                                     $this->getValue(ColumnKeys::PATH)
143
                                 )
144
                             );
145
                    }
146
                }
147
            }
148
        }
149
    }
150
151
    /**
152
     * Prepare's the URL rewrites that has to be created/updated.
153
     *
154
     * @return void
155
     */
156
    protected function prepareUrlRewrites()
157
    {
158
159
        // call the parent method
160
        parent::prepareUrlRewrites();
161
162
        // (re-)initialize the array for the existing URL rewrites
163
        $this->existingUrlRewrites = array();
164
165
        // load primary key and entity type
166
        $pk = $this->getPrimaryKey();
167
        $entityType = UrlRewriteObserver::ENTITY_TYPE;
168
169
        // load the store ID to use
170
        $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...
171
172
        // load the existing URL rewrites of the actual entity
173
        $existingUrlRewrites = $this->getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $pk, $storeId);
174
175
        // prepare the existing URL rewrites to improve searching them by store ID/request path
176
        foreach ($existingUrlRewrites as $existingUrlRewrite) {
177
            // load the request path from the existing URL rewrite
178
            $requestPath = $existingUrlRewrite[MemberNames::REQUEST_PATH];
179
180
            // append the URL rewrite with its store ID/request path
181
            $this->existingUrlRewrites[$requestPath] = $existingUrlRewrite;
182
        }
183
    }
184
185
    /**
186
     * Initialize the URL rewrite with the passed attributes and returns an instance.
187
     *
188
     * @param array $attr The URL rewrite attributes
189
     *
190
     * @return array The initialized URL rewrite
191
     */
192
    protected function initializeUrlRewrite(array $attr)
193
    {
194
195
        // load store ID and request path
196
        $categoryId = $attr[MemberNames::ENTITY_ID];
197
        $requestPath = $attr[MemberNames::REQUEST_PATH];
198
199
        // iterate over the available URL rewrites to find the one that matches the category ID
200
        foreach ($this->existingUrlRewrites as $urlRewrite) {
201
            // compare the category IDs AND the request path
202
            if ($categoryId === $urlRewrite[MemberNames::ENTITY_ID] &&
203
                $requestPath === $urlRewrite[MemberNames::REQUEST_PATH]
204
            ) {
205
                // if a URL rewrite has been found, do NOT create a redirect
206
                $this->removeExistingUrlRewrite($urlRewrite);
207
208
                // if the found URL rewrite has been created manually
209
                if ((integer) $urlRewrite[MemberNames::IS_AUTOGENERATED] === 0) {
210
                    // do NOT update it nor create a another redirect
211
                    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\Cate...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...
212
                }
213
214
                // if the found URL rewrite has been autogenerated, then update it
215
                return $this->mergeEntity($urlRewrite, $attr);
216
            }
217
        }
218
219
        // simple return the attributes
220
        return $attr;
221
    }
222
223
    /**
224
     * Return's the category with the passed ID.
225
     *
226
     * @param integer $categoryId The ID of the category to return
227
     *
228
     * @return array The category data
229
     * @throws \Exception Is thrown, if the category is not available
230
     */
231
    protected function getCategory($categoryId)
232
    {
233
        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\Cate...AbstractCategorySubject, TechDivision\Import\Category\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...
234
    }
235
236
    /**
237
     * Return's the URL rewrites for the passed URL entity type and ID.
238
     *
239
     * @param string  $entityType The entity type to load the URL rewrites for
240
     * @param integer $entityId   The entity ID to load the URL rewrites for
241
     * @param integer $storeId    The store ID to load the URL rewrites for
242
     *
243
     * @return array The URL rewrites
244
     */
245
    protected function getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId)
246
    {
247
        return $this->getCategoryBunchProcessor()->getUrlRewritesByEntityTypeAndEntityIdAndStoreId($entityType, $entityId, $storeId);
248
    }
249
250
    /**
251
     * Delete's the URL rewrite with the passed attributes.
252
     *
253
     * @param array       $row  The attributes of the entity to delete
254
     * @param string|null $name The name of the prepared statement that has to be executed
255
     *
256
     * @return void
257
     */
258
    protected function deleteUrlRewrite($row, $name = null)
259
    {
260
        $this->getCategoryBunchProcessor()->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...
261
    }
262
}
263