Completed
Push — 19.x ( 239dba...3864f1 )
by Tim
01:48
created

CatalogAttributeObserver   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 298
Duplicated Lines 7.05 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 86.89%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 3
dl 21
loc 298
ccs 53
cts 61
cp 0.8689
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 13 13 1
A process() 0 11 2
A mergeEntity() 8 8 2
B prepareAttributes() 0 60 9
A serializeAdditionalData() 0 11 3
A loadRawEntity() 0 4 1
A initializeAttribute() 0 4 1
A getAttributeBunchProcessor() 0 4 1
A addAttributeCodeIdMapping() 0 4 1
A hasBeenProcessed() 0 4 1
A getLastAttributeId() 0 4 1
A isSwatchType() 0 4 2
A persistCatalogAttribute() 0 4 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
/**
4
 * TechDivision\Import\Attribute\Observers\CatalogAttributeObserver
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-attribute
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Attribute\Observers;
22
23
use TechDivision\Import\Utils\EntityStatus;
24
use TechDivision\Import\Attribute\Utils\ColumnKeys;
25
use TechDivision\Import\Attribute\Utils\MemberNames;
26
use TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface;
27
use TechDivision\Import\Attribute\Utils\EntityTypeCodes;
28
use TechDivision\Import\Observers\StateDetectorInterface;
29
use TechDivision\Import\Observers\EntityMergers\EntityMergerInterface;
30
31
/**
32
 * Observer that create's the EAV catalog attribute itself.
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-attribute
38
 * @link      http://www.techdivision.com
39
 */
