Completed
Pull Request — develop (#720)
by Imants
03:58
created

AbstractObject::add()   F

Complexity

Conditions 25
Paths 1321

Size

Total Lines 154
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 25
c 5
b 0
f 0
dl 0
loc 154
rs 2
eloc 83
nc 1321
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * A base class for all objects/classes in Agile Toolkit.
4
 * Do not directly inherit from this class, instead use one of
5
 * AbstractModel, AbstractController or AbstractView.
6
 */
7
abstract class AbstractObject
8
{
9
    const DOC = 'core-features/objects';
10
11
    /**
12
     * Reference to the current model. Read only. Use setModel()
13
     *
14
     * @var Model
15
     */
16
    public $model;
17
18
    /**
19
     * Reference to the current controller. Read only. Use setController()
20
     *
21
     * @var Controller
22
     */
23
    public $controller;
24
25
    /**
26
     * Exception class to use when $this->exception() is called. When
27
     * you later call ->exception() method, you can either override
28
     * or postfix your exception with second argument.
29
     *
30
     *
31
     *  $default_exception='PathFinder' by default would
32
     *  return 'Exception_PathFinder' from exception().
33
     *
34
     *  $default_exceptoin='PathFinder' in combination with
35
     *  ->exception('Blah','_NotFound') will return
36
     *  'Exception_PathFinder_NotFound'
37
     *
38
     *  $default_exception='BaseException' in combination with
39
     *  ->exception('Blah', 'PathFinder')   will create
40
     *  'Exception_PathFinder' exception.
41
     *
42
     *  and finally
43
     *
44
     *  $default_exception='PathFinder' in combination with
45
     *  ->exception('Blah','NotFound') will return
46
     *  'Exception_NotFound';
47
     *
48
     * @todo implement test-cases for the above
49
     *
50
     * @var string
51
     */
52
    public $default_exception = 'BaseException';
53
54
    /**
55
     * Default controller to initialize when calling setModel()
56
     *
57
     * @var string
58
     */
59
    public $default_controller = null;
60
61
    /**
62
     * Setting this to true will output additional debug info about object
63
     *
64
     * @var boolean
65
     */
66
    public $debug = null;
67
68
    // {{{ Object hierarchy management
69
70
    /**
71
     * Unique object name
72
     *
73
     * @var string
74
     */
75
    public $name;
76
77
    /**
78
     * Name of the object in owner's element array
79
     *
80
     * @var string
81
     */
82
    public $short_name;
83
84
    /**
85
     * short_name => object hash of children objects
86
     *
87
     * @var array
88
     */
89
    public $elements = array();
90
91
    /**
92
     * Link to object into which we added this object
93
     *
94
     * @var AbstractObject
95
     */
96
    public $owner;
97
98
    /**
99
     * Always points to current Application
100
     *
101
     * @var App_CLI
102
     */
103
    public $app;
104
105
    /**
106
     * @deprecated 4.3.0 Left for compatibility with ATK 4.2 and lower, use ->app instead
107
     */
108
    public $api;
109
110
    /**
111
     * When this object is added, owner->elements[$this->short_name]
112
     * will be == $this;.
113
     *
114
     * @var boolean
115
     */
116
    public $auto_track_element = false;
117
118
    /**
119
     * To make sure you have called parent::init() properly.
120
     *
121
     * @var boolean
122
     */
123
    public $_initialized = false;
124
125
126
127
    /**
128
     * Initialize object. Always call parent::init(). Do not call directly.
129
     */
130
    public function init()
131
    {
132
        /*
133
         * This method is called for initialization
134
         */
135
        $this->_initialized = true;
136
    }
137
138
    /**
139
     * This is default constructor of ATK4. Please do not re-define it
140
     * and avoid calling it directly. Always use add() and init() methods.
141
     *
142
     * @param array $options will initialize class properties
143
     */
144
    public function __construct($options = array())
145
    {
146
        foreach ($options as $key => $val) {
147
            if ($key !== 'name') {
148
                $this->$key = $val;
149
            }
150
        }
151
    }
152
153
    /* \section Object Management Methods */
154
    /**
155
     * Clones associated controller and model. If cloning views, add them to
156
     * the owner.
157
     *
158
     * @return AbstractObject Copy of this object
159
     */
160
    public function __clone()
161
    {
162
        if ($this->model && is_object($this->model)) {
163
            $this->model = clone $this->model;
164
        }
165
        if ($this->controller && is_object($this->controller)) {
166
            $this->controller = clone $this->controller;
167
        }
168
    }
169
170
    /**
171
     * Converts into string "Object View(myapp_page_view)".
172
     *
173
     * @return string
174
     */
175
    public function __toString()
176
    {
177
        return 'Object '.get_class($this).'('.$this->name.')';
178
    }
179
180
    /**
181
     * Removes object from parent and prevents it from rendering
182
     * \code
183
     * $view = $this->add('View');
184
     * $view -> destroy();
185
     * \endcode.
186
     */
187
    public function destroy($recursive = true)
188
    {
189
        if ($recursive) {
190
            foreach ($this->elements as $el) {
191
                if ($el instanceof self) {
192
                    $el->destroy();
193
                }
194
            }
195
        }
196
        /*
197
        if (@$this->model && $this->model instanceof AbstractObject) {
198
            $this->model->destroy();
199
            unset($this->model);
200
        }
201
        if (@$this->controller && $this->controller instanceof AbstractObject) {
202
            $this->controller->destroy();
203
            unset($this->controller);
204
        }
205
        */
206
        $this->owner->_removeElement($this->short_name);
207
    }
208
209
    /**
210
     * Remove child element if it exists.
211
     *
212
     * @param string $short_name short name of the element
213
     *
214
     * @return $this
215
     */
216
    public function removeElement($short_name)
217
    {
218
        if (is_object($this->elements[$short_name])) {
219
            $this->elements[$short_name]->destroy();
220
        } else {
221
            unset($this->elements[$short_name]);
222
        }
223
224
        return $this;
225
    }
226
227
    /**
228
     * Actually removes the element.
229
     *
230
     * @param string $short_name short name
231
     *
232
     * @return $this
233
     * @access private
234
     */
235
    public function _removeElement($short_name)
236
    {
237
        unset($this->elements[$short_name]);
238
        if ($this->_element_name_counts[$short_name] === 1) {
239
            unset($this->_element_name_counts[$short_name]);
240
        }
241
242
        return $this;
243
    }
244
245
    /**
246
     * Creates one more instance of $this object.
247
     *
248
     * @param array $properties Set initial properties for new object
249
     *
250
     * @return self
251
     */
252
    public function newInstance($properties = null)
253
    {
254
        return $this->owner->add(get_class($this), $properties);
255
    }
256
257
    /**
258
     * Creates new object and adds it as a child of current object.
259
     * Returns new object.
260
     *
261
     * @param array|string|object $class    Name of the new class. Can also be array with 0=>name and
262
     *                                      rest of array will be considered as $options or object.
263
     * @param array|string $options         Short name or array of properties.
264
     *                                      0=>name will be used as a short-name or your object.
265
     * @param string       $template_spot   Tag where output will appear
266
     * @param array|string $template_branch Redefine template
267
     *
268
     * @link http://agiletoolkit.org/learn/understand/base/adding
269
     *
270
     * @return AbstractObject
271
     */
272
    public function add(
273
        $class,
274
        $options = null,
275
        $template_spot = null,
276
        $template_branch = null
277
    ) {
278
        if (is_array($class)) {
279
            if (!$class[0]) {
280
                throw $this->exception('When passing class as array, use ["Class", "option"=>123] format')
281
                    ->addMoreInfo('class', var_export($class, true));
282
            }
283
            $o = $class;
284
            $class = $o[0];
285
            unset($o[0]);
286
            $options = $options ? array_merge($options, $o) : $o;
287
        }
288
289
        if (is_string($options)) {
290
            $options = array('name' => $options);
291
        }
292
        if (!is_array($options)) {
293
            $options = array();
294
        }
295
296
        if (is_object($class)) {
297
            // Object specified, just add the object, do not create anything
298
            if (!($class instanceof self)) {
299
                throw $this->exception(
300
                    'You may only add objects based on AbstractObject'
301
                );
302
            }
303
            if (!$class->short_name) {
304
                $class->short_name = str_replace('\\', '_', strtolower(get_class($class)));
305
            }
306
            if (!$class->app) {
307
                $class->app = $this->app;
308
                $class->api = $this->app; // compatibility with ATK 4.2 and lower
0 ignored issues
show
Deprecated Code introduced by
The property AbstractObject::$api has been deprecated with message: 4.3.0 Left for compatibility with ATK 4.2 and lower, use ->app instead

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
309
            }
310
            $class->short_name = $this->_unique_element($class->short_name);
311
            $class->name = $this->_shorten($this->name.'_'.$class->short_name);
312
313
            $this->elements[$class->short_name] = $class;
314
            if ($class instanceof AbstractView) {
315
                /** @type AbstractView $class */
316
                $class->owner->elements[$class->short_name] = true;
317
            }
318
            $class->owner = $this;
319
            if ($class instanceof AbstractView) {
320
                /** @type AbstractView $class */
321
                // Scrutinizer complains that $this->template is not defined and it really is not :)
322
                if (!isset($this->template) || !$this->template) {
0 ignored issues
show
Bug introduced by
The property template does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
323
                    $class->initializeTemplate($template_spot, $template_branch);
324
                }
325
            }
326
327
            return $class;
328
        }
329
330
        if (!is_string($class) || !$class) {
331
            throw $this->exception('Class is not valid')
332
                ->addMoreInfo('class', $class);
333
        }
334
335
        $class = str_replace('/', '\\', $class);
336
337
        if ($class[0] == '.') {
338
            // Relative class name specified, extract current namespace
339
            // and make new class name relative to this namespace
340
            $ns = get_class($this);
341
            $ns = substr($ns, 0, strrpos($ns, '\\'));
342
            $class = $ns.'\\'.substr($class, 2);
343
        }
344
345
        $short_name = isset($options['name'])
346
            ? $options['name']
347
            : str_replace('\\', '_', strtolower($class));
348
349
        // Adding same controller twice will return existing one
350
        if (isset($this->elements[$short_name])) {
351
            if ($this->elements[$short_name] instanceof AbstractController) {
352
                return $this->elements[$short_name];
353
            }
354
        }
355
356
        $short_name = $this->_unique_element($short_name);
357
358
        if (isset($this->elements[$short_name])) {
359
            throw $this->exception($class.' with requested name already exists')
360
                ->addMoreInfo('class', $class)
361
                ->addMoreInfo('new_short_name', $short_name)
362
                ->addMoreInfo('object', $this)
363
                ->addMoreInfo('counts', json_encode($this->_element_name_counts))
364
                ->addThis($this);
365
        }
366
367
        $class_name_nodash = str_replace('-', '', $class);
368
        /*
369
         * Even though this might break some applications,
370
         * your loading must be configured properly instead
371
         * of relying on this
372
         *
373
        if (!class_exists($class_name_nodash, false)
374
            && isset($this->app->pathfinder)
375
        ) {
376
            $this->app->pathfinder->loadClass($class);
377
        }*/
378
        $element = new $class_name_nodash($options);
379
380
        if (!($element instanceof self)) {
381
            throw $this->exception(
382
                'You can add only classes based on AbstractObject'
383
            );
384
        }
385
386
        $element->owner = $this;
387
        $element->app = $this->app;
388
        $element->api = $this->app; // compatibility with ATK 4.2 and lower
0 ignored issues
show
Deprecated Code introduced by
The property AbstractObject::$api has been deprecated with message: 4.3.0 Left for compatibility with ATK 4.2 and lower, use ->app instead

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
389
        $element->name = $this->_shorten($this->name.'_'.$short_name);
390
        $element->short_name = $short_name;
391
392
        if (!$element->auto_track_element) {
393
            // dont store extra reference to models and controlers
394
            // for purposes of better garbage collection
395
            $this->elements[$short_name] = true;
396
        } else {
397
            $this->elements[$short_name] = $element;
398
        }
399
400
        // Initialize template before init() starts
401
        if ($element instanceof AbstractView) {
402
            $element->initializeTemplate($template_spot, $template_branch);
403
        }
404
405
        // Avoid using this hook. Agile Toolkit creates LOTS of objects,
406
        // so you'll get significantly slower code if you try to use this
407
        $this->app->hook('beforeObjectInit', array(&$element));
408
409
        // Initialize element
410
        $element->init();
411
412
        // Make sure init()'s parent was called. It's a popular coder's mistake.
413
        if (!$element->_initialized) {
414
            throw $element->exception(
415
                'You should call parent::init() when you override it'
416
            )
417
                ->addMoreInfo('object_name', $element->name)
418
                ->addMoreInfo('class', get_class($element));
419
        }
420
421
        // Great hook to affect children recursively
422
        $this->hook('afterAdd', array($element));
423
424
        return $element;
425
    }
426
427
    /**
428
     * Find child element by its short name. Use in chaining.
429
     * Exception if not found.
430
     *
431
     * @param string $short_name Short name of the child element
432
     *
433
     * @return AbstractObject
434
     */
435
    public function getElement($short_name)
436
    {
437
        if (!isset($this->elements[$short_name])) {
438
            throw $this->exception('Child element not found')
439
                ->addMoreInfo('element', $short_name);
440
        }
441
442
        return $this->elements[$short_name];
443
    }
444
445
    /**
446
     * Find child element. Use in condition.
447
     *
448
     * @param string $short_name Short name of the child element
449
     *
450
     * @return AbstractObject|bool
451
     */
452
    public function hasElement($short_name)
453
    {
454
        return isset($this->elements[$short_name])
455
            ? $this->elements[$short_name]
456
            : false;
457
    }
458
459
    /**
460
     * Names object accordingly. May not work on some objects.
461
     *
462
     * @param string $short_name Short name of the child element
463
     *
464
     * @return $this
465
     */
466
    public function rename($short_name)
467
    {
468
        unset($this->owner->elements[$this->short_name]);
469
        $this->name = $this->name.'_'.$short_name;
470
        $this->short_name = $short_name;
471
472
        if (!$this->auto_track_element) {
473
            $this->owner->elements[$short_name] = true;
474
        } else {
475
            $this->owner->elements[$short_name] = $this;
476
        }
477
478
        return $this;
479
    }
480
    // }}}
481
482
    // {{{ Model and Controller handling
483
    /**
484
     * Associate controller with the object.
485
     *
486
     * @param string|object $controller Class or instance of controller
487
     * @param string|array  $name       Name or property for new controller
488
     *
489
     * @return AbstractController Newly added controller
490
     */
491
    public function setController($controller, $name = null)
492
    {
493
        $controller = $this->app->normalizeClassName($controller, 'Controller');
494
495
        return $this->add($controller, $name);
496
    }
497
498
    /**
499
     * Associate model with object.
500
     *
501
     * @param string|object $model Class or instance of model
502
     *
503
     * @return AbstractModel Newly added Model
504
     */
505
    public function setModel($model)
506
    {
507
        $model = $this->app->normalizeClassName($model, 'Model');
508
        $this->model = $this->add($model);
509
510
        return $this->model;
511
    }
512
513
    /**
514
     * Return current model.
515
     *
516
     * @return AbstractModel Currently associated model object
517
     */
518
    public function getModel()
519
    {
520
        return $this->model;
521
    }
522
    // }}}
523
524
    // {{{ Session management: http://agiletoolkit.org/doc/session
525
    /**
526
     * Remember data in object-relevant session data.
527
     *
528
     * @param string $key   Key for the data
529
     * @param mixed  $value Value
530
     *
531
     * @return mixed $value
532
     */
533
    public function memorize($key, $value)
534
    {
535
        /** @type App_Web $this->app */
536
537
        if (!session_id()) {
538
            $this->app->initializeSession();
539
        }
540
541
        if ($value instanceof Model) {
542
            unset($_SESSION['o'][$this->name][$key]);
543
            $_SESSION['s'][$this->name][$key] = serialize($value);
544
545
            return $value;
546
        }
547
548
        unset($_SESSION['s'][$this->name][$key]);
549
        $_SESSION['o'][$this->name][$key] = $value;
550
551
        return $value;
552
    }
553
554
    /**
555
     * Similar to memorize, but if value for key exist, will return it.
556
     *
557
     * @param string $key     Data Key
558
     * @param mixed  $default Default value
559
     *
560
     * @return mixed Previously memorized data or $default
561
     */
562
    public function learn($key, $default = null)
563
    {
564
        /** @type App_Web $this->app */
565
566
        if (!session_id()) {
567
            $this->app->initializeSession(false);
568
        }
569
570
        if (!isset($_SESSION['o'][$this->name][$key])
571
            || is_null($_SESSION['o'][$this->name][$key])
572
        ) {
573
            if (is_callable($default)) {
574
                $default = call_user_func($default);
575
            }
576
577
            return $this->memorize($key, $default);
578
        } else {
579
            return $this->recall($key);
580
        }
581
    }
582
583
    /**
584
     * Forget session data for arg $key. If $key is omitted will forget all
585
     * associated session data.
586
     *
587
     * @param string $key Optional key of data to forget
588
     *
589
     * @return $this
590
     */
591
    public function forget($key = null)
592
    {
593
        /** @type App_Web $this->app */
594
595
        if (!session_id()) {
596
            $this->app->initializeSession(false);
597
        }
598
599
        // Prevent notice generation when using custom session handler
600
        if (!isset($_SESSION)) {
601
            return $this;
602
        }
603
604
        if (is_null($key)) {
605
            unset($_SESSION['o'][$this->name]);
606
            unset($_SESSION['s'][$this->name]);
607
        } else {
608
            unset($_SESSION['o'][$this->name][$key]);
609
            unset($_SESSION['s'][$this->name][$key]);
610
        }
611
612
        return $this;
613
    }
614
615
    /**
616
     * Returns session data for this object. If not previously set, then
617
     * $default is returned.
618
     *
619
     * @param string $key     Data Key
620
     * @param mixed  $default Default value
621
     *
622
     * @return mixed Previously memorized data or $default
623
     */
624
    public function recall($key, $default = null)
625
    {
626
        /** @type App_Web $this->app */
627
628
        if (!session_id()) {
629
            $this->app->initializeSession(false);
630
        }
631
632
        if (!isset($_SESSION['o'][$this->name][$key])
633
            || is_null($_SESSION['o'][$this->name][$key])
634
        ) {
635
            if (!isset($_SESSION['s'][$this->name][$key])) {
636
                return $default;
637
            }
638
            $v = $this->add(unserialize($_SESSION['s'][$this->name][$key]));
639
            $v->init();
640
641
            return $v;
642
        }
643
644
        return $_SESSION['o'][$this->name][$key];
645
    }
646
    // }}}
647
648
    // {{{ Exception handling: http://agiletoolkit.org/doc/exception
649
    /**
650
     * Returns relevant exception class. Use this method with "throw".
651
     *
652
     * @param string $message Static text of exception.
653
     * @param string $type    Exception class or class postfix
654
     * @param string $code    Optional error code
655
     *
656
     * @return BaseException
657
     */
658
    public function exception($message = 'Undefined Exception', $type = null, $code = null)
659
    {
660
        if ($type === null) {
661
            $type = $this->default_exception;
662
        } elseif ($type[0] == '_') {
663
            if ($this->default_exception == 'BaseException') {
664
                $type = 'Exception_'.substr($type, 1);
665
            } else {
666
                $type = $this->default_exception.'_'.substr($type, 1);
667
            }
668
        } elseif ($type != 'BaseException') {
669
            $type = $this->app->normalizeClassName($type, 'Exception');
670
        }
671
672
        // Localization support
673
        $message = $this->app->_($message);
674
675
        if ($type == 'Exception') {
676
            $type = 'BaseException';
677
        }
678
679
        $e = new $type($message, $code);
680
        if (!($e instanceof BaseException)) {
681
            throw $e;
682
        }
683
        $e->owner = $this;
684
        $e->app = $this->app;
685
        $e->api = $this->app; // compatibility with ATK 4.2 and lower
0 ignored issues
show
Deprecated Code introduced by
The property BaseException::$api has been deprecated with message: 4.3.0 Left for compatibility with ATK 4.2 and lower, use ->app instead

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
686
        $e->init();
687
688
        return $e;
689
    }
690
    // }}}
691
692
    // {{{ Code which can be potentially obsoleted.
693
    /**
694
     * Reports fatal error. Use ->exception instead.
695
     *
696
     * @param string $error error text
697
     * @param int    $shift relative offset in backtrace
698
     *
699
     * @obsolete
700
     */
701
    public function fatal($error, $shift = 0)
702
    {
703
        return $this->upCall(
704
            'outputFatal',
705
            array(
706
                $error,
707
                $shift,
708
            )
709
        );
710
    }
711
712
    /**
713
     * Records debug information.
714
     *
715
     * @param string $msg information
716
     *
717
     * @obsolete
718
     */
719
    public $_info = array();
720
    public function info($msg)
721
    {
722
        /*
723
         * Call this function to send some information to Application. Example:
724
         *
725
         * $this->info("User tried buying traffic without enough money in bank");
726
         */
727
        $args = func_get_args();
728
        array_shift($args);
729
        $this->_info[] = vsprintf($msg, $args);
730
    }
731
732
    /**
733
     * Turns on debug mode for this object.
734
     * Using first argument as string is obsolete.
735
     *
736
     * @param bool|string $msg  "true" to start debugging
737
     * @param string      $file obsolete
738
     * @param string      $line obsolete
739
     */
740
    public function debug($msg = true, $file = null, $line = null)
741
    {
742
        if (is_bool($msg)) {
743
            $this->debug = $msg;
744
745
            return $this;
746
        }
747
748
        if (is_object($msg)) {
749
            throw $this->exception('Do not debug objects');
750
        }
751
752
        // The rest of this method is obsolete
753 View Code Duplication
        if ((isset($this->debug) && $this->debug)
754
            || (isset($this->app->debug) && $this->app->debug)
755
        ) {
756
            $this->app->outputDebug($this, $msg, $file/*, $line*/);
0 ignored issues
show
Deprecated Code introduced by
The method App_CLI::outputDebug() has been deprecated with message: 4.3.2

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
757
        }
758
    }
759
760
    /**
761
     * Records warning.
762
     *
763
     * @param string $msg   information
764
     * @param int    $shift relative offset in backtrace
765
     *
766
     * @obsolete
767
     */
768
    public function warning($msg, $shift = 0)
769
    {
770
        $this->upCall(
771
            'outputWarning',
772
            array(
773
                $msg,
774
                $shift,
775
            )
776
        );
777
    }
778
779
    /**
780
     * Call specified method for this class and all parents up to app.
781
     *
782
     * @param string $type information
783
     * @param array  $args relative offset in backtrace
784
     *
785
     * @obsolete
786
     */
787
    public function upCall($type, $args = array())
788
    {
789
        /*
790
         * Try to handle something on our own and in case we are not able,
791
         * pass to parent. Such as messages, notifications and request for
792
         * additional info or descriptions are passed this way.
793
         */
794
        if (method_exists($this, $type)) {
795
            return call_user_func_array(
796
                array(
797
                    $this,
798
                    $type,
799
                ),
800
                $args
801
            );
802
        }
803
        if (!$this->owner) {
804
            return false;
805
        }
806
807
        return $this->owner->upCall($type, $args);
808
    }
809
    // }}}
810
811
    // {{{ Hooks: http://agiletoolkit.org/doc/hooks
812
    public $hooks = array();
813
814
    /**
815
     * If priority is negative, then hooks will be executed in reverse order.
816
     *
817
     * @param string                  $hook_spot Hook identifier to bind on
818
     * @param AbstractObject|callable $callable  Will be called on hook()
819
     * @param array                   $arguments Arguments are passed to $callable
820
     * @param int                     $priority  Lower priority is called sooner
821
     *
822
     * @return $this
823
     */
824
    public function addHook($hook_spot, $callable, $arguments = array(), $priority = 5)
825
    {
826
        if (!is_array($arguments)) {
827
            throw $this->exception('Incorrect arguments');
828
        }
829
        if (is_string($hook_spot) && strpos($hook_spot, ',') !== false) {
830
            $hook_spot = explode(',', $hook_spot);
831
        }
832
        if (is_array($hook_spot)) {
833
            foreach ($hook_spot as $h) {
834
                $this->addHook($h, $callable, $arguments, $priority);
835
            }
836
837
            return $this;
838
        }
839
        if (!is_callable($callable)
840
            && ($callable instanceof self
841
            && !$callable->hasMethod($hook_spot))
842
        ) {
843
            throw $this->exception('Hook does not exist');
844
        }
845 View Code Duplication
        if (is_object($callable) && !is_callable($callable)) {
846
            $callable = array($callable, $hook_spot);
847
            // short for addHook('test', $this); to call $this->test();
848
        }
849
850
        $this->hooks[$hook_spot][$priority][] = array($callable, $arguments);
851
852
        return $this;
853
    }
854
855
    /**
856
     * Delete all hooks for specified spot.
857
     *
858
     * @param string $hook_spot Hook identifier to bind on
859
     *
860
     * @return $this
861
     */
862
    public function removeHook($hook_spot)
863
    {
864
        unset($this->hooks[$hook_spot]);
865
866
        return $this;
867
    }
868
869
    /**
870
     * Execute all callables assigned to $hook_spot.
871
     *
872
     * @param string $hook_spot Hook identifier
873
     * @param array  $arg       Additional arguments to callables
874
     *
875
     * @return mixed Array of responses or value specified to breakHook
876
     */
877
    public function hook($hook_spot, $arg = array())
878
    {
879
        if (!is_array($arg)) {
880
            throw $this->exception(
881
                'Incorrect arguments, or hook does not exist'
882
            );
883
        }
884
        $return = array();
885
        if ($arg === UNDEFINED) {
886
            $arg = array();
887
        }
888
889
        if (isset($this->hooks[$hook_spot])) {
890
            if (is_array($this->hooks[$hook_spot])) {
891
                krsort($this->hooks[$hook_spot]); // lower priority is called sooner
892
                $hook_backup = $this->hooks[$hook_spot];
893
894
                try {
895
                    while ($_data = array_pop($this->hooks[$hook_spot])) {
896
                        foreach ($_data as $prio => &$data) {
897
898
                            // Our extension
899
                            if (is_string($data[0])
900
                                && !preg_match(
901
                                    '/^[a-zA-Z_][a-zA-Z0-9_]*$/',
902
                                    $data[0]
903
                                )
904
                            ) {
905
                                $result = eval($data[0]);
906
                            } elseif (is_callable($data[0])) {
907
                                $result = call_user_func_array(
908
                                    $data[0],
909
                                    array_merge(
910
                                        array($this),
911
                                        $arg,
912
                                        $data[1]
913
                                    )
914
                                );
915
                            } else {
916
                                if (!is_array($data[0])) {
917
                                    $data[0] = array(
918
                                        'STATIC',
919
                                        $data[0],
920
                                    );
921
                                }
922
                                throw $this->exception(
923
                                    'Cannot call hook. Function might not exist.'
924
                                )
925
                                    ->addMoreInfo('hook', $hook_spot)
926
                                    ->addMoreInfo('arg1', $data[0][0])
927
                                    ->addMoreInfo('arg2', $data[0][1]);
928
                            }
929
                            $return[] = $result;
930
                        }
931
                    }
932
933
                } catch (Exception_Hook $e) {
934
                    /** @type Exception_Hook $e */
935
                    $this->hooks[$hook_spot] = $hook_backup;
936
937
                    return $e->return_value;
938
                }
939
940
                $this->hooks[$hook_spot] = $hook_backup;
941
            }
942
        }
943
944
        return $return;
945
    }
946
947
    /**
948
     * When called from inside a hook callable, will stop execution of other
949
     * callables on same hook. The passed argument will be returned by the
950
     * hook method.
951
     *
952
     * @param mixed $return What would hook() return?
953
     */
954
    public function breakHook($return)
955
    {
956
        /** @type Exception_Hook $e */
957
        $e = $this->exception(null, 'Hook');
958
        $e->return_value = $return;
959
        throw $e;
960
    }
961
    // }}}
962
963
    // {{{ Dynamic Methods: http://agiletoolkit.org/learn/dynamic
964
    /**
965
     * Call method is used to display exception for non-existant methods and
966
     * provides ability to extend objects with addMethod().
967
     *
968
     * @param string $method    Name of the method
969
     * @param array  $arguments Arguments
970
     *
971
     * @return mixed
972
     */
973
    public function __call($method, $arguments)
974
    {
975
        if (($ret = $this->tryCall($method, $arguments))) {
976
            return $ret[0];
977
        }
978
        throw $this->exception(
979
            'Method is not defined for this object',
980
            'Logic'
981
        )
982
            ->addMoreInfo('class', get_class($this))
983
            ->addMoreInfo('method', $method)
984
            ->addMoreInfo('arguments', var_export($arguments, true));
985
    }
986
987
    /**
988
     * Attempts to call dynamic method. Returns array containing result or false.
989
     *
990
     * @param string $method    Name of the method
991
     * @param array  $arguments Arguments
992
     *
993
     * @return mixed
994
     */
995
    public function tryCall($method, $arguments)
996
    {
997
        if ($ret = $this->hook('method-'.$method, $arguments)) {
998
            return $ret;
999
        }
1000
        array_unshift($arguments, $this);
1001
        if (($ret = $this->app->hook('global-method-'.$method, $arguments))) {
1002
            return $ret;
1003
        }
1004
    }
1005
1006
    /**
1007
     * Add new method for this object.
1008
     *
1009
     * @param string|array $name     Name of new method of $this object
1010
     * @param callable     $callable Callback
1011
     *
1012
     * @return $this
1013
     */
1014
    public function addMethod($name, $callable)
1015
    {
1016 View Code Duplication
        if (is_string($name) && strpos($name, ',') !== false) {
1017
            $name = explode(',', $name);
1018
        }
1019
        if (is_array($name)) {
1020
            foreach ($name as $h) {
1021
                $this->addMethod($h, $callable);
1022
            }
1023
1024
            return $this;
1025
        }
1026 View Code Duplication
        if (is_object($callable) && !is_callable($callable)) {
1027
            $callable = array($callable, $name);
1028
        }
1029
        if ($this->hasMethod($name)) {
1030
            throw $this->exception('Registering method twice');
1031
        }
1032
        $this->addHook('method-'.$name, $callable);
1033
1034
        return $this;
1035
    }
1036
1037
    /**
1038
     * Return if this object has specified method (either native or dynamic).
1039
     *
1040
     * @param string $name Name of the method
1041
     *
1042
     * @return bool
1043
     */
1044
    public function hasMethod($name)
1045
    {
1046
        return method_exists($this, $name)
1047
            || isset($this->hooks['method-'.$name])
1048
            || isset($this->app->hooks['global-method-'.$name]);
1049
    }
1050
1051
    /**
1052
     * Remove dynamically registered method.
1053
     *
1054
     * @param string $name Name of the method
1055
     *
1056
     * @return $this
1057
     */
1058
    public function removeMethod($name)
1059
    {
1060
        $this->removeHook('method-'.$name);
1061
1062
        return $this;
1063
    }
1064
    // }}}
1065
1066
    // {{{ Logger: to be moved out
1067
    /**
1068
     * Output string into log file.
1069
     *
1070
     * @param string $var var
1071
     * @param string $msg msg
1072
     *
1073
     * @obsolete
1074
     */
1075
    public function logVar($var, $msg = '')
1076
    {
1077
        $this->app->getLogger()->logVar($var, $msg);
1078
    }
1079
1080
    /**
1081
     * Output string into info file.
1082
     *
1083
     * @param string $info info
1084
     * @param string $msg  msg
1085
     *
1086
     * @obsolete
1087
     */
1088
    public function logInfo($info, $msg = '')
1089
    {
1090
        $this->app->getLogger()->logLine($msg.' '.$info."\n");
1091
    }
1092
1093
    /**
1094
     * Output string into error file.
1095
     *
1096
     * @param string $error error
1097
     * @param string $msg   msg
1098
     *
1099
     * @obsolete
1100
     */
1101
    public function logError($error, $msg = '')
1102
    {
1103
        if (is_object($error)) {
1104
            // we got exception object obviously
1105
            $error = $error->getMessage();
1106
        }
1107
        $this->app->getLogger()->logLine($msg.' '.$error."\n", null, 'error');
1108
    }
1109
    // }}}
1110
1111
    /**
1112
     * A handy shortcut for foreach(){ .. } code. Make your callable return
1113
     * "false" if you would like to break the loop.
1114
     *
1115
     * @param string|callable $callable will be executed for each member
1116
     *
1117
     * @return $this
1118
     */
1119
    public function each($callable)
1120
    {
1121
        if ($this instanceof Iterator) {
1122
1123
            if (is_string($callable)) {
1124
                foreach ($this as $obj) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<AbstractObject> is not traversable.
Loading history...
1125
                    if ($obj->$callable() === false) {
1126
                        break;
1127
                    }
1128
                }
1129
1130
                return $this;
1131
            }
1132
1133
            foreach ($this as $obj) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<AbstractObject> is not traversable.
Loading history...
1134
                if (call_user_func($callable, $obj) === false) {
1135
                    break;
1136
                }
1137
            }
1138
1139
        } else {
1140
            throw $this->exception('Calling each() on non-iterative object');
1141
        }
1142
1143
        return $this;
1144
    }
