Completed
Push — master ( 300272...727596 )
by Tim
01:51
created

UrlKeyUtil::makeUnique()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 36
c 0
b 0
f 0
ccs 10
cts 10
cp 1
rs 8.7217
cc 6
nc 6
nop 4
crap 6
1
<?php
2
3
/**
4
 * TechDivision\Import\Utils\UrlKeyUtil
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 2021 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
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Utils;
22
23
use TechDivision\Import\Loaders\LoaderInterface;
24
use TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface;
25
use TechDivision\Import\Services\UrlKeyAwareProcessorInterface;
26
27
/**
28
 * Utility class that provides functionality to make URL keys unique.
29
 *
30
 * @author    Tim Wagner <[email protected]>
31
 * @copyright 2021 TechDivision GmbH <[email protected]>
32
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
33
 * @link      https://github.com/techdivision/import
34
 * @link      http://www.techdivision.com
35
 */
36
class UrlKeyUtil implements UrlKeyUtilInterface
37
{
38
39
    /**
40
     * The URL key aware processor instance.
41
     *
42
     * \TechDivision\Import\Services\UrlKeyAwareProcessorInterface
43
     */
44
    protected $urlKeyAwareProcessor;
45
46
    /**
47
     * The array with the entity type and store view specific suffixes.
48
     *
49
     * @var array
50
     */
51
    protected $suffixes = array();
52
53
    /**
54
     * The URL rewrite entity type to use.
55
     *
56
     * @var \TechDivision\Import\Utils\EnumInterface
57
     */
58
    protected $urlRewriteEntityType;
59
60
    /**
61
     * The array with the entity type code > configuration key mapping.
62
     *
63
     * @var array
64
     */
65
    protected $entityTypeCodeToConfigKeyMapping = array(
66
        EntityTypeCodes::CATALOG_PRODUCT  => CoreConfigDataKeys::CATALOG_SEO_PRODUCT_URL_SUFFIX,
67
        EntityTypeCodes::CATALOG_CATEGORY => CoreConfigDataKeys::CATALOG_SEO_CATEGORY_URL_SUFFIX
68
    );
69
70
    /**
71
     * Construct a new instance.
72
     *
73
     * @param \TechDivision\Import\Services\UrlKeyAwareProcessorInterface $urlKeyAwareProcessor The URL key aware processor instance
74
     * @param \TechDivision\Import\Loaders\LoaderInterface                $coreConfigDataLoader The core config data loader instance
75
     * @param \TechDivision\Import\Loaders\LoaderInterface                $storeIdLoader        The core config data loader instance
76
     * @param \TechDivision\Import\Utils\EnumInterface                    $urlRewriteEntityType The URL rewrite entity type to use
77
     */
78 11
    public function __construct(
79
        UrlKeyAwareProcessorInterface $urlKeyAwareProcessor,
80
        LoaderInterface $coreConfigDataLoader,
81
        LoaderInterface $storeIdLoader,
82
        EnumInterface $urlRewriteEntityType
83
    ) {
84
85
        // initialize the URL kew aware processor instance
86 11
        $this->urlKeyAwareProcessor = $urlKeyAwareProcessor;
87 11
        $this->urlRewriteEntityType = $urlRewriteEntityType;
88
89
        // load the available stores
90 11
        $storeIds = $storeIdLoader->load();
91
92
        // initialize the URL suffixs from the Magento core configuration
93 11
        foreach ($storeIds as $storeId) {
94
            // prepare the array with the entity type and store ID specific suffixes
95 11
            foreach ($this->entityTypeCodeToConfigKeyMapping as $entityTypeCode => $configKey) {
96
                // load the suffix for the given entity type => configuration key and store ID
97 11
                $suffix = $coreConfigDataLoader->load($configKey, '.html', ScopeKeys::SCOPE_DEFAULT, $storeId);
0 ignored issues
show
Unused Code introduced by
The call to LoaderInterface::load() has too many arguments starting with $configKey.

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...
98
                // register the suffux in the array
99 11
                $this->suffixes[$entityTypeCode][$storeId] = $suffix;
100
            }
101
        }
102 11
    }
103
104
    /**
105
     * Returns the URL key aware processor instance.
106
     *
107
     * @return \TechDivision\Import\Services\UrlKeyAwareProcessorInterface The processor instance
108
     */
109 9
    protected function getUrlKeyAwareProcessor()
110
    {
111 9
        return $this->urlKeyAwareProcessor;
112
    }
113
114
    /**
115
     * Load's and return's the URL rewrite for the given request path and store ID
116
     *
117
     * @param string $requestPath The request path to load the URL rewrite for
118
     * @param int    $storeId     The store ID to load the URL rewrite for
119
     *
120
     * @return string|null The URL rewrite found for the given request path and store ID
121
     */
122
    protected function loadUrlRewriteByRequestPathAndStoreId(string $requestPath, int $storeId)
123
    {
124
        return $this->getUrlKeyAwareProcessor()->loadUrlRewriteByRequestPathAndStoreId($requestPath, $storeId);
125
    }
126
127
    /**
128
     * Make's the passed URL key unique by adding/raising a number to the end.
129
     *
130
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject The subject to make the URL key unique for
131
     * @param array                                                     $entity  The entity to make the URL key unique for
132
     * @param string                                                    $urlKey  The URL key to make unique
133
     * @param string|null                                               $urlPath The URL path to make unique (only used for categories)
134
     *
135
     * @return string The unique URL key
136
     */
137 11
    protected function doMakeUnique(UrlKeyAwareSubjectInterface $subject, array $entity, string $urlKey, string $urlPath = null) : string
138
    {
139
140
        // initialize the store view ID, use the default store view if no store view has
141
        // been set, because the default url_key value has been set in default store view
142 11
        $storeId = (int) $subject->getRowStoreId();
143 11
        $entityTypeCode = $subject->getEntityTypeCode();
144
145
        // initialize entity ID + type from the passed entity
146 11
        $entityId = (int) $entity[MemberNames::ENTITY_ID];
147 11
        $entityType = (string) $this->urlRewriteEntityType;
148
149
        // initialize the counter
150 11
        $counter = 0;
151
152
        // initialize the counters
153 11
        $matchingCounters = array();
154 11
        $notMatchingCounters = array();
155
156
        // pre-initialze the URL by concatenating path and/or key to query for
157 11
        $url = $urlPath ? sprintf('%s/%s', $urlPath, $urlKey) : $urlKey;
158
159
        do {
160
            // prepare the request path to load an existing URL rewrite
161 11
            $requestPath = sprintf('%s%s', $url, $this->suffixes[$entityTypeCode][$storeId]);
162
            // try to load an existing URL rewrite
163 11
            $urlRewrite = $this->loadUrlRewriteByRequestPathAndStoreId($requestPath, $storeId);
164
165
            // query whether or not an entity with the given
166
            // request path and store ID is available
167 11
            if ($urlRewrite) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlRewrite 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...
168
                // if yes, query if this IS the URL key of the passed entity
169 8
                if (((int) $urlRewrite[MemberNames::ENTITY_ID]   === $entityId) &&
170 8
                    ((int) $urlRewrite[MemberNames::STORE_ID]    === $storeId) &&
171 8
                           $urlRewrite[MemberNames::ENTITY_TYPE] === $entityType
172
                ) {
173
                    // add the matching counter
174 8
                    $matchingCounters[] = $counter;
175
                    // stop further processing here, because we've a matching
176
                    // URL key and that's all we want for the moment
177 8
                    break;
178
                } else {
179 5
                    $notMatchingCounters[] = $counter;
180
                }
181
182
                // prepare the next URL key to query for
183 5
                $url = sprintf('%s-%d', $urlKey, ++$counter);
184
            } else {
185
                // we've temporary persist a dummy URL rewrite to keep track of the new URL key, e. g. for
186
                // the case the import contains another product or category that wants to use the same one
187 9
                $this->getUrlKeyAwareProcessor()->persistUrlRewrite(
188
                    array(
189 9
                        MemberNames::URL_REWRITE_ID => md5(sprintf('%d-%s', $storeId, $requestPath)),
190 9
                        MemberNames::REDIRECT_TYPE  => 0,
191 9
                        MemberNames::STORE_ID       => $storeId,
192 9
                        MemberNames::ENTITY_ID      => $entityId,
193 9
                        MemberNames::REQUEST_PATH   => $requestPath,
194 9
                        MemberNames::ENTITY_TYPE    => $entityType,
195
                        EntityStatus::MEMBER_NAME   => EntityStatus::STATUS_CREATE
196
                    )
197
                );
198
            }
199 9
        } while ($urlRewrite);
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlRewrite 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...
200
201
        // sort the array ascending according to the counter
202 11
        asort($matchingCounters);
203 11
        asort($notMatchingCounters);
204
205
        // this IS the URL key of the passed entity => we've an UPDATE
206 11
        if (sizeof($matchingCounters) > 0) {
207
            // load highest counter
208 8
            $counter = end($matchingCounters);
209
            // if the counter is > 0, we've to append it to the new URL key
210 8
            if ($counter > 0) {
211 8
                $urlKey = sprintf('%s-%d', $urlKey, $counter);
212
            }
213 9
        } elseif (sizeof($notMatchingCounters) > 0) {
214
            // load the last entry that contains the
215
            // the last NOT matching counter
216 5
            $newCounter = end($notMatchingCounters);
217
            // create a new URL key by raising the counter
218 5
            $urlKey = sprintf('%s-%d', $urlKey, ++$newCounter);
219
        }
220
221
        // return the passed URL key, if NOT
222 11
        return $urlKey;
223
    }
