Completed
Pull Request — master (#74)
by
unknown
03:16 queued 01:39
created

UrlKeyAndPathObserver::makeUnique()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Category\Observers\UrlKeyAndPathObserver
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 2019 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 Zend\Filter\FilterInterface;
24
use TechDivision\Import\Category\Utils\ColumnKeys;
25
use TechDivision\Import\Category\Utils\MemberNames;
26
use TechDivision\Import\Utils\StoreViewCodes;
27
use TechDivision\Import\Utils\UrlKeyUtilInterface;
28
use TechDivision\Import\Utils\Filter\UrlKeyFilterTrait;
29
use TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface;
30
use TechDivision\Import\Category\Services\CategoryBunchProcessorInterface;
31
use TechDivision\Import\Category\Utils\ConfigurationKeys;
32
33
/**
34
 * Observer that extracts the URL key/path from the category path
35
 * and adds them as two new columns with the their values.
36
 *
37
 * @author    Tim Wagner <[email protected]>
38
 * @copyright 2019 TechDivision GmbH <[email protected]>
39
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
40
 * @link      https://github.com/techdivision/import-category
41
 * @link      http://www.techdivision.com
42
 */
43
class UrlKeyAndPathObserver extends AbstractCategoryImportObserver
44
{
45
46
    /**
47
     * The trait that provides string => URL key conversion functionality.
48
     *
49
     * @var \TechDivision\Import\Utils\Filter\UrlKeyFilterTrait
50
     */
51
    use UrlKeyFilterTrait;
52
53
    /**
54
     * The URL key utility instance.
55
     *
56
     * @var \TechDivision\Import\Utils\UrlKeyUtilInterface
57
     */
58
    protected $urlKeyUtil;
59
60
    /**
61
     * The category bunch processor instance.
62
     *
63
     * @var \TechDivision\Import\Category\Services\CategoryBunchProcessorInterface
64
     */
65
    protected $categoryBunchProcessor;
66
67
    /**
68
     * Initialize the observer with the passed product bunch processor instance.
69
     *
70
     * @param \TechDivision\Import\Category\Services\CategoryBunchProcessorInterface $categoryBunchProcessor  The category bunch processor instance
71
     * @param \Zend\Filter\FilterInterface                                           $convertLiteralUrlFilter The URL filter instance
72
     * @param \TechDivision\Import\Utils\UrlKeyUtilInterface                         $urlKeyUtil              The URL key utility instance
73
     */
74
    public function __construct(
75
        CategoryBunchProcessorInterface $categoryBunchProcessor,
76
        FilterInterface $convertLiteralUrlFilter,
77
        UrlKeyUtilInterface $urlKeyUtil
78
    ) {
79
80
        // set the processor and the URL filter instance
81
        $this->categoryBunchProcessor = $categoryBunchProcessor;
82
        $this->convertLiteralUrlFilter = $convertLiteralUrlFilter;
83
        $this->urlKeyUtil = $urlKeyUtil;
84
    }
85
86
    /**
87
     * Process the observer's business logic.
88
     *
89
     * @return void
90
     */
91
    protected function process()
92
    {
93
94
        // initialize the URL key and array for the categories
95
        $category = array();
96
97
        // set the entity ID for the category with the passed path
98
        try {
99
            $this->setIds($category = $this->getCategoryByPath($this->getValue(ColumnKeys::PATH)));
100
        } catch (\Exception $e) {
101
            $this->setIds(array());
102
        }
103
104
        // query whether or not the URL key column has a
105
        // value, if yes, use the value from the column
106
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
107
            $urlKey =  $this->getValue(ColumnKeys::URL_KEY);
108
        } else {
109
            // query whether or not the column `url_key` has a value
110
            if ($category &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $category of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
111
                $this->getSubject()->getConfiguration()->hasParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME) &&
112
                !$this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME, true)
113
            ) {
114
                // product already exists and NO new URL key
115
                // has been specified in column `url_key`, so
116
                // we stop processing here
117
                return;
118
            }
119
            // initialize the URL key with the converted name
120
            $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
121
        }
122
123
        // prepare the store view code
124
        $this->prepareStoreViewCode();
125
126
        // load ID of the actual store view
127
        $storeId = $this->getRowStoreId(StoreViewCodes::ADMIN);
128
129
        // explode the path into the category names
130
        if ($categories = $this->explode($this->getValue(ColumnKeys::PATH), '/')) {
131
            // initialize the array for the category paths
132
            $categoryPaths = array();
133
            // iterate over the parent category names and try
134
            // to load the categories to build the URL path
135
            for ($i = sizeof($categories) - 1; $i > 1; $i--) {
136
                try {
137
                    // prepare the expected category name
138
                    $categoryPath = implode('/', array_slice($categories, 0, $i));
139
                    // load the existing category and prepend the URL key the array with the category URL keys
140
                    $existingCategory = $this->getCategoryByPkAndStoreId($this->mapPath($categoryPath), $storeId);
141
                    // query whether or not an URL key is available or not
142
                    if (isset($existingCategory[MemberNames::URL_KEY])) {
143
                        array_unshift($categoryPaths, $existingCategory[MemberNames::URL_KEY]);
144
                    } else {
145
                        $this->getSystemLogger()->debug(sprintf('Can\'t find URL key for category "%s"', $categoryPath));
146
                    }
147
                } catch (\Exception $e) {
148
                    $this->getSystemLogger()->debug(sprintf('Can\'t load parent category "%s"', $categoryPath));
149
                }
150
            }
151
        }