1145
1146
    /**
1147
     * This method will find private methods started with test_ in
1148
     * the current class and will execute each method in succession
1149
     * by passing $t argument to it. Before each test execution
1150
     * takes place, $t->prepareForTest($test) will be called. It must
1151
     * return non-false for test to be carried out.
1152
     *
1153
     * $test will be an array containing keys for 'name', 'object' and
1154
     * 'class'
1155
     *
1156
     * @param Tester $tester
1157
     */
1158
    public function runTests(Tester $tester = null)
1159
    {
1160
        $test = array('object' => $this->name, 'class' => get_class($this));
1161
1162
        foreach (get_class_methods($this) as $method) {
1163
            if (strpos($method, 'test_') === 0) {
1164
                $test['name'] = substr($method, 5);
1165
            } else {
1166
                continue;
1167
            }
1168
1169
            if ($tester && $tester->prepareForTest($test) === false) {
1170
                continue;
1171
            }
1172
1173
            if ($tester !== null) {
1174
                /** @type Model $r */
1175
                $r = $tester->results;
1176
                $r->unload();
1177
                $r->set($test);
1178
            }
1179
1180
            // Proceed with test
1181
            $me = memory_get_peak_usage();
1182
            $ms = microtime(true);
1183
            $this->_ticks = 0;
1184
            declare (ticks = 1);
1185
            register_tick_function(array($this, '_ticker'));
1186
1187
            // Execute here
1188
            try {
1189
                $result = $this->$method($tester);
1190
            } catch (Exception $e) {
1191
                unregister_tick_function(array($this, '_ticker'));
1192
                $time = microtime(true) - $ms;
1193
                $memory = (memory_get_peak_usage()) - $me;
1194
                $ticks = $this->_ticks;
1195
1196
                if ($e instanceof Exception_SkipTests) {
1197
                    if ($tester !== null) {
1198
                        $r['exception'] = 'SKIPPED';
0 ignored issues
show
Bug introduced by
The variable $r does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1199
                        $r->saveAndUnload();
1200
                    }
1201
1202
                    return array(
1203
                        'skipped' => $e->getMessage(),
1204
                        );
1205
                }
1206
1207
                if ($tester !== null) {
1208
                    $r['time'] = $time;
1209
                    $r['memory'] = $memory;
1210
                    $r['ticks'] = $ticks;
1211
                    $r['exception'] = $e;
1212
                    $r->saveAndUnload();
1213
                }
1214
1215
                continue;
1216
            }
1217
1218
            // Unregister
1219
            unregister_tick_function(array($this, '_ticker'));
1220
            $time = microtime(true) - $ms;
1221
            $memory = (memory_get_peak_usage()) - $me;
1222
            $ticks = $this->_ticks - 3;     // there are always minimum of 3 ticks
1223
1224
            if ($tester !== null) {
1225
                $r['time'] = $time;
1226
                $r['memory'] = $memory;
1227
                $r['ticks'] = $ticks;
1228
                $r['is_success'] = true;
1229
                $r['result'] = $result;
1230
                $r->saveAndUnload();
1231
            }
1232
        }
1233
    }