224
225
    /**
226
     * Make's the passed URL key unique by adding the next number to the end.
227
     *
228
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject  The subject to make the URL key unique for
229
     * @param array                                                     $entity   The entity to make the URL key unique for
230
     * @param string                                                    $urlKey   The URL key to make unique
231
     * @param array                                                     $urlPaths The URL paths to make unique
232
     *
233
     * @return string The unique URL key
234
     */
235 11
    public function makeUnique(UrlKeyAwareSubjectInterface $subject, array $entity, string $urlKey, array $urlPaths = array()) : string
236
    {
237
238
        // in general, we want to start at -1, because if NO URL paths has been given
239
        // e. g. we've a product or a root category, we want to make sure that we've
240
        // no URL collisions.
241 11
        $i = -1;
242
243
        // only in case we've a category AND URL paths have been given, we start at 0,
244
        // because the we always want to make sure that also the URL path will be taken
245
        // into account when we make the URL key unique.
246 11
        if ($this->urlRewriteEntityType->equals(UrlRewriteEntityType::CATEGORY) && sizeof($urlPaths) > 0) {
247 1
            $i = 0;
248
        }
249
250
        // iterate over the passed URL paths
251
        // and try to find a unique URL key
252 11
        for ($i; $i < sizeof($urlPaths); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function sizeof() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
253
            // try to make the URL key unique for the given URL path
254 11
            $proposedUrlKey = $this->doMakeUnique($subject, $entity, $urlKey, isset($urlPaths[$i]) ? $urlPaths[$i] : null);
255
256
            // if the URL key is NOT the same as the passed one or with the parent URL path
257
            // it can NOT be used, so we've to persist it temporarily and try it again for
258
            // all the other URL paths until we found one that works with every URL path
259 11
            if ($urlKey !== $proposedUrlKey) {
260
                // temporarily persist the URL key
261 5
                $urlKey = $proposedUrlKey;
262
                // reset the counter and restart the
263
                // iteration with the first URL path
264 5
                $i = -2;
265
            }
266
        }
267
268
        // return the unique URL key
269 11
        return $urlKey;
270
    }