40
class CatalogAttributeObserver extends AbstractAttributeImportObserver
41
{
42
43
    /**
44
     * The key for the additional data containing the swatch type.
45
     *
46
     * @var string
47
     */
48
    const SWATCH_INPUT_TYPE = 'swatch_input_type';
49
50
    /**
51
     * The available swatch types.
52
     *
53
     * @var array
54
     */
55
    protected $swatchTypes = array('text', 'visual', 'image');
56
57
    /**
58
     * The attribute processor instance.
59
     *
60
     * @var \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface
61
     */
62
    protected $attributeBunchProcessor;
63
64
    /**
65
     * The collection with entity merger instances.
66
     *
67
     * @var \Doctrine\Common\Collections\Collection
68
     */
69
    protected $entityMergers;
70
71
    /**
72
     * The array with the possible column names.
73
     *
74
     * @var array
75
     */
76
    protected $columnNames = array(
77
        ColumnKeys::FRONTEND_INPUT_RENDERER,
78
        ColumnKeys::IS_GLOBAL,
79
        ColumnKeys::IS_VISIBLE,
80
        ColumnKeys::IS_SEARCHABLE,
81
        ColumnKeys::IS_FILTERABLE,
82
        ColumnKeys::IS_COMPARABLE,
83
        ColumnKeys::IS_VISIBLE_ON_FRONT,
84
        ColumnKeys::IS_HTML_ALLOWED_ON_FRONT,
85
        ColumnKeys::IS_USED_FOR_PRICE_RULES,
86
        ColumnKeys::IS_FILTERABLE_IN_SEARCH,
87
        ColumnKeys::USED_IN_PRODUCT_LISTING,
88
        ColumnKeys::USED_FOR_SORT_BY,
89
        ColumnKeys::APPLY_TO,
90
        ColumnKeys::IS_VISIBLE_IN_ADVANCED_SEARCH,
91
        ColumnKeys::POSITION,
92
        ColumnKeys::IS_WYSIWYG_ENABLED,
93
        ColumnKeys::IS_USED_FOR_PROMO_RULES,
94
        ColumnKeys::IS_REQUIRED_IN_ADMIN_STORE,
95
        ColumnKeys::IS_USED_IN_GRID,
96
        ColumnKeys::IS_VISIBLE_IN_GRID,
97
        ColumnKeys::IS_FILTERABLE_IN_GRID,
98
        ColumnKeys::SEARCH_WEIGHT,
99
        ColumnKeys::ADDITIONAL_DATA
100
    );
101
102
    /**
103
     * Initializes the observer with the passed subject instance.
104
     *
105
     * @param \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface $attributeBunchProcessor The attribute bunch processor instance
106
     * @param \TechDivision\Import\Observers\EntityMergers\EntityMergerInterface       $entityMerger            The entity merger instance
107
     * @param \TechDivision\Import\Observers\StateDetectorInterface|null               $stateDetector           The state detector instance to use
108
     */
109 3 View Code Duplication
    public function __construct(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
110
        AttributeBunchProcessorInterface $attributeBunchProcessor,
111
        EntityMergerInterface $entityMerger = null,
112
        StateDetectorInterface $stateDetector = null
113
    ) {
114
115
        // initialize the bunch processor and the entity merger instance
116 3
        $this->attributeBunchProcessor = $attributeBunchProcessor;
117 3
        $this->entityMerger = $entityMerger;
0 ignored issues
show
Bug introduced by
The property entityMerger does not seem to exist. Did you mean entityMergers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
118
119
        // pass the state detector to the parent method
120 3
        parent::__construct($stateDetector);
121 3
    }
122
123
    /**
124
     * Process the observer's business logic.
125
     *
126
     * @return void
127
     */
128 3
    protected function process()
129
    {
130
131
        // query whether or not, we've found a new attribute code => means we've found a new attribute
132 3
        if ($this->hasBeenProcessed($this->getValue(ColumnKeys::ATTRIBUTE_CODE))) {
133
            return;
134
        }
135
136
        // initialize and persist the EAV catalog attribute
137 3
        $this->persistCatalogAttribute($this->initializeAttribute($this->prepareAttributes()));
138 3
    }
139
140
    /**
141
     * Merge's and return's the entity with the passed attributes and set's the
142
     * passed status.
143
     *
144
     * @param array       $entity        The entity to merge the attributes into
145
     * @param array       $attr          The attributes to be merged
146
     * @param string|null $changeSetName The change set name to use
147
     *
148
     * @return array The merged entity
149
     * @todo https://github.com/techdivision/import/issues/179
150
     */
151 2 View Code Duplication
    protected function mergeEntity(array $entity, array $attr, $changeSetName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
152
    {
153 2
        return array_merge(
154 2
            $entity,
155 2
            $this->entityMerger ? $this->entityMerger->merge($this, $entity, $attr) : $attr,
0 ignored issues
show
Bug introduced by
The property entityMerger does not seem to exist. Did you mean entityMergers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
156 2
            array(EntityStatus::MEMBER_NAME => $this->detectState($entity, $attr, $changeSetName))
157
        );
158
    }
159
160
    /**
161
     * Prepare the attributes of the entity that has to be persisted.
162
     *
163
     * @return array The prepared attributes
164
     * @throws \Exception Is thrown, if the size of the option values doesn't equals the size of swatch values, in case
165
     */
166 3
    protected function prepareAttributes()
167
    {
168
169
        // load the recently created EAV attribute ID
170 3
        $attributeId = $this->getLastAttributeId();
171
172
        // initialize the attributes
173 3
        $attr = array(MemberNames::ATTRIBUTE_ID => $attributeId);
174
175
        // iterate over the possible columns and handle the data
176 3
        foreach ($this->columnNames as $columnName) {
177
            // query whether or not, the column is available in the CSV file
178 3
            if ($this->getSubject()->hasHeader($columnName)) {
179
                // custom handling for the additional_data column
180 1
                if ($columnName === ColumnKeys::ADDITIONAL_DATA) {
181
                    // load the raw additional data
182 1
                    $explodedAdditionalData = $this->getValue(ColumnKeys::ADDITIONAL_DATA, array(), array($this->getSubject(), 'explode'));
183
184
                    // query whether or not additional data has been set
185 1
                    if (sizeof($explodedAdditionalData) > 0) {
186
                        // load and extract the additional data
187 1
                        $additionalData = array();
188 1
                        foreach ($explodedAdditionalData as $value) {
189 1
                            list ($key, $val) = $this->getSubject()->explode($value, '=');
190 1
                            $additionalData[$key] = $val;
191
                        }
192
193
                        // set the additional data
194 1
                        $attr[$columnName] = $additionalData;
195
196
                        // query whether or not the attribute is a text or a visual swatch
197 1
                        if ($this->isSwatchType($additionalData)) {
198
                            // load the attribute option values for the custom store views
199 1
                            $attributeOptionValues = $this->getValue(ColumnKeys::ATTRIBUTE_OPTION_VALUES, array(), array($this, 'explode'));
200 1
                            $attributeOptionSwatch = $this->getSubject()->explode($this->getValue(ColumnKeys::ATTRIBUTE_OPTION_SWATCH), $this->getSubject()->getMultipleValueDelimiter());
201
202
                            // query whether or not the size of the option values equals the size of the swatch values
203 1
                            if (($sizeOfSwatchValues = sizeof($attributeOptionSwatch)) !== ($sizeOfOptionValues = sizeof($attributeOptionValues))) {
204
                                throw new \Exception(
205
                                    sprintf(
206
                                        'Size of option values "%d" doesn\'t equals size of swatch values "%d"',
207
                                        $sizeOfOptionValues,
208 1
                                        $sizeOfSwatchValues
209
                                    )
210
                                );
211
                            }
212
                        }
213
                    }
214
                } else {
215
                    // query whether or not a column contains a value
216 1
                    if ($this->hasValue($columnName)) {
217 1
                        $attr[$columnName] = $this->getValue($columnName);
218
                    }
219
                }
220
            }
221
        }
222
223
        // return the prepared product
224 3
        return $this->initializeEntity($this->loadRawEntity($attr));
225
    }
226
227
    /**
228
     * Serialize the additional_data attribute of the passed array.
229
     *
230
     * @param array $attr The attribute with the data to serialize
231
     *
232
     * @return array The attribute with the serialized additional_data
233
     */
234 3
    protected function serializeAdditionalData(array $attr)
235
    {
236
237
        // serialize the additional data value if available
238 3
        if (isset($attr[MemberNames::ADDITIONAL_DATA]) && $attr[MemberNames::ADDITIONAL_DATA] !== null) {
239 2
            $attr[MemberNames::ADDITIONAL_DATA] = json_encode($attr[MemberNames::ADDITIONAL_DATA]);
240
        }
241
242
        // return the attribute
243 3
        return $attr;
244
    }
245
246
    /**
247
     * Load's and return's a raw customer entity without primary key but the mandatory members only and nulled values.
248
     *
249
     * @param array $data An array with data that will be used to initialize the raw entity with
250
     *
251
     * @return array The initialized entity
252
     */
253 3
    protected function loadRawEntity(array $data = array())
254
    {
255 3
        return $this->getAttributeBunchProcessor()->loadRawEntity(EntityTypeCodes::CATALOG_EAV_ATTRIBUTE, $data);
256
    }
257
258
    /**
259
     * Initialize the attribute with the passed attributes and returns an instance.
260
     *
261
     * @param array $attr The attribute attributes
262
     *
263
     * @return array The initialized attribute
264
     */
265 1
    protected function initializeAttribute(array $attr)
266
    {
267 1
        return $this->serializeAdditionalData($attr);
268
    }
269
270
    /**
271
     * Return's the attribute bunch processor instance.
272
     *
273
     * @return \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface The attribute bunch processor instance
274
     */
275 3
    protected function getAttributeBunchProcessor()
276
    {
277 3
        return $this->attributeBunchProcessor;
278
    }
279
280
    /**
281
     * Map's the passed attribute code to the attribute ID that has been created recently.
282
     *
283
     * @param string $attributeCode The attribute code that has to be mapped
284
     *
285
     * @return void
286
     */
287
    protected function addAttributeCodeIdMapping($attributeCode)
288
    {
289
        $this->getSubject()->addAttributeCodeIdMapping($attributeCode);
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 addAttributeCodeIdMapping() does only exist in the following implementations of said interface: TechDivision\Import\Attr...e\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...
290
    }
291
292
    /**
293
     * Queries whether or not the attribute with the passed code has already been processed.
294
     *
295
     * @param string $attributeCode The attribute code to check
296
     *
297
     * @return boolean TRUE if the path has been processed, else FALSE
298
     */
299 3
    protected function hasBeenProcessed($attributeCode)
300
    {
301 3
        return $this->getSubject()->hasBeenProcessed($attributeCode);
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 hasBeenProcessed() does only exist in the following implementations of said interface: TechDivision\Import\Attr...rs\AttributeSubjectImpl, TechDivision\Import\Attr...e\Subjects\BunchSubject, TechDivision\Import\Attr...\Subjects\OptionSubject.

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...
302
    }
303
304
    /**
305
     * Return's the ID of the attribute that has been created recently.
306
     *
307
     * @return integer The attribute ID
308
     */
309 3
    protected function getLastAttributeId()
310
    {
311 3
        return $this->getSubject()->getLastAttributeId();
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 getLastAttributeId() does only exist in the following implementations of said interface: TechDivision\Import\Attr...rs\AttributeSubjectImpl, TechDivision\Import\Attr...e\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...
312
    }
313
314
    /**
315
     * Return's TRUE if the additional data contains a swatch type.
316
     *
317
     * @param array $additionalData The additional data to query for a valid swatch type
318
     *
319
     * @return boolean TRUE if the data contains a swatch type, else FALSE
320
     */
321 1
    protected function isSwatchType(array $additionalData)
322
    {
323 1
        return isset($additionalData[CatalogAttributeObserver::SWATCH_INPUT_TYPE]) && in_array($additionalData[CatalogAttributeObserver::SWATCH_INPUT_TYPE], $this->swatchTypes);
324
    }
325
326
    /**
327
     * Persist the passed EAV catalog attribute.
328
     *
329
     * @param array $catalogAttribute The EAV catalog attribute to persist
330
     *
331
     * @return void
332
     */
333 3
    protected function persistCatalogAttribute(array $catalogAttribute)
334
    {
335 3
        return $this->getAttributeBunchProcessor()->persistCatalogAttribute($catalogAttribute);
336
    }
337
}
338