Completed
Branch master (e8947e)
by Andreas
15:09
created

midcom_helper_reflector_copy   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 684
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 684
rs 2.5469
c 0
b 0
f 0
wmc 67
lcom 1
cbo 15

14 Methods

Rating   Name   Duplication   Size   Complexity  
A get_object() 0 4 1
B get_object_properties() 0 26 3
A get_parent_property() 0 5 1
B get_target_properties() 0 58 5
B resolve_object() 0 22 4
B copy_tree() 0 41 6
F copy_object() 0 118 23
A _copy_data() 0 11 3
B copy_parameters() 0 30 6
A copy_metadata() 0 15 3
A copy_attachments() 0 14 2
B copy_privileges() 0 39 4
A copy_object_tree() 0 16 2
B execute() 0 28 4

How to fix   Complexity   

Complex Class

Complex classes like midcom_helper_reflector_copy often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use midcom_helper_reflector_copy, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package midcom.helper.reflector
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
use midgard\introspection\helper;
10
11
/**
12
 * The Grand Unified Reflector, copying helper class
13
 *
14
 * @package midcom.helper.reflector
15
 */
16
class midcom_helper_reflector_copy extends midcom_baseclasses_components_purecode
1 ignored issue
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
17
{
18
    /**
19
     * Source
20
     *
21
     * @var mixed        GUID, MgdSchema or MidCOM dba object
22
     */
23
    public $source = null;
24
25
    /**
26
     * Target
27
     *
28
     * @var mixed        GUID, MgdSchema or MidCOM dba object
29
     */
30
    public $target = null;
31
32
    /**
33
     * Exclusion list
34
     *
35
     * @var array        List of GUIDs of objects that shall not be copied
36
     */
37
    public $exclude = array();
38
39
    /**
40
     * Override properties of the new root object. This feature is
41
     * directed for overriding e.g. parent information.
42
     *
43
     * @var array        Property-value pairs
44
     */
45
    public $root_object_values = array();
46
47
    /**
48
     * Switch for attachments
49
     *
50
     * @var boolean
51
     */
52
    public $copy_attachments = true;
53
54
    /**
55
     * Switch for parameters
56
     *
57
     * @var boolean
58
     */
59
    public $copy_parameters = true;
60
61
    /**
62
     * Switch for privileges
63
     *
64
     * @var boolean
65
     */
66
    public $copy_privileges = true;
67
68
    /**
69
     * Switch for metadata
70
     *
71
     * @var boolean
72
     */
73
    public $copy_metadata = true;
74
75
    /**
76
     * Copy the whole tree
77
     *
78
     * @var boolean
79
     */
80
    public $copy_tree = true;
81
82
    /**
83
     * Switch for name catenating
84
     *
85
     * @var boolean
86
     */
87
    public $allow_name_catenate = true;
88
89
    /**
90
     * Metadata fields that shall be copied
91
     */
92
    public $copy_metadata_fields = array
93
    (
94
        'owner',
95
        'authors',
96
        'schedulestart',
97
        'scheduleend',
98
        'navnoentry',
99
        'hidden',
100
        'score',
101
    );
102
103
    /**
104
     * Switch for halt on error. If this is set to false, errors will be
105
     * reported, but will not stop executing
106
     *
107
     * @var boolean        Set to false to continue on errors
108
     */
109
    public $halt_on_errors = true;
110
111
    /**
112
     * Encountered errors
113
     *
114
     * @var array
115
     */
116
    public $errors = array();
117
118
    /**
119
     * Newly created objects
120
     *
121
     * @var array
122
     */
123
    public $new_objects = array();
124
125
    /**
126
     * New root object
127
     */
128
    public $new_root_object = null;
129
130
    /**
131
     * Properties for each encountered MgdSchema object
132
     *
133
     * @var array         class_name => array of properties
134
     */
135
    private $properties = array();
136
137
    /**
138
     * Get the newly created root object
139
     *
140
     * @return mixed     Lowest level new MgdSchema object
141
     */
142
    public function get_object()
143
    {
144
        return $this->new_root_object;
145
    }
146
147
    /**
148
     * Get object properties
149
     *
150
     * @param mixed $object
151
     * @return array
152
     */
153
    public function get_object_properties($object)
154
    {
155
        $mgdschema_class = midcom_helper_reflector::resolve_baseclass(get_class($object));
156
157
        if (!isset($this->properties[$mgdschema_class]))
158
        {
159
            // Get property list and start checking (or abort on error)
160
            if (midcom::get()->dbclassloader->is_midcom_db_object($object))
161
            {
162
                $properties = $object->get_object_vars();
163
            }
164
            else
165
            {
166
                $helper = new helper;
167
                $properties = $helper->get_all_properties($object);
168
            }
169
170
            $return = array_diff($properties, array('id', 'guid', 'metadata'));
171
172
            // Cache them
173
            $this->properties[$mgdschema_class] = $return;
174
        }
175
176
        // ...and return
177
        return $this->properties[$mgdschema_class];
178
    }
179
180
    /**
181
     * Get the parent property for overriding it
182
     *
183
     * @param mixed $object     MgdSchema object for resolving the parent property
184
     * @return string            Parent property
185
     */
186
    public function get_parent_property($object)
187
    {
188
        $properties = self::get_target_properties($object);
189
        return $properties['parent'];
190
    }
191
192
    /**
193
     * Get the target properties and return an array that is used e.g. in copying
194
     *
195
     * @param mixed $object     MgdSchema object or MidCOM db object
196
     * @return array            id, parent property, class and label of the object
197
     */
198
    static public function get_target_properties($object)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
199
    {
200
        $mgdschema_class = midcom_helper_reflector::resolve_baseclass($object);
201
        $mgdschema_object = new $mgdschema_class($object->guid);
202
203
        static $targets = array();
204
205
        // Return the cached results
206
        if (isset($targets[$mgdschema_class]))
207
        {
208
            return $targets[$mgdschema_class];
209
        }
210
211
        // Empty result set for the current class
212
        $target = array
213
        (
214
            'id' => null,
215
            'parent' => '',
216
            'class' => $mgdschema_class,
217
            'label' => '',
218
            'reflector' => new midcom_helper_reflector($object),
219
        );
220
221
        // Try to get the parent property for determining, which property should be
222
        // used to point the parent of the new object. Attachments are a special case.
223
        if (!midcom::get()->dbfactory->is_a($object, 'midcom_db_attachment'))
224
        {
225
            $parent_property = midgard_object_class::get_property_parent($mgdschema_object);
226
        }
227
        else
228
        {
229
            $parent_property = 'parentobject';
230
        }
231
232
        // Get the class label
233
        $target['label'] = $target['reflector']->get_label_property();
234
235
        // Try once more to get the parent property, but now try up as a backup
236
        if (!$parent_property)
237
        {
238
            $up_property = midgard_object_class::get_property_up($mgdschema_object);
239
240
            if (!$up_property)
241
            {
242
                throw new midcom_error('Failed to get the parent property for copying');
243
            }
244
245
            $target['parent'] = $up_property;
246
        }
247
        else
248
        {
249
            $target['parent'] = $parent_property;
250
        }
251
252
        // Cache the results
253
        $targets[$mgdschema_class] = $target;
254
        return $targets[$mgdschema_class];
255
    }
256
257
    /**
258
     * Resolve MgdSchema object from guid or miscellaneous extended object
259
     *
260
     * @param mixed &$object    MgdSchema object, GUID or ID
261
     * @return mixed MgdSchema object or false on failure
262
     */
263
    static public function resolve_object(&$object)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
264
    {
265
        // Check the type of the requested parent
266
        if (mgd_is_guid($object))
267
        {
268
            $object = midcom::get()->dbfactory->get_object_by_guid($object);
269
        }
270
        if (!is_object($object))
271
        {
272
            return false;
273
        }
274
275
        $object_class = midcom_helper_reflector::resolve_baseclass(get_class($object));
276
277
        // Get the initial MgdSchema class
278
        if ($object_class !== get_class($object))
279
        {
280
            $object = new $object_class($object->guid);
281
        }
282
283
        return $object;
284
    }
285
286
    /**
287
     * Copy an object tree. Both source and parent may be liberally filled. Source can be either
288
     * MgdSchema or MidCOM db object or GUID of the object and parent can be
289
     *
290
     * - MgdSchema object
291
     * - MidCOM db object
292
     * - predefined target array (@see get_target_properties())
293
     * - ID or GUID of the object
294
     * - left empty to copy as a parentless object
295
     *
296
     * This method is self-aware and will refuse to perform any infinite loops (e.g. to copy
297
     * itself to its descendant, copying itself again and again and again).
298
     *
299
     * Eventually this method will return the first root object that was created, i.e. the root
300
     * of the new tree.
301
     *
302
     * @param mixed $source        GUID or MgdSchema object that will be copied
303
     * @param mixed $parent        MgdSchema or MidCOM db object, predefined array or ID of the parent object
304
     * @return mixed               False on failure, newly created MgdSchema root object on success
305
     */
306
    public function copy_tree(&$source, &$parent)
307
    {
308
        // Copy the root object
309
        $root = $this->copy_object($source, $parent);
310
311
        if (empty($root->guid))
312
        {
313
            $this->errors[] = sprintf($this->_l10n->get('failed to copy object %s'), $source->guid);
314
            return false;
315
        }
316
317
        // Add the newly copied object to the exclusion list to prevent infinite loops
318
        $this->exclude[] = $this->source->guid;
319
320
        // Get the children
321
        $children = midcom_helper_reflector_tree::get_child_objects($source);
322
323
        if (empty($children))
324
        {
325
            return $root;
326
        }
327
328
        // Loop through the children and copy them to their corresponding parents
329
        foreach ($children as $subchildren)
330
        {
331
            // Get the children of each type
332
            foreach ($subchildren as $child)
333
            {
334
                // Skip the excluded child
335
                if (in_array($child->guid, $this->exclude))
336
                {
337
                    continue;
338
                }
339
340
                $this->copy_tree($child, $root);
341
            }
342
        }
343
344
        // Return the newly created root object
345
        return $root;
346
    }
347
348
    /**
349
     * Copy an object
350
     *
351
     * @param mixed &$source     MgdSchema object for reading the parameters
352
     * @param mixed &$parent      MgdSchema parent object
353
     * @param array $defaults
354
     * @return boolean Indicating success
355
     */
356
    public function copy_object(&$source, &$parent = null, array $defaults = array())
357
    {
358
        // Resolve the source object
359
        self::resolve_object($source);
360
361
        // Duplicate the object
362
        $class_name = get_class($source);
363
        $target = new $class_name();
364
365
        $properties = $this->get_object_properties($source);
366
367
        // Copy the object properties
368
        foreach ($properties as $property)
369
        {
370
            // Skip certain fields
371
            if (preg_match('/^(_|metadata|guid|id)/', $property))
372
            {
373
                continue;
374
            }
375
376
            $target->$property = $source->$property;
377
        }
378
379
        // Override requested root object properties
380
        if (   !empty($this->target->guid)
381
            && $target->guid === $this->target->guid)
382
        {
383
            foreach ($this->root_object_values as $name => $value)
384
            {
385
                $target->$name = $value;
386
            }
387
        }
388
389
        // Override with defaults
390
        foreach ($defaults as $name => $value)
391
        {
392
            $target->$name = $value;
393
        }
394
395
        $parent_property = $this->get_parent_property($source);
396
397
        // Copy the link to parent
398
        if ($parent)
399
        {
400
            self::resolve_object($parent);
401
402
            if (empty($parent->guid))
403
            {
404
                return false;
405
            }
406
407
            // @TODO: Is there a sure way to determine if the parent is
408
            // GUID or is it ID? If so, please change it here.
409
            $parent_key = (is_string($source->$parent_property)) ? 'guid' : 'id';
410
            $target->$parent_property = $parent->$parent_key;
411
        }
412
        else
413
        {
414
            $target->$parent_property = (is_string($source->$parent_property)) ? '' : 0;
415
        }
416
417
        $name_property = midcom_helper_reflector::get_name_property($target);
418
        $resolver = new midcom_helper_reflector_nameresolver($target);
419
420
        if (   !empty($name_property)
421
            && !$resolver->name_is_safe_or_empty($name_property))
422
        {
423
            debug_add('Source object ' . get_class($source) . " {$source->guid} has unsafe name, rewriting to safe form for the target", MIDCOM_LOG_WARN);
424
            $generator = midcom::get()->serviceloader->load('midcom_core_service_urlgenerator');
425
            $name_parts = explode('.', $target->$name_property, 2);
426
            if (isset($name_parts[1]))
427
            {
428
                $target->$name_property = $generator->from_string($name_parts[0]) . ".{$name_parts[1]}";
429
                // Doublecheck safety and fall back if needed
430
                if (!$resolver->name_is_safe_or_empty())
431
                {
432
                    $target->$name_property = $generator->from_string($target->$name_property);
433
                }
434
            }
435
            else
436
            {
437
                $target->$name_property = $generator->from_string($target->$name_property);
438
            }
439
            unset($name_parts, $name_property);
440
        }
441
442
        if (   $this->allow_name_catenate
443
            && $name_property)
444
        {
445
            $name = $resolver->generate_unique_name();
446
447
            if ($name !== $target->$name_property)
448
            {
449
                $target->$name_property = $name;
450
            }
451
        }
452
453
        // This needs to be here, otherwise it will be overridden
454
        $target->allow_name_catenate = true;
455
        if (!$target->create())
456
        {
457
            $this->errors[] = $this->_l10n->get('failed to create object: ' . midcom_connection::get_error_string());
458
            return false;
459
        }
460
461
        // Store for later use - if ever needed
462
        $this->new_objects[] = $target;
463
464
        if (   !$this->_copy_data('parameters', $source, $target)
465
            || !$this->_copy_data('metadata', $source, $target)
466
            || !$this->_copy_data('attachments', $source, $target)
467
            || !$this->_copy_data('privileges', $source, $target))
468
        {
469
            return false;
470
        }
471
472
        return $target;
473
    }
474
475
    /**
476
     * Copy object data
477
     *
478
     * @param string $type        The type of data to copy
479
     * @param mixed &$source      MgdSchema object for reading the parameters
480
     * @param mixed &$target      MgdSchema object for storing the parameters
481
     * @return boolean Indicating success
482
     */
483
    private function _copy_data($type, $source, &$target)
484
    {
485
        $method = 'copy_' . $type;
486
        if (   !$this->$method($source, $target)
487
            && $this->halt_on_errors)
488
        {
489
            $this->errors[] = $this->_l10n->get('failed to copy ' . $type);
490
            return false;
491
        }
492
        return true;
493
    }
494
495
    /**
496
     * Copy parameters for the object
497
     *
498
     * @param mixed $source      MgdSchema object for reading the parameters
499
     * @param mixed $target      MgdSchema object for storing the parameters
500
     * @return boolean Indicating success
501
     */
502
    public function copy_parameters($source, $target)
503
    {
504
        if (!$this->copy_parameters)
505
        {
506
            return true;
507
        }
508
509
        $params = $source->list_parameters();
510
511
        if (count($params) === 0)
512
        {
513
            return true;
514
        }
515
516
        // Loop through the parameters
517
        foreach ($params as $parameter)
518
        {
519
            if (!$target->set_parameter($parameter->domain, $parameter->name, $parameter->value))
520
            {
521
                $this->errors[] = sprintf($this->_l10n->get('failed to copy parameters from %s to %s'), $source->guid, $target->guid);
522
523
                if ($this->halt_on_errors)
524
                {
525
                    return false;
526
                }
527
            }
528
        }
529
530
        return true;
531
    }
532
533
    /**
534
     * Copy metadata for the object
535
     *
536
     * @param mixed $source      MgdSchema object for reading the metadata
537
     * @param mixed $target      MgdSchema object for storing the metadata
538
     * @return boolean Indicating success
539
     */
540
    public function copy_metadata($source, $target)
541
    {
542
        foreach ($this->copy_metadata_fields as $property)
543
        {
544
            $target->metadata->$property = $source->metadata->$property;
545
        }
546
547
        if ($target->update())
548
        {
549
            return true;
550
        }
551
552
        $this->errors[] = sprintf($this->_l10n->get('failed to copy metadata from %s to %s'), $source->guid, $target->guid);
553
        return false;
554
    }
555
556
    /**
557
     * Copy attachments
558
     *
559
     * @param mixed &$source      MgdSchema object for reading the attachments
560
     * @param mixed &$target      MgdSchema object for storing the attachments
561
     * @return boolean Indicating success
562
     */
563
    public function copy_attachments(&$source, &$target)
564
    {
565
        $defaults = array
566
        (
567
            'parentguid' => $target->guid,
568
        );
569
570
        foreach ($source->list_attachments() as $attachment)
571
        {
572
            $this->copy_object($attachment, $target, $defaults);
573
        }
574
575
        return true;
576
    }
577
578
    /**
579
     * Copy privileges
580
     *
581
     * @param mixed $source      MgdSchema object for reading the privileges
582
     * @param mixed $target      MgdSchema object for storing the privileges
583
     * @return boolean Indicating success
584
     */
585
    public function copy_privileges($source, $target)
586
    {
587
        $qb = midcom_db_privilege::new_query_builder();
588
        $qb->add_constraint('objectguid', '=', $source->guid);
589
590
        $results = $qb->execute();
591
592
        static $privilege_fields = null;
593
594
        if (is_null($privilege_fields))
595
        {
596
            $privilege_fields = array
597
            (
598
                'classname',
599
                'assignee',
600
                'name',
601
                'value',
602
            );
603
        }
604
605
        foreach ($results as $privilege)
0 ignored issues
show
Bug introduced by
The expression $results of type array|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
606
        {
607
            $new = new midcom_db_privilege();
608
            $new->objectguid = $target->guid;
609
610
            $new->classname = $privilege->classname;
611
            $new->privilegename = $privilege->privilegename;
612
            $new->value = $privilege->value;
613
            $new->assignee = $privilege->assignee;
614
615
            if (!$new->create())
616
            {
617
                $this->errors[] = 'privilege creation failed';
618
                return false;
619
            }
620
        }
621
622
        return true;
623
    }
624
625
    /**
626
     * Copy an object tree. Both source and parent may be liberally filled. Source can be either
627
     * MgdSchema or MidCOM db object or GUID of the object and parent can be
628
     *
629
     * - MgdSchema object
630
     * - MidCOM db object
631
     * - predefined target array (@see get_target_properties())
632
     * - ID or GUID of the object
633
     * - left empty to copy as a parentless object
634
     *
635
     * This method is self-aware and will refuse to perform any infinite loops (e.g. to copy
636
     * itself to its descendant, copying itself again and again and again).
637
     *
638
     * Eventually this method will return the first root object that was created, i.e. the root
639
     * of the new tree.
640
     *
641
     * @param mixed $source        GUID or MgdSchema object that will be copied
642
     * @param mixed $parent        MgdSchema or MidCOM db object, predefined array or ID of the parent object
643
     * @param array $exclude       IDs that will be excluded from the copying
644
     * @param boolean $copy_parameters  Switch to determine if the parameters should be copied
645
     * @param boolean $copy_metadata    Switch to determine if the metadata should be copied (excluding created and published)
646
     * @param boolean $copy_attachments Switch to determine if the attachments should be copied (creates only a new link, doesn't duplicate the content)
647
     * @return mixed               False on failure, newly created MgdSchema root object on success
648
     */
649
    static public function copy_object_tree($source, $parent, $exclude = array(), $copy_parameters = true, $copy_metadata = true, $copy_attachments = true)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
650
    {
651
        $copy = new midcom_helper_reflector_copy();
652
        $copy->source =& $source;
653
        $copy->target =& $parent;
654
        $copy->copy_parameters = $copy_parameters;
655
        $copy->copy_metadata = $copy_metadata;
656
        $copy->copy_attachments = $copy_attachments;
657
658
        if (!$copy->execute())
659
        {
660
            return false;
661
        }
662
663
        return $copy->get_object();
664
    }
665
666
    /**
667
     * Dispatches the copy command according to the attributes set
668
     *
669
     * @return boolean Indicating success
670
     */
671
    public function execute()
672
    {
673
        if (!$this->resolve_object($this->source))
674
        {
675
            $this->errors[] = $this->_l10n->get('failed to get the source object');
676
            return false;
677
        }
678
679
        if ($this->copy_tree)
680
        {
681
            // Disable execution timeout and memory limit, this can be very intensive
682
            midcom::get()->disable_limits();
683
684
            $this->new_root_object = $this->copy_tree($this->source, $this->target);
685
        }
686
        else
687
        {
688
            $this->new_root_object = $this->copy_object($this->source, $this->target);
689
        }
690
691
        if (empty($this->new_root_object->guid))
692
        {
693
            $this->errors[] = $this->_l10n->get('failed to get the new root object');
694
            return false;
695
        }
696
697
        return true;
698
    }
699
}
700