271
272
    /**
273
     * Load the url_key if exists
274
     *
275
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject      The subject to make the URL key unique for
276
     * @param int                                                       $primaryKeyId The ID from category or product
277
     *
278
     * @return string|null The URL key
279
     */
280
    public function loadUrlKey(UrlKeyAwareSubjectInterface $subject, $primaryKeyId)
281
    {
282
283
        // initialize the entity type ID
284
        $entityType = $subject->getEntityType();
0 ignored issues
show
Bug introduced by
The method getEntityType() does not exist on TechDivision\Import\Subj...eyAwareSubjectInterface. Did you maybe mean getEntityTypeCode()?

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...
285
        $entityTypeId = (integer) $entityType[MemberNames::ENTITY_TYPE_ID];
286
287
        // initialize the store view ID, use the admin store view if no store view has
288
        // been set, because the default url_key value has been set in admin store view
289
        $storeId = $subject->getRowStoreId(StoreViewCodes::ADMIN);
290
291
        // try to load the attribute
292
        $attribute = $this->getUrlKeyAwareProcessor()
293
            ->loadVarcharAttributeByAttributeCodeAndEntityTypeIdAndStoreIdAndPrimaryKey(
294
                MemberNames::URL_KEY,
295
                $entityTypeId,
296
                $storeId,
297
                $primaryKeyId
298
            );
299
300
        // return the attribute value or null, if not available
301
        return $attribute ? $attribute['value'] : null;
302
    }
303
}
304