152
153
        // update the URL key with the unique value
154
        $this->setValue(
155
            ColumnKeys::URL_KEY,
156
            $urlKey = $this->makeUnique($this->getSubject(), $urlKey, implode('/', $categoryPaths))
0 ignored issues
show
Bug introduced by
The variable $categoryPaths does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Documentation introduced by
$this->getSubject() is of type object<TechDivision\Impo...jects\SubjectInterface>, but the function expects a object<TechDivision\Impo...yAwareSubjectInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Unused Code introduced by
The call to UrlKeyAndPathObserver::makeUnique() has too many arguments starting with implode('/', $categoryPaths).

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...
157
        );
158
159
        // finally, append the URL key as last element to the path
160
        array_push($categoryPaths, $urlKey);
161
162
        // create the virtual column for the URL path
163
        if ($this->hasHeader(ColumnKeys::URL_PATH) === false) {
164
            $this->addHeader(ColumnKeys::URL_PATH);
165
        }
166
167
        // set the URL path
168
        $this->setValue(ColumnKeys::URL_PATH, implode('/', $categoryPaths));
169
    }
170
171
    /**
172
     * Return the primary key member name.
173
     *
174
     * @return string The primary key member name
175
     */
176
    protected function getPkMemberName()
177
    {
178
        return MemberNames::ENTITY_ID;
179
    }
180
181
    /**
182
     * Returns the category bunch processor instance.
183
     *
184
     * @return \TechDivision\Import\Category\Services\CategoryBunchProcessorInterface The category bunch processor instance
185
     */
186
    protected function getCategoryBunchProcessor()
187
    {
188
        return $this->categoryBunchProcessor;
189
    }
190
191
    /**
192
     * Returns the URL key utility instance.
193
     *
194
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
195
     */
196
    protected function getUrlKeyUtil()
197
    {
198
        return $this->urlKeyUtil;
199
    }
200
201
    /**
202
     * Return's the category with the passed path.
203
     *
204
     * @param string $path The path of the category to return
205
     *
206
     * @return array The category
207
     * @throws \Exception Is thrown, if the requested category is not available
208
     */
209
    protected function getCategoryByPath($path)
210
    {
211
        return $this->getSubject()->getCategoryByPath($path);
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 getCategoryByPath() does only exist in the following implementations of said interface: TechDivision\Import\Cate...AbstractCategorySubject, TechDivision\Import\Category\Subjects\BunchSubject, TechDivision\Import\Cate...ts\SortableBunchSubject.

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...
212
    }
213
214
    /**
215
     * Returns the category with the passed primary key and the attribute values for the passed store ID.
216
     *
217
     * @param string  $pk      The primary key of the category to return
218
     * @param integer $storeId The store ID of the category values
219
     *
220
     * @return array|null The category data
221
     */
222
    protected function getCategoryByPkAndStoreId($pk, $storeId)
223
    {
224
        return $this->getCategoryBunchProcessor()->getCategoryByPkAndStoreId($pk, $storeId);
225
    }
226
227
    /**
228
     * Temporarily persist's the IDs of the passed category.
229
     *
230
     * @param array $category The category to temporarily persist the IDs for
231
     *
232
     * @return void
233
     */
234
    protected function setIds(array $category)
235
    {
236
        $this->setLastEntityId(isset($category[MemberNames::ENTITY_ID]) ? $category[MemberNames::ENTITY_ID] : null);
237
    }
238
239
    /**
240
     * Set's the ID of the category that has been created recently.
241
     *
242
     * @param string $lastEntityId The entity ID
243
     *
244
     * @return void
245
     */
246
    protected function setLastEntityId($lastEntityId)
247
    {
248
        $this->getSubject()->setLastEntityId($lastEntityId);
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 setLastEntityId() does only exist in the following implementations of said interface: TechDivision\Import\Cate...AbstractCategorySubject, TechDivision\Import\Category\Subjects\BunchSubject, TechDivision\Import\Cate...ts\SortableBunchSubject, TechDivision\Import\Plugins\ExportableSubjectImpl, 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...
249
    }
250
251
    /**
252
     * Make's the passed URL key unique by adding the next number to the end.
253
     *
254
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject The subject to make the URL key unique for
255
     * @param string                                                    $urlKey  The URL key to make unique
256
     *
257
     * @return string The unique URL key
258
     */
259
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, $urlKey)
260
    {
261
        return $this->getUrlKeyUtil()->makeUnique($subject, $urlKey);
262
    }
263
}
264