Completed
Pull Request — master (#88)
by Tim
03:01
created

UrlKeyObserver::setIds()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 1
nop 1
crap 6
1
<?php
2
3
/**
4
 * TechDivision\Import\Product\Observers\UrlKeyObserver
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 Zend\Filter\FilterInterface;
24
use TechDivision\Import\Utils\StoreViewCodes;
25
use TechDivision\Import\Product\Utils\MemberNames;
26
use TechDivision\Import\Product\Utils\ColumnKeys;
27
use TechDivision\Import\Utils\Filter\UrlKeyFilterTrait;
28
use TechDivision\Import\Product\Services\ProductBunchProcessorInterface;
29
30
/**
31
 * Observer that extracts the URL key from the product name and adds a two new columns
32
 * with the their values.
33
 *
34
 * @author    Tim Wagner <[email protected]>
35
 * @copyright 2016 TechDivision GmbH <[email protected]>
36
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
37
 * @link      https://github.com/techdivision/import-product
38
 * @link      http://www.techdivision.com
39
 */
40
class UrlKeyObserver extends AbstractProductImportObserver
41
{
42
43
    /**
44
     * The trait that provides string => URL key conversion functionality.
45
     *
46
     * @var \TechDivision\Import\Utils\Filter\UrlKeyFilterTrait
47
     */
48
    use UrlKeyFilterTrait;
49
50
    /**
51
     * The product bunch processor instance.
52
     *
53
     * @var \TechDivision\Import\Product\Services\ProductBunchProcessorInterface
54
     */
55
    protected $productBunchProcessor;
56
57
    /**
58
     * Initialize the observer with the passed product bunch processor and filter instance.
59
     *
60
     * @param \TechDivision\Import\Product\Services\ProductBunchProcessorInterface $productBunchProcessor   The product bunch processor instance
61
     * @param \Zend\Filter\FilterInterface                                         $convertLiteralUrlFilter The URL filter instance
62
     */
63
    public function __construct(ProductBunchProcessorInterface $productBunchProcessor, FilterInterface $convertLiteralUrlFilter)
64
    {
65
        $this->productBunchProcessor = $productBunchProcessor;
66
        $this->convertLiteralUrlFilter = $convertLiteralUrlFilter;
67
    }
68
69
    /**
70
     * Return's the product bunch processor instance.
71
     *
72
     * @return \TechDivision\Import\Product\Services\ProductBunchProcessorInterface The product bunch processor instance
73
     */
74
    protected function getProductBunchProcessor()
75
    {
76
        return $this->productBunchProcessor;
77
    }
78
79
    /**
80
     * Process the observer's business logic.
81
     *
82
     * @return void
83
     * @throws \Exception Is thrown, if either column "url_key" or "name" have a value set
84
     */
85
    protected function process()
86
    {
87
88
        // prepare the store view code
89
        $this->getSubject()->prepareStoreViewCode();
90
91
        // set the entity ID for the product with the passed SKU
92
        if ($product = $this->loadProduct($this->getValue(ColumnKeys::SKU))) {
93
            $this->setIds($product);
94
        } else {
95
            $this->setIds(array());
96
        }
97
98
        // query whether or not the URL key column has a value
99 View Code Duplication
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
100
            $this->setValue(ColumnKeys::URL_KEY, $this->makeUrlKeyUnique($this->getValue(ColumnKeys::URL_KEY)));
101
            return;
102
        }
103
104
        // query whether or not a product name is available
105 View Code Duplication
        if ($this->hasValue(ColumnKeys::NAME)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
106
            $this->setValue(ColumnKeys::URL_KEY, $this->makeUrlKeyUnique($this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME))));
107
            return;
108
        }
109
110
        // throw an exception, that the URL key can not be initialized and we're in admin store view
111
        if ($this->getSubject()->getStoreViewCode(StoreViewCodes::ADMIN) === StoreViewCodes::ADMIN) {
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 getStoreViewCode() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Subjects\AbstractEavSubject, TechDivision\Import\Subjects\AbstractSubject, TechDivision\Import\Subjects\MoveFilesSubject.

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...
112
            throw new \Exception('Can\'t initialize the URL key because either columns "url_key" or "name" have a value set for default store view');
113
        }
114
    }
115
116
    /**
117
     * Temporarily persist's the IDs of the passed product.
118
     *
119
     * @param array $product The product to temporarily persist the IDs for
120
     *
121
     * @return void
122
     */
123
    protected function setIds(array $product)