1234
1235
    private $_ticks;
1236
    public function _ticker()
1237
    {
1238
        ++$this->_ticks;
1239
    }
1240
1241
    /**
1242
     * Method used internally for shortening object names.
1243
     *
1244
     * @param string $desired Desired name of new object.
1245
     *
1246
     * @return string Shortened name of new object.
1247
     */
1248
    public function _shorten($desired)
1249
    {
1250
        if (strlen($desired) > $this->app->max_name_length
1251
            && $this->app->max_name_length !== false
1252
        ) {
1253
            $len = $this->app->max_name_length - 10;
1254
            if ($len < 5) {
1255
                $len = $this->app->max_name_length;
1256
            }
1257
1258
            $key = substr($desired, 0, $len);
1259
            $rest = substr($desired, $len);
1260
1261
            if (!$this->app->unique_hashes[$key]) {
1262
                $this->app->unique_hashes[$key] = dechex(crc32($key));
1263
            }
1264
            $desired = $this->app->unique_hashes[$key].'__'.$rest;
1265
        };
1266
1267
        return $desired;
1268
    }
1269
1270
    private $_element_name_counts = array();
1271
    public function _unique_element($desired = null)
1272
    {
1273
        $postfix = @++$this->_element_name_counts[$desired];
1274
1275
        return $desired.($postfix > 1 ? ('_'.$postfix) : '');
1276
    }
