Struct::offsetSet()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
3
namespace Flying\Struct;
4
5
use Flying\Config\AbstractConfig;
6
use Flying\Struct\Common\ComplexPropertyInterface;
7
use Flying\Struct\Common\SimplePropertyInterface;
8
use Flying\Struct\Common\UpdateNotifyListenerInterface;
9
use Flying\Struct\Metadata\MetadataInterface;
10
use Flying\Struct\Metadata\MetadataModificationInterface;
11
use Flying\Struct\Metadata\StructMetadata;
12
use Flying\Struct\Property\Property;
13
use Flying\Struct\Property\PropertyInterface;
14
15
/**
16
 * Base implementation of structure class
17
 */
18
class Struct extends AbstractConfig implements StructInterface, MetadataModificationInterface
19
{
20
    /**
21
     * Initial contents for structure properties
22
     *
23
     * @var array
24
     */
25
    protected $initialContents;
26
    /**
27
     * TRUE to skip property change notification, FALSE otherwise
28
     *
29
     * @var boolean
30
     */
31
    protected $skipNotify = false;
32
    /**
33
     * Parent structure or NULL if this is top-level structure
34
     *
35
     * @var Struct
36
     */
37
    protected $parent;
38
    /**
39
     * Structure contents
40
     *
41
     * @var array
42
     */
43
    private $struct;
44
    /**
45
     * Structure size (for Countable interface)
46
     *
47
     * @var int
48
     */
49
    private $count = 0;
50
    /**
51
     * Current index in structure (for Iterator interface)
52
     *
53
     * @var int
54
     */
55
    private $index = 0;
56
    /**
57
     * Structure metadata
58
     *
59
     * @var StructMetadata
60
     */
61
    private $metadata;
62
63
    /**
64
     * Class constructor
65
     *
66
     * @param array|object $contents OPTIONAL Contents to initialize structure with
67
     * @param array|object $config   OPTIONAL Configuration for this structure
68
     * @throws \Flying\Struct\Exception
69
     * @throws \RuntimeException
70
     */
71 163 View Code Duplication
    public function __construct($contents = null, $config = 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...
72
    {
73
        // No change notification is required during object construction
74 163
        $flag = $this->skipNotify;
75 163
        $this->skipNotify = true;
76 163
        $this->setConfig($config);
0 ignored issues
show
Bug introduced by
It seems like $config defined by parameter $config on line 71 can also be of type null; however, Flying\Config\AbstractConfig::setConfig() does only seem to accept array|object|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
77 163
        if ($contents !== null) {
78 9
            $this->setInitialContents($contents);
79 9
        }
80 163
        $this->createStruct($this->getMetadata());
81 163
        $this->skipNotify = $flag;
82 163
    }
83
84
    /**
85
     * Create structure from given metadata information
86
     *
87
     * @param StructMetadata $metadata
88
     * @throws Exception
89
     * @return void
90
     * @throws \RuntimeException
91
     */
92 163
    protected function createStruct(StructMetadata $metadata = null)
93
    {
94 163
        if (!$metadata) {
95 14
            $metadata = $this->getMetadata();
96 14
        }
97 163
        $contents = $this->getInitialContents();
98
        $baseConfig = [
99 163
            'parent_structure'       => $this,
100 163
            'update_notify_listener' => $this,
101 163
        ];
102 163
        $this->struct = [];
103
        /** @var $property MetadataInterface */
104 163
        foreach ($metadata->getProperties() as $name => $property) {
105 163
            $class = $property->getClass();
106 163
            $value = array_key_exists($name, $contents) ? $contents[$name] : null;
107 163
            $config = array_merge($property->getConfig(), $baseConfig);
108 163
            if ($property instanceof StructMetadata) {
109 61
                $config['metadata'] = $property;
110 61
            }
111 163
            if ($class === null) {
112
                // Structure properties metadata is defined explicitly
113 14
                $class = $this->getConfig('explicit_metadata_class');
114 14
                $config['metadata'] = $property;
115 14
            }
116 163
            $instance = new $class($value, $config);
117 163
            if ((!$instance instanceof PropertyInterface) && (!$instance instanceof StructInterface)) {
118
                throw new Exception('Invalid class "' . $class . '" for structure property: ' . $name);
119
            }
120 163
            $this->struct[$name] = $instance;
121 163
        }
122 163
        $this->count = count($this->struct);
123 163
        $this->rewind();
124 163
    }
125
126
    /**
127
     * Get structure metadata
128
     *
129
     * @return StructMetadata
130
     * @throws \Flying\Struct\Exception
131
     */
132 163
    protected function getMetadata()
133
    {
134 163
        if (!$this->metadata) {
135
            try {
136
                /** @var $metadata StructMetadata */
137 161
                $metadata = $this->getConfig('metadata');
138 161
                if (!$metadata instanceof StructMetadata) {
139
                    /** @var $configuration Configuration */
140
                    $configuration = $this->getConfig('configuration');
141
                    $metadata = $configuration->getMetadataManager()->getMetadata($this);
142
                    /** @noinspection NotOptimalIfConditionsInspection */
143
                    if (!$metadata instanceof StructMetadata) {
144
                        throw new Exception('No metadata information is found for structure: ' . get_class($this));
145
                    }
146
                }
147 161
            } catch (\RuntimeException $e) {
148
                throw new Exception('Failed to obtain metadata information for structure: ' . get_class($this));
149
            }
150 161
            $this->metadata = $metadata;
151 161
        }
152 163
        return $this->metadata;
153
    }
154
155
    /**
156
     * Get initial structure contents
157
     *
158
     * @param string $name      OPTIONAL Structure property name to get contents of,
159
     *                          NULL to get all available contents
160
     * @return mixed
161
     */
162 163
    protected function getInitialContents($name = null)
163
    {
164 163
        if (!is_array($this->initialContents)) {
165 83
            $this->initialContents = [];
166 83
        }
167 163
        if ($name !== null) {
168
            return array_key_exists($name, $this->initialContents) ? $this->initialContents[$name] : null;
169
        }
170
171 163
        return $this->initialContents;
172
    }
173
174
    /**
175
     * Set initial structure contents
176
     *
177
     * @param array|object $contents
178
     * @return void
179
     */
180 9
    protected function setInitialContents($contents)
181
    {
182 9
        if (is_array($contents)) {
183 2
            $this->initialContents = $contents;
184 9
        } elseif (is_object($contents)) {
185 5
            $this->initialContents = $this->convertToArray($contents);
186 5
        }
187 9
        if (!is_array($this->initialContents)) {
188 2
            $this->initialContents = [];
189 2
        }
190 9
    }
191
192
    /**
193
     * Defined by Iterator interface
194
     *
195
     * @return void
196
     */
197 163
    public function rewind()
198
    {
199 163
        reset($this->struct);
200 163
        $this->index = 0;
201 163
    }
202
203
    /**
204
     * Modify metadata for this structure after it was parsed by MetadataManager
205
     *
206
     * @param StructMetadata $metadata
207
     */
208 161
    public static function modifyMetadata(StructMetadata $metadata)
209
    {
210
211 161
    }
212
213
    /**
214
     * Handling of object cloning
215
     *
216
     * @return void
217
     */
218 4
    public function __clone()
219
    {
220
        $config = [
221 4
            'parent_structure'       => $this,
222 4
            'update_notify_listener' => $this,
223 4
        ];
224
        /** @var $property Property */
225 4
        foreach ($this->struct as &$property) {
226 4
            $property = clone $property;
227 4
            $property->setConfig($config);
228 4
        }
229 4
    }
230
231
    /**
232
     * Get structure property with given name
233
     *
234
     * @param string $name
235
     * @return PropertyInterface|ComplexPropertyInterface|null
236
     */
237 7
    public function getProperty($name)
238
    {
239 7
        if ($this->__isset($name)) {
240 7
            return $this->struct[$name];
241
        }
242 6
        return null;
243
    }
244
245
    /**
246
     * Support isset() overloading
247
     *
248
     * @param string $name
249
     * @return boolean
250
     */
251 9
    public function __isset($name)
252
    {
253 9
        return array_key_exists($name, $this->struct);
254
    }
255
256
    /**
257
     * Magic function so that $obj->value will work.
258
     *
259
     * @param string $name
260
     * @return mixed
261
     */
262 23
    public function __get($name)
263
    {
264 23
        return $this->get($name);
265
    }
266
267
    /**
268
     * Magic function for setting structure property value
269
     *
270
     * @param string $name Structure property name to set value of
271
     * @param mixed $value New value for this property
272
     * @return void
273
     * @throws \RuntimeException
274
     */
275 15
    public function __set($name, $value)
276
    {
277 15
        $this->set($name, $value);
278 15
    }
279
280
    /**
281
     * Retrieve value of structure property with given name and return $default if there is no such property
282
     *
283
     * @param string $name   Structure property name to get value of
284
     * @param mixed $default OPTIONAL Default value to return in a case if property is not available
285
     * @return mixed
286
     */
287 77
    public function get($name, $default = null)
288
    {
289 77
        $result = $default;
290 77
        if (array_key_exists($name, $this->struct)) {
291 75
            $property = $this->struct[$name];
292 75
            if ($property instanceof ComplexPropertyInterface) {
293 40
                $result = $property;
294 75
            } elseif ($property instanceof PropertyInterface) {
295 64
                $result = $property->getValue();
296 64
            }
297 75
        } else {
298 4
            $result = $this->getMissed($name, $default);
299
        }
300 77
        return $result;
301
    }
302
303
    /**
304
     * Handle get requests for missed structure properties
305
     *
306
     * @param string $name   Requested structure property name
307
     * @param mixed $default Given default value
308
     * @return mixed
309
     */
310 4
    protected function getMissed($name, $default)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
311
    {
312
        // This method can be overridden into derived classes
313
        // to handle attempts to get missed structure properties
314 4
        return $default;
315
    }
316
317
    /**
318
     * Set value of structure property with given name
319
     *
320
     * @param string|array $name    Either name of structure property to set value of
321
     *                              or array of structure properties to set
322
     * @param mixed $value          OPTIONAL New value for this property (only if $name is a string)
323
     * @return void
324
     * @throws \RuntimeException
325
     */
326 54
    public function set($name, $value = null)
327
    {
328 54
        $values = is_scalar($name) ? [$name => $value] : $name;
329 54
        foreach ($values as $k => $v) {
330 51
            if (!array_key_exists($k, $this->struct)) {
331 10
                $this->setMissed($k, $v);
332 10
                continue;
333
            }
334 49
            $property = $this->struct[$k];
335 49
            if ($property instanceof PropertyInterface) {
336 49
                $property->setValue($v);
337 49
            } elseif ($property instanceof StructInterface) {
338 12
                $property->set($v);
339 12
                $this->updateNotify($property);
340 12
            }
341 49
            $this->onChange($k);
342 54
        }
343 54
    }
344
345
    /**
346
     * Handle set requests for missed structure properties
347
     *
348
     * @param string $name Structure property name to set
349
     * @param mixed $value Given value for the property
350
     * @return void
351
     */
352 10
    protected function setMissed($name, $value)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
353
    {
354
        // This method can be overridden into derived classes
355
        // to handle attempts to set missed structure properties
356 10
    }
357
358
    /**
359
     * Handle notification about update of given property
360
     *
361
     * @param SimplePropertyInterface $property
362
     * @return void
363
     * @throws \RuntimeException
364
     */
365 53 View Code Duplication
    public function updateNotify(SimplePropertyInterface $property)
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...
366
    {
367 53
        if (!$this->skipNotify) {
368 42
            $owner = $this->getConfig('update_notify_listener');
369 42
            if ($owner instanceof UpdateNotifyListenerInterface) {
370 20
                $owner->updateNotify($property);
371 20
            }
372 42
        }
373 53
    }
374
375
    /**
376
     * Structure properties change event handler
377
     *
378
     * @param string $name Name of changed property
379
     * @return void
380
     */
381 49
    protected function onChange($name)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
382
    {
383
        // This method can be overridden into derived classes
384
        // to perform some additional tasks upon structure's properties change
385 49
    }
386
387
    /**
388
     * Reset structure to its initial state
389
     *
390
     * @return void
391
     */
392 2
    public function reset()
393
    {
394 2
        $flag = $this->skipNotify;
395 2
        $this->skipNotify = true;
396
        /** @var $property PropertyInterface */
397 2
        foreach ($this->struct as $property) {
398 2
            $property->reset();
399 2
        }
400 2
        $this->skipNotify = $flag;
401 2
        $this->rewind();
402 2
    }
403
404
    /**
405
     * Defined by Countable interface
406
     *
407
     * @return int
408
     */
409 14
    public function count()
410
    {
411 14
        return $this->count;
412
    }
413
414
    /**
415
     * Defined by Iterator interface
416
     *
417
     * @return mixed
418
     */
419 32
    public function current()
420
    {
421 32
        return $this->get(key($this->struct));
422
    }
423
424
    /**
425
     * Defined by Iterator interface
426
     *
427
     * @return mixed
428
     */
429 32
    public function key()
430
    {
431 32
        return key($this->struct);
432
    }
433
434
    /**
435
     * Defined by Iterator interface
436
     *
437
     * @return void
438
     */
439 32
    public function next()
440
    {
441 32
        next($this->struct);
442 32
        $this->index++;
443 32
    }
444
445
    /**
446
     * Defined by Iterator interface
447
     *
448
     * @return boolean
449
     */
450 32
    public function valid()
451
    {
452 32
        return ($this->index < $this->count);
453
    }
454
455
    /**
456
     * Defined by RecursiveIterator interface
457
     *
458
     * @return boolean
459
     */
460 14
    public function hasChildren()
461
    {
462 14
        return ($this->struct[key($this->struct)] instanceof StructInterface);
463
    }
464
465
    /**
466
     * Defined by RecursiveIterator interface
467
     *
468
     * @return \RecursiveIterator
469
     */
470 7
    public function getChildren()
471
    {
472 7
        return $this->struct[key($this->struct)];
473
    }
474
475
    /**
476
     * Defined by ArrayAccess interface
477
     *
478
     * @param mixed $offset
479
     * @return boolean
480
     */
481 2
    public function offsetExists($offset)
482
    {
483 2
        return $this->__isset($offset);
484
    }
485
486
    /**
487
     * Defined by ArrayAccess interface
488
     *
489
     * @param mixed $offset
490
     * @return mixed
491
     */
492 18
    public function offsetGet($offset)
493
    {
494 18
        return $this->get($offset);
495
    }
496
497
    /**
498
     * Defined by ArrayAccess interface
499
     *
500
     * @param mixed $offset
501
     * @param mixed $value
502
     * @return void
503
     * @throws \RuntimeException
504
     */
505 4
    public function offsetSet($offset, $value)
506
    {
507 4
        $this->set($offset, $value);
508 4
    }
509
510
    /**
511
     * Defined by ArrayAccess interface
512
     *
513
     * @param mixed $offset
514
     * @return void
515
     */
516 2
    public function offsetUnset($offset)
517
    {
518 2
        $this->__unset($offset);
519 2
    }
520
521
    /**
522
     * Support unset() overloading
523
     * Unset of structure property in a term of removing it from structure is not allowed,
524
     * so unset() just reset field's value.
525
     *
526
     * @param  string $name
527
     * @return void
528
     */
529 4
    public function __unset($name)
530
    {
531 4
        if (array_key_exists($name, $this->struct)) {
532
            /** @var $property PropertyInterface */
533 4
            $property = $this->struct[$name];
534 4
            $property->reset();
535 4
        }
536 4
    }
537
538
    /**
539
     * Implementation of Serializable interface
540
     *
541
     * @return string
542
     * @throws \Flying\Struct\Exception
543
     */
544 14
    public function serialize()
545
    {
546 14
        return serialize([
547 14
            'metadata' => $this->getMetadata(),
548 14
            'struct'   => $this->toArray(),
549 14
        ]);
550
    }
551
552
    /**
553
     * Get structure contents as associative array
554
     *
555
     * @return array
556
     */
557 74
    public function toArray()
558
    {
559 74
        $array = [];
560 74
        foreach ($this->struct as $key => $value) {
561 74
            if ($value instanceof ComplexPropertyInterface) {
562 29
                $array[$key] = $value->toArray();
563 74
            } elseif ($value instanceof PropertyInterface) {
564 74
                $array[$key] = $value->getValue();
565 74
            }
566 74
        }
567 74
        return $array;
568
    }
569
570
    /**
571
     * Implementation of Serializable interface
572
     *
573
     * @param string $serialized Serialized object data
574
     * @return void
575
     * @throws \InvalidArgumentException
576
     * @throws \RuntimeException
577
     * @throws \Flying\Struct\Exception
578
     */
579 14
    public function unserialize($serialized)
580
    {
581 14
        $data = unserialize($serialized);
582 14
        if ((!is_array($data)) ||
583 14
            (!array_key_exists('metadata', $data)) ||
584 14
            (!array_key_exists('struct', $data)) ||
585 14
            (!$data['metadata'] instanceof StructMetadata) ||
586 14
            (!is_array($data['struct']))
587 14
        ) {
588
            throw new \InvalidArgumentException('Serialized structure information has invalid format');
589
        }
590 14
        $flag = $this->skipNotify;
591 14
        $this->skipNotify = true;
592 14
        $this->setConfig('metadata', $data['metadata']);
593 14
        $this->createStruct();
594 14
        $this->set($data['struct']);
595 14
        $this->skipNotify = $flag;
596 14
    }
597
598
    /**
599
     * Attempt to convert given structure contents to array
600
     *
601
     * @param mixed $contents Value to convert to array
602
     * @return array
603
     */
604 10
    protected function convertToArray($contents)
605
    {
606 10
        if (is_object($contents)) {
607 10
            if (is_callable([$contents, 'toArray'])) {
608 4
                $contents = $contents->toArray();
609 10
            } elseif ($contents instanceof \ArrayObject) {
610 4
                $contents = $contents->getArrayCopy();
611 6
            } elseif ($contents instanceof \Iterator) {
612 2
                $temp = [];
613 2
                foreach ($contents as $k => $v) {
614 2
                    $temp[$k] = $v;
615 2
                }
616 2
                $contents = $temp;
617 2
            }
618 10
        }
619 10
        if (!is_array($contents)) {
620
            $contents = [];
621
        }
622 10
        return $contents;
623
    }
624
625
    /**
626
     * Initialize list of configuration options
627
     *
628
     * @return void
629
     * @throws \RuntimeException
630
     * @throws \InvalidArgumentException
631
     */
632 1
    protected function initConfig()
633
    {
634 1
        parent::initConfig();
635 1
        $this->mergeConfig([
636 1
            'configuration'           => null, // Structures configuration object (@see Configuration)
637 1
            'metadata'                => null, // Structure metadata
638 1
            'parent_structure'        => null, // Link to parent structure in a case of multi-level structures
639 1
            'update_notify_listener'  => null, // Listener for structure's update notifications
640 1
            'explicit_metadata_class' => null, // Name of the class to use to create structures from explicitly defined properties
641 1
        ]);
642 1
    }
643
644
    /**
645
     * Perform "lazy initialization" of configuration option with given name
646
     *
647
     * @param string $name Configuration option name
648
     * @return mixed
649
     * @throws \RuntimeException
650
     */
651 161
    protected function lazyConfigInit($name)
652
    {
653
        switch ($name) {
654 161
            case 'configuration':
655 161
                return ConfigurationManager::getConfiguration();
656 161
            case 'metadata':
657
                /** @var $configuration Configuration */
658 161
                $configuration = $this->getConfig('configuration');
659 161
                return $configuration->getMetadataManager()->getMetadata($this);
660 45
            case 'explicit_metadata_class':
661 14
                return __CLASS__;
662 31
            default:
663 31
                return parent::lazyConfigInit($name);
664 31
        }
665
    }
666
667
    /**
668
     * Check that given value of configuration option is valid
669
     *
670
     * @param string $name Configuration option name
671
     * @param mixed $value Option value (passed by reference)
672
     * @throws \InvalidArgumentException
673
     * @return boolean
674
     */
675 163
    protected function validateConfig($name, &$value)
676
    {
677
        switch ($name) {
678 163
            case 'configuration':
679 161
                if (($value !== null) && (!$value instanceof Configuration)) {
680
                    throw new \InvalidArgumentException('Structure configuration object must be instance of Configuration');
681
                }
682 161
                break;
683 163
            case 'metadata':
684 163
                if (($value !== null) && (!$value instanceof StructMetadata)) {
685
                    throw new \InvalidArgumentException('Structure metadata object must be instance of StructMetadata');
686
                }
687 163
                break;
688 85
            case 'parent_structure':
689 61
                if (($value !== null) && (!$value instanceof StructInterface)) {
690
                    throw new \InvalidArgumentException('Only structure object can be used as parent structure');
691
                }
692 61
                break;
693 85 View Code Duplication
            case 'update_notify_listener':
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...
694 85
                if (($value !== null) && (!$value instanceof UpdateNotifyListenerInterface)) {
695
                    throw new \InvalidArgumentException('Update notification listener must implement UpdateNotifyListenerInterface interface');
696
                }
697 85
                break;
698 14
            case 'explicit_metadata_class':
699 14
                if (!is_string($value)) {
700
                    throw new \InvalidArgumentException('Explicit metadata class name should be defined as string');
701
                }
702 14
                if (!class_exists($value)) {
703
                    throw new \InvalidArgumentException('Unknown class name is defined for explicit metadata class');
704
                }
705
                try {
706 14
                    $reflection = new \ReflectionClass($value);
707 14
                } catch (\ReflectionException $e) {
708
                    throw new \InvalidArgumentException('Invalid class is defined for explicit metadata class');
709
                }
710 14
                if ((!$reflection->isInstantiable()) || (!$reflection->implementsInterface(StructInterface::class))) {
711
                    throw new \InvalidArgumentException('Invalid class is defined for explicit metadata class');
712
                }
713 14
                break;
714
        }
715 163
        return true;
716
    }
717
718
    /**
719
     * {@inheritdoc}
720
     */
721 73
    protected function onConfigChange($name, $value)
722
    {
723
        switch ($name) {
724 73
            case 'metadata':
725 70
                $this->metadata = $value;
726 70
                break;
727 64
            case 'parent_structure':
728 61
                $this->parent = $value;
729 61
                break;
730
        }
731 73
        parent::onConfigChange($name, $value);
732 73
    }
733
}
734