124
    {
125
        $this->setLastEntityId(isset($product[MemberNames::ENTITY_ID]) ? $product[MemberNames::ENTITY_ID] : null);
126
    }
127
128
    /**
129
     * Set's the ID of the product that has been created recently.
130
     *
131
     * @param string $lastEntityId The entity ID
132
     *
133
     * @return void
134
     */
135
    protected function setLastEntityId($lastEntityId)
136
    {
137
        $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\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...
138
    }
139
140
    /**
141
     * Load's and return's the product with the passed SKU.
142
     *
143
     * @param string $sku The SKU of the product to load
144
     *
145
     * @return array The product
146
     */
147
    protected function loadProduct($sku)
148
    {
149
        return $this->getProductBunchProcessor()->loadProduct($sku);
150
    }
151
152
    /**
153
     * Make's the passed URL key unique by adding the next number to the end.
154
     *
155
     * @param string $urlKey The URL key to make unique
156
     *
157
     * @return string The unique URL key
158
     */
159
    protected function makeUrlKeyUnique($urlKey)
160
    {
161
162
        // initialize the entity type ID
163
        $entityType = $this->getEntityType();
164
        $entityTypeId = (integer) $entityType[MemberNames::ENTITY_TYPE_ID];
165
166
        // initialize the store view ID, use the admin store view if no store view has
167
        // been set, because the default url_key value has been set in admin store view
168
        $storeId = $this->getSubject()->getRowStoreId(StoreViewCodes::ADMIN);
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...
169
170
        // initialize the counter
171
        $counter = 0;
172
173
        // initialize the counters
174
        $matchingCounters = array();
175
        $notMatchingCounters = array();
176
177
        // pre-initialze the URL key to query for
178
        $value = $urlKey;
179
180
        do {
181
            // try to load the attribute
182
            $productVarcharAttribute = $this->getProductBunchProcessor()
183
                                            ->loadProductVarcharAttributeByAttributeCodeAndEntityTypeIdAndStoreIdAndValue(
184
                                                MemberNames::URL_KEY,
185
                                                $entityTypeId,
186
                                                $storeId,
187
                                                $value
188
                                            );
189
190
            // try to load the product's URL key
191
            if ($productVarcharAttribute) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $productVarcharAttribute 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...
192
                // this IS the URL key of the passed entity
193
                if ($this->isUrlKeyOf($productVarcharAttribute)) {
194
                    $matchingCounters[] = $counter;
195
                } else {
196
                    $notMatchingCounters[] = $counter;
197
                }
198
199
                // prepare the next URL key to query for
200
                $value = sprintf('%s-%d', $urlKey, ++$counter);
201
            }
202
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
203
        } while ($productVarcharAttribute);
0 ignored issues
show
Bug Best Practice introduced by
The expression $productVarcharAttribute 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...
204
205
        // sort the array ascending according to the counter
206
        asort($matchingCounters);
207
        asort($notMatchingCounters);
208
209
        // this IS the URL key of the passed entity => we've an UPDATE
210
        if (sizeof($matchingCounters) > 0) {
211
            // load highest counter
212
            $counter = end($matchingCounters);
213
            // if the counter is > 0, we've to append it to the new URL key
214
            if ($counter > 0) {
215
                $urlKey = sprintf('%s-%d', $urlKey, $counter);
216
            }
217
        } elseif (sizeof($notMatchingCounters) > 0) {
218
            // create a new URL key by raising the counter
219
            $newCounter = end($notMatchingCounters);
220
            $urlKey = sprintf('%s-%d', $urlKey, ++$newCounter);
221
        }
222
223
        // return the passed URL key, if NOT
224
        return $urlKey;
225
    }
226
227
    /**
228
     * Return's the entity type for the configured entity type code.
229
     *
230
     * @return array The requested entity type
231
     * @throws \Exception Is thrown, if the requested entity type is not available
232
     */
233
    protected function getEntityType()
234
    {
235
        return $this->getSubject()->getEntityType();
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 getEntityType() does only exist in the following implementations of said interface: 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...
236
    }
237
238
    /**
239
     * Return's TRUE, if the passed URL key varchar value IS related with the actual PK.
240
     *
241
     * @param array $productVarcharAttribute The varchar value to check
242
     *
243
     * @return boolean TRUE if the URL key is related, else FALSE
244
     */
245
    protected function isUrlKeyOf(array $productVarcharAttribute)
246
    {
247
        return $this->getSubject()->isUrlKeyOf($productVarcharAttribute);
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 isUrlKeyOf() does only exist in the following implementations of said interface: 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...
248
    }
249
}
250