1277
1278
    /**
1279
     * This funcion given the associative $array and desired new key will return
1280
     * the best matching key which is not yet in the array.
1281
     * For example, if you have array('foo'=>x,'bar'=>x) and $desired is 'foo'
1282
     * function will return 'foo_2'. If 'foo_2' key also exists in that array,
1283
     * then 'foo_3' is returned and so on.
1284
     *
1285
     * @param array  &$array  Reference to array which stores key=>value pairs
1286
     * @param string $desired Desired key for new object
1287
     *
1288
     * @return string unique key for new object
1289
     */
1290
    public function _unique(&$array, $desired = null)
1291
    {
1292
        if (!is_array($array)) {
1293
            throw $this->exception('not array');
1294
        }
1295
        $postfix = count($array);
1296
        $attempted_key = $desired;
1297
        while (array_key_exists($attempted_key, $array)) {
1298
            // already used, move on
1299
            $attempted_key = ($desired ?: 'undef').'_'.(++$postfix);
1300
        }
1301
1302
        return $attempted_key;
1303
    }
1304
1305
    /**
1306
     * Always call parent if you redefine this/.
1307
     */
1308
    public function __destruct()
1309
    {
1310
    }
1311
1312
    /**
1313
     * Do not serialize objects.
1314
     *
1315
     * @return mixed
1316
     */
1317
    public function __sleep()
1318
    {
1319
        return array('name');
1320
    }
1321
}
1322