Completed
Push — master ( 0eb774...254a9f )
by Romans
02:34 queued 02:29
created

AbstractObject::addMethod()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 22
Code Lines 13

Duplication

Lines 6
Ratio 27.27 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 6
loc 22
rs 6.6037
cc 8
eloc 13
nc 12
nop 2
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 AbstractObject
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->api = // 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...
308
                    $class->app = $this->app;
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
                $class->owner->elements[$class->short_name] = true;
316
            }
317
            $class->owner = $this;
318
            if ($class instanceof AbstractView && !$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...
319
                $class->initializeTemplate($template_spot, $template_branch);
320
            }
321
322
            return $class;
323
        }
324
325
        if (!is_string($class) || !$class) {
326
            throw $this->exception('Class is not valid')
327
                ->addMoreInfo('class', $class);
328
        }
329
330
        $class = str_replace('/', '\\', $class);
331
332
        if ($class[0] == '.') {
333
            // Relative class name specified, extract current namespace
334
            // and make new class name relative to this namespace
335
            $ns = get_class($this);
336
            $ns = substr($ns, 0, strrpos($ns, '\\'));
337
            $class = $ns.'\\'.substr($class, 2);
338
        }
339
340
        $short_name = isset($options['name'])
341
            ? $options['name']
342
            : str_replace('\\', '_', strtolower($class));
343
344
        // Adding same controller twice will return existing one
345
        if (isset($this->elements[$short_name])) {
346
            if ($this->elements[$short_name] instanceof AbstractController) {
347
                return $this->elements[$short_name];
348
            }
349
        }
350
351
        $short_name = $this->_unique_element($short_name);
352
353
        if (isset($this->elements[$short_name])) {
354
            throw $this->exception($class.' with requested name already exists')
355
                ->addMoreInfo('class', $class)
356
                ->addMoreInfo('new_short_name', $short_name)
357
                ->addMoreInfo('object', $this)
358
                ->addMoreInfo('counts', json_encode($this->_element_name_counts))
359
                ->addThis($this);
360
        }
361
362
        $class_name_nodash = str_replace('-', '', $class);
363
        /*
364
         * Even though this might break some applications,
365
         * your loading must be configured properly instead
366
         * of relying on this
367
         *
368
        if (!class_exists($class_name_nodash, false)
369
            && isset($this->app->pathfinder)
370
        ) {
371
            $this->app->pathfinder->loadClass($class);
372
        }*/
373
        $element = new $class_name_nodash($options);
374
375
        if (!($element instanceof self)) {
376
            throw $this->exception(
377
                'You can add only classes based on AbstractObject'
378
            );
379
        }
380
381
        $element->owner = $this;
382
        $element->api = // 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...
383
            $element->app = $this->app;
384
        $element->name = $this->_shorten($this->name.'_'.$short_name);
385
        $element->short_name = $short_name;
386
387
        if (!$element->auto_track_element) {
388
            // dont store extra reference to models and controlers
389
            // for purposes of better garbage collection
390
            $this->elements[$short_name] = true;
391
        } else {
392
            $this->elements[$short_name] = $element;
393
        }
394
395
        // Initialize template before init() starts
396
        if ($element instanceof AbstractView) {
397
            $element->initializeTemplate($template_spot, $template_branch);
398
        }
399
400
        // Avoid using this hook. Agile Toolkit creates LOTS of objects,
401
        // so you'll get significantly slower code if you try to use this
402
        $this->app->hook('beforeObjectInit', array(&$element));
403
404
        // Initialize element
405
        $element->init();
406
407
        // Make sure init()'s parent was called. It's a popular coder's mistake.
408
        if (!$element->_initialized) {
409
            throw $element->exception(
410
                'You should call parent::init() when you override it'
411
            )
412
                ->addMoreInfo('object_name', $element->name)
413
                ->addMoreInfo('class', get_class($element));
414
        }
415
416
        // Great hook to affect children recursively
417
        $this->hook('afterAdd', array($element));
418
419
        return $element;
420
    }
421
422
    /**
423
     * Find child element by its short name. Use in chaining.
424
     * Exception if not found.
425
     *
426
     * @param string $short_name Short name of the child element
427
     *
428
     * @return AbstractObject
429
     */
430
    public function getElement($short_name)
431
    {
432
        if (!isset($this->elements[$short_name])) {
433
            throw $this->exception('Child element not found')
434
                ->addMoreInfo('element', $short_name);
435
        }
436
437
        return $this->elements[$short_name];
438
    }
439
440
    /**
441
     * Find child element. Use in condition.
442
     *
443
     * @param string $short_name Short name of the child element
444
     *
445
     * @return AbstractObject|bool
446
     */
447
    public function hasElement($short_name)
448
    {
449
        return isset($this->elements[$short_name])
450
            ? $this->elements[$short_name]
451
            : false;
452
    }
453
454
    /**
455
     * Names object accordingly. May not work on some objects.
456
     *
457
     * @param string $short_name Short name of the child element
458
     *
459
     * @return $this
460
     */
461
    public function rename($short_name)
462
    {
463
        unset($this->owner->elements[$this->short_name]);
464
        $this->name = $this->name.'_'.$short_name;
465
        $this->short_name = $short_name;
466
467
        if (!$this->auto_track_element) {
468
            $this->owner->elements[$short_name] = true;
469
        } else {
470
            $this->owner->elements[$short_name] = $this;
471
        }
472
473
        return $this;
474
    }
475
    // }}}
476
477
    // {{{ Model and Controller handling
478
    /**
479
     * Associate controller with the object.
480
     *
481
     * @param string|object $controller Class or instance of controller
482
     * @param string|array  $name       Name or property for new controller
483
     *
484
     * @return AbstractController Newly added controller
485
     */
486
    public function setController($controller, $name = null)
487
    {
488
        $controller = $this->app->normalizeClassName($controller, 'Controller');
489
490
        return $this->add($controller, $name);
491
    }
492
493
    /**
494
     * Associate model with object.
495
     *
496
     * @param string|object $model Class or instance of model
497
     *
498
     * @return AbstractModel Newly added Model
499
     */
500
    public function setModel($model)
501
    {
502
        $model = $this->app->normalizeClassName($model, 'Model');
503
        $this->model = $this->add($model);
504
505
        return $this->model;
506
    }
507
508
    /**
509
     * Return current model.
510
     *
511
     * @return AbstractModel Currently associated model object
512
     */
513
    public function getModel()
514
    {
515
        return $this->model;
516
    }
517
    // }}}
518
519
    // {{{ Session management: http://agiletoolkit.org/doc/session
520
    /**
521
     * Remember data in object-relevant session data.
522
     *
523
     * @param string $key   Key for the data
524
     * @param mixed  $value Value
525
     *
526
     * @return mixed $value
527
     */
528
    public function memorize($key, $value)
529
    {
530
        if (!session_id()) {
531
            $this->app->initializeSession();
0 ignored issues
show
Documentation Bug introduced by
The method initializeSession does not exist on object<App_CLI>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
532
        }
533
534
        if ($value instanceof Model) {
535
            unset($_SESSION['o'][$this->name][$key]);
536
            $_SESSION['s'][$this->name][$key] = serialize($value);
537
538
            return $value;
539
        }
540
541
        unset($_SESSION['s'][$this->name][$key]);
542
        $_SESSION['o'][$this->name][$key] = $value;
543
544
        return $value;
545
    }
546
547
    /**
548
     * Similar to memorize, but if value for key exist, will return it.
549
     *
550
     * @param string $key     Data Key
551
     * @param mixed  $default Default value
552
     *
553
     * @return mixed Previously memorized data or $default
554
     */
555
    public function learn($key, $default = null)
556
    {
557
        if (!session_id()) {
558
            $this->app->initializeSession(false);
0 ignored issues
show
Documentation Bug introduced by
The method initializeSession does not exist on object<App_CLI>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
559
        }
560
561
        if (!isset($_SESSION['o'][$this->name][$key])
562
            || is_null($_SESSION['o'][$this->name][$key])
563
        ) {
564
            if (is_callable($default)) {
565
                $default = call_user_func($default);
566
            }
567
568
            return $this->memorize($key, $default);
569
        } else {
570
            return $this->recall($key);
571
        }
572
    }
573
574
    /**
575
     * Forget session data for arg $key. If $key is omitted will forget all
576
     * associated session data.
577
     *
578
     * @param string $key Optional key of data to forget
579
     *
580
     * @return $this
581
     */
582
    public function forget($key = null)
583
    {
584
        if (!session_id()) {
585
            $this->app->initializeSession(false);
0 ignored issues
show
Documentation Bug introduced by
The method initializeSession does not exist on object<App_CLI>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
586
        }
587
588
        // Prevent notice generation when using custom session handler
589
        if (!isset($_SESSION)) {
590
            return $this;
591
        }
592
593
        if (is_null($key)) {
594
            unset($_SESSION['o'][$this->name]);
595
            unset($_SESSION['s'][$this->name]);
596
        } else {
597
            unset($_SESSION['o'][$this->name][$key]);
598
            unset($_SESSION['s'][$this->name][$key]);
599
        }
600
601
        return $this;
602
    }
603
604
    /**
605
     * Returns session data for this object. If not previously set, then
606
     * $default is returned.
607
     *
608
     * @param string $key     Data Key
609
     * @param mixed  $default Default value
610
     *
611
     * @return mixed Previously memorized data or $default
612
     */
613
    public function recall($key, $default = null)
614
    {
615
        if (!session_id()) {
616
            $this->app->initializeSession(false);
0 ignored issues
show
Documentation Bug introduced by
The method initializeSession does not exist on object<App_CLI>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
617
        }
618
619
        if (!isset($_SESSION['o'][$this->name][$key])
620
            || is_null($_SESSION['o'][$this->name][$key])
621
        ) {
622
            if (!isset($_SESSION['s'][$this->name][$key])) {
623
                return $default;
624
            }
625
            $v = $this->add(unserialize($_SESSION['s'][$this->name][$key]));
626
            $v->init();
627
628
            return $v;
629
        }
630
631
        return $_SESSION['o'][$this->name][$key];
632
    }
633
    // }}}
634
635
    // {{{ Exception handling: http://agiletoolkit.org/doc/exception
636
    /**
637
     * Returns relevant exception class. Use this method with "throw".
638
     *
639
     * @param string $message Static text of exception.
640
     * @param string $type    Exception class or class postfix
641
     * @param string $code    Optional error code
642
     *
643
     * @return BaseException
644
     */
645
    public function exception($message = 'Undefined Exception', $type = null, $code = null)
646
    {
647
        if ($type === null) {
648
            $type = $this->default_exception;
649
        } elseif ($type[0] == '_') {
650
            if ($this->default_exception == 'BaseException') {
651
                $type = 'Exception_'.substr($type, 1);
652
            } else {
653
                $type = $this->default_exception.'_'.substr($type, 1);
654
            }
655
        } elseif ($type != 'BaseException') {
656
            $type = $this->app->normalizeClassName($type, 'Exception');
657
        }
658
659
        // Localization support
660
        $message = $this->app->_($message);
661
662
        if ($type == 'Exception') {
663
            $type = 'BaseException';
664
        }
665
666
        $e = new $type($message, $code);
667
        if (!($e instanceof BaseException)) {
668
            throw $e;
669
        }
670
        $e->owner = $this;
671
        $e->app = $this->app;
672
        $e->api = $e->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...
673
        $e->init();
674
675
        return $e;
676
    }
677
    // }}}
678
679
    // {{{ Code which can be potentially obsoleted.
680
    /**
681
     * Reports fatal error. Use ->exception instead.
682
     *
683
     * @param string $error error text
684
     * @param int    $shift relative offset in backtrace
685
     *
686
     * @obsolete
687
     */
688
    public function fatal($error, $shift = 0)
689
    {
690
        return $this->upCall(
691
            'outputFatal',
692
            array(
693
                $error,
694
                $shift,
695
            )
696
        );
697
    }
698
699
    /**
700
     * Records debug information.
701
     *
702
     * @param string $msg information
703
     *
704
     * @obsolete
705
     */
706
    public $_info = array();
707
    public function info($msg)
708
    {
709
        /*
710
         * Call this function to send some information to Application. Example:
711
         *
712
         * $this->info("User tried buying traffic without enough money in bank");
713
         */
714
        $args = func_get_args();
715
        array_shift($args);
716
        $this->_info[] = vsprintf($msg, $args);
717
    }
718
719
    /**
720
     * Turns on debug mode for this object. Using first argument as string
721
     * is obsolete.
722
     *
723
     * @param bool|string $msg  "true" to start debugging
724
     * @param string      $file obsolete
725
     * @param string      $line obsolete
726
     */
727
    public function debug($msg = true, $file = null, $line = null)
728
    {
729
        if (is_bool($msg)) {
730
            $this->debug = $msg;
731
732
            return $this;
733
        }
734
735
        if (is_object($msg)) {
736
            throw $this->exception('Do not debug objects');
737
        }
738
739
        // The rest of this method is obsolete
740 View Code Duplication
        if ((isset($this->debug) && $this->debug)
741
            || (isset($this->app->debug) && $this->app->debug)
742
        ) {
743
            $this->app->outputDebug($this, $msg, $file, $line);
0 ignored issues
show
Unused Code introduced by
The call to App_CLI::outputDebug() has too many arguments starting with $line.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
744
        }
745
    }
746
747
    /**
748
     * Records warning.
749
     *
750
     * @param string $msg   information
751
     * @param int    $shift relative offset in backtrace
752
     *
753
     * @obsolete
754
     */
755
    public function warning($msg, $shift = 0)
756
    {
757
        $this->upCall(
758
            'outputWarning',
759
            array(
760
                $msg,
761
                $shift,
762
            )
763
        );
764
    }
765
766
    /**
767
     * Call specified method for this class and all parents up to app.
768
     *
769
     * @param string $type information
770
     * @param array  $args relative offset in backtrace
771
     *
772
     * @obsolete
773
     */
774
    public function upCall($type, $args = array())
775
    {
776
        /*
777
         * Try to handle something on our own and in case we are not able,
778
         * pass to parent. Such as messages, notifications and request for
779
         * additional info or descriptions are passed this way.
780
         */
781
        if (method_exists($this, $type)) {
782
            return call_user_func_array(
783
                array(
784
                    $this,
785
                    $type,
786
                ),
787
                $args
788
            );
789
        }
790
        if (!$this->owner) {
791
            return false;
792
        }
793
794
        return $this->owner->upCall($type, $args);
795
    }
796
    // }}}
797
798
    // {{{ Hooks: http://agiletoolkit.org/doc/hooks
799
    public $hooks = array();
800
801
    /**
802
     * If priority is negative, then hooks will be executed in reverse order.
803
     *
804
     * @param string                  $hook_spot Hook identifier to bind on
805
     * @param AbstractObject|callable $callable  Will be called on hook()
806
     * @param array                   $arguments Arguments are passed to $callable
807
     * @param int                     $priority  Lower priority is called sooner
808
     *
809
     * @return $this
810
     */
811
    public function addHook($hook_spot, $callable, $arguments = array(), $priority = 5)
812
    {
813
        if (!is_array($arguments)) {
814
            throw $this->exception('Incorrect arguments');
815
        }
816
        if (is_string($hook_spot) && strpos($hook_spot, ',') !== false) {
817
            $hook_spot = explode(',', $hook_spot);
818
        }
819
        if (is_array($hook_spot)) {
820
            foreach ($hook_spot as $h) {
821
                $this->addHook($h, $callable, $arguments, $priority);
822
            }
823
824
            return $this;
825
        }
826
        if (!is_callable($callable)
827
            && ($callable instanceof self
828
            && !$callable->hasMethod($hook_spot))
829
        ) {
830
            throw $this->exception('Hook does not exist');
831
        }
832 View Code Duplication
        if (is_object($callable) && !is_callable($callable)) {
833
            $callable = array($callable, $hook_spot);
834
            // short for addHook('test', $this); to call $this->test();
835
        }
836
837
        $this->hooks[$hook_spot][$priority][] = array($callable, $arguments);
838
839
        return $this;
840
    }
841
842
    /**
843
     * Delete all hooks for specified spot.
844
     *
845
     * @param string $hook_spot Hook identifier to bind on
846
     *
847
     * @return $this
848
     */
849
    public function removeHook($hook_spot)
850
    {
851
        unset($this->hooks[$hook_spot]);
852
853
        return $this;
854
    }
855
856
    /**
857
     * Execute all callables assigned to $hook_spot.
858
     *
859
     * @param string $hook_spot Hook identifier
860
     * @param array  $arg       Additional arguments to callables
861
     *
862
     * @return mixed Array of responses or value specified to breakHook
863
     */
864
    public function hook($hook_spot, $arg = array())
865
    {
866
        if (!is_array($arg)) {
867
            throw $this->exception(
868
                'Incorrect arguments, or hook does not exist'
869
            );
870
        }
871
        $return = array();
872
        if ($arg === UNDEFINED) {
873
            $arg = array();
874
        }
875
876
        try {
877
            if (isset($this->hooks[$hook_spot])) {
878
                if (is_array($this->hooks[$hook_spot])) {
879
                    krsort($this->hooks[$hook_spot]); // lower priority is called sooner
880
                    $hook_backup = $this->hooks[$hook_spot];
881
                    while ($_data = array_pop($this->hooks[$hook_spot])) {
882
                        foreach ($_data as $prio => &$data) {
883
884
                            // Our extension
885
                            if (is_string($data[0])
886
                                && !preg_match(
887
                                    '/^[a-zA-Z_][a-zA-Z0-9_]*$/',
888
                                    $data[0]
889
                                )
890
                            ) {
891
                                $result = eval($data[0]);
892
                            } elseif (is_callable($data[0])) {
893
                                $result = call_user_func_array(
894
                                    $data[0],
895
                                    array_merge(
896
                                        array($this),
897
                                        $arg,
898
                                        $data[1]
899
                                    )
900
                                );
901
                            } else {
902
                                if (!is_array($data[0])) {
903
                                    $data[0] = array(
904
                                        'STATIC',
905
                                        $data[0],
906
                                    );
907
                                }
908
                                throw $this->exception(
909
                                    'Cannot call hook. Function might not exist.'
910
                                )
911
                                    ->addMoreInfo('hook', $hook_spot)
912
                                    ->addMoreInfo('arg1', $data[0][0])
913
                                    ->addMoreInfo('arg2', $data[0][1]);
914
                            }
915
                            $return[] = $result;
916
                        }
917
                    }
918
919
                    $this->hooks[$hook_spot] = $hook_backup;
920
                }
921
            }
922
        } catch (Exception_Hook $e) {
923
            $this->hooks[$hook_spot] = $hook_backup;
0 ignored issues
show
Bug introduced by
The variable $hook_backup 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...
924
925
            return $e->return_value;
926
        }
927
928
        return $return;
929
    }
930
931
    /**
932
     * When called from inside a hook callable, will stop execution of other
933
     * callables on same hook. The passed argument will be returned by the
934
     * hook method.
935
     *
936
     * @param mixed $return What would hook() return?
937
     */
938
    public function breakHook($return)
939
    {
940
        $e = $this->exception(null, 'Hook');
941
        $e->return_value = $return;
0 ignored issues
show
Bug introduced by
The property return_value does not seem to exist in BaseException.

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

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

Loading history...
942
        throw $e;
943
    }
944
    // }}}
945
946
    // {{{ Dynamic Methods: http://agiletoolkit.org/learn/dynamic
947
    /**
948
     * Call method is used to display exception for non-existant methods and
949
     * provides ability to extend objects with addMethod().
950
     *
951
     * @param string $method    Name of the method
952
     * @param array  $arguments Arguments
953
     *
954
     * @return mixed
955
     */
956
    public function __call($method, $arguments)
957
    {
958
        if (($ret = $this->tryCall($method, $arguments))) {
959
            return $ret[0];
960
        }
961
        throw $this->exception(
962
            'Method is not defined for this object',
963
            'Logic'
964
        )
965
            ->addMoreInfo('class', get_class($this))
966
            ->addMoreInfo('method', $method)
967
            ->addMoreInfo('arguments', var_export($arguments, true));
968
    }
969
970
    /**
971
     * Attempts to call dynamic method. Returns array containing result or false.
972
     *
973
     * @param string $method    Name of the method
974
     * @param array  $arguments Arguments
975
     *
976
     * @return mixed
977
     */
978
    public function tryCall($method, $arguments)
979
    {
980
        if ($ret = $this->hook('method-'.$method, $arguments)) {
981
            return $ret;
982
        }
983
        array_unshift($arguments, $this);
984
        if (($ret = $this->app->hook('global-method-'.$method, $arguments))) {
985
            return $ret;
986
        }
987
    }
988
989
    /**
990
     * Add new method for this object.
991
     *
992
     * @param string|array $name     Name of new method of $this object
993
     * @param callable     $callable Callback
994
     *
995
     * @return $this
996
     */
997
    public function addMethod($name, $callable)
998
    {
999 View Code Duplication
        if (is_string($name) && strpos($name, ',') !== false) {
1000
            $name = explode(',', $name);
1001
        }
1002
        if (is_array($name)) {
1003
            foreach ($name as $h) {
1004
                $this->addMethod($h, $callable);
1005
            }
1006
1007
            return $this;
1008
        }
1009 View Code Duplication
        if (is_object($callable) && !is_callable($callable)) {
1010
            $callable = array($callable, $name);
1011
        }
1012
        if ($this->hasMethod($name)) {
1013
            throw $this->exception('Registering method twice');
1014
        }
1015
        $this->addHook('method-'.$name, $callable);
1016
1017
        return $this;
1018
    }
1019
1020
    /**
1021
     * Return if this object has specified method (either native or dynamic).
1022
     *
1023
     * @param string $name Name of the method
1024
     *
1025
     * @return bool
1026
     */
1027
    public function hasMethod($name)
1028
    {
1029
        return method_exists($this, $name)
1030
            || isset($this->hooks['method-'.$name])
1031
            || isset($this->app->hooks['global-method-'.$name]);
1032
    }
1033
1034
    /**
1035
     * Remove dynamically registered method.
1036
     *
1037
     * @param string $name Name of the method
1038
     *
1039
     * @return $this
1040
     */
1041
    public function removeMethod($name)
1042
    {
1043
        $this->removeHook('method-'.$name);
1044
1045
        return $this;
1046
    }
1047
    // }}}
1048
1049
    // {{{ Logger: to be moved out
1050
    /**
1051
     * Output string into log file.
1052
     *
1053
     * @param string $var var
1054
     * @param string $msg msg
1055
     *
1056
     * @obsolete
1057
     */
1058
    public function logVar($var, $msg = '')
1059
    {
1060
        $this->app->getLogger()->logVar($var, $msg);
1061
    }
1062
1063
    /**
1064
     * Output string into info file.
1065
     *
1066
     * @param string $info info
1067
     * @param string $msg  msg
1068
     *
1069
     * @obsolete
1070
     */
1071
    public function logInfo($info, $msg = '')
1072
    {
1073
        $this->app->getLogger()->logLine($msg.' '.$info."\n");
1074
    }
1075
1076
    /**
1077
     * Output string into error file.
1078
     *
1079
     * @param string $error error
1080
     * @param string $msg   msg
1081
     *
1082
     * @obsolete
1083
     */
1084
    public function logError($error, $msg = '')
1085
    {
1086
        if (is_object($error)) {
1087
            // we got exception object obviously
1088
            $error = $error->getMessage();
1089
        }
1090
        $this->app->getLogger()->logLine($msg.' '.$error."\n", null, 'error');
1091
    }
1092
    // }}}
1093
1094
    /**
1095
     * A handy shortcut for foreach(){ .. } code. Make your callable return
1096
     * "false" if you would like to break the loop.
1097
     *
1098
     * @param string|callable $callable will be executed for each member
1099
     *
1100
     * @return $this
1101
     */
1102
    public function each($callable)
1103
    {
1104
        if ($this instanceof Iterator) {
1105
1106
            if (is_string($callable)) {
1107
                foreach ($this as $obj) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<AbstractObject> is not traversable.
Loading history...
1108
                    $obj->$callable();
1109
                }
1110
1111
                return $this;
1112
            }
1113
1114
            foreach ($this as $obj) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<AbstractObject> is not traversable.
Loading history...
1115
                if (call_user_func($callable, $obj) === false) {
1116
                    break;
1117
                }
1118
            }
1119
1120
        } else {
1121
            throw $this->exception('Calling each() on non-iterative object');
1122
        }
1123
1124
        return $this;
1125
    }
1126
1127
    /**
1128
     * This method will find private methods started with test_ in
1129
     * the current class and will execute each method in succession
1130
     * by passing $t argument to it. Before each test execution
1131
     * takes place, $t->prepareForTest($test) will be called. It must
1132
     * return non-false for test to be carried out.
1133
     *
1134
     * $test will be an array containing keys for 'name', 'object' and
1135
     * 'class'
1136
     */
1137
    public function runTests(Tester $tester = null)
1138
    {
1139
        $test = array('object' => $this->name, 'class' => get_class($this));
1140
1141
        foreach (get_class_methods($this) as $method) {
1142
            if (strpos($method, 'test_') === 0) {
1143
                $test['name'] = substr($method, 5);
1144
            } else {
1145
                continue;
1146
            }
1147
1148
            if ($tester && $tester->prepareForTest($test) === false) {
0 ignored issues
show
Unused Code introduced by
The call to Tester::prepareForTest() has too many arguments starting with $test.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1149
                continue;
1150
            }
1151
1152
            if ($tester) {
1153
                $r = $tester->results;
1154
                $r->unload();
0 ignored issues
show
Documentation Bug introduced by
The method unload does not exist on object<AbstractObject>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1155
                $r->set($test);
0 ignored issues
show
Documentation Bug introduced by
The method set does not exist on object<AbstractObject>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1156
            }
1157
1158
            // Proceed with test
1159
            $me = memory_get_peak_usage();
1160
            $ms = microtime(true);
1161
            $this->_ticks = 0;
1162
            declare (ticks = 1);
1163
            register_tick_function(array($this, '_ticker'));
1164
1165
            // Execute here
1166
            try {
1167
                $result = $this->$method($tester);
1168
            } catch (Exception $e) {
1169
                unregister_tick_function(array($this, '_ticker'));
1170
                $time = microtime(true) - $ms;
1171
                $memory = (memory_get_peak_usage()) - $me;
1172
                $ticks = $this->_ticks;
1173
1174
                if ($e instanceof Exception_SkipTests) {
1175
                    if ($tester) {
1176
                        $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...
1177
                        $r->saveAndUnload();
0 ignored issues
show
Documentation Bug introduced by
The method saveAndUnload does not exist on object<AbstractObject>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1178
                    }
1179
1180
                    return array(
1181
                        'skipped' => $e->getMessage(),
1182
                        );
1183
                }
1184
1185
                if ($tester) {
1186
                    $r['time'] = $time;
1187
                    $r['memory'] = $memory;
1188
                    $r['ticks'] = $ticks;
1189
                    $r['exception'] = $e;
1190
                    $r->saveAndUnload();
0 ignored issues
show
Documentation Bug introduced by
The method saveAndUnload does not exist on object<AbstractObject>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1191
                }
1192
1193
                continue;
1194
            }
1195
1196
            // Unregister
1197
            unregister_tick_function(array($this, '_ticker'));
1198
            $time = microtime(true) - $ms;
1199
            $memory = (memory_get_peak_usage()) - $me;
1200
            $ticks = $this->_ticks - 3;     // there are always minimum of 3 ticks
1201
1202
            if ($tester) {
1203
                $r['time'] = $time;
1204
                $r['memory'] = $memory;
1205
                $r['ticks'] = $ticks;
1206
                $r['is_success'] = true;
1207
                $r['result'] = $result;
1208
                $r->saveAndUnload();
0 ignored issues
show
Documentation Bug introduced by
The method saveAndUnload does not exist on object<AbstractObject>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1209
            }
1210
        }
1211
    }
1212
    private $_ticks;
1213
    public function _ticker()
1214
    {
1215
        ++$this->_ticks;
1216
    }
1217
1218
    /**
1219
     * Method used internally for shortening object names.
1220
     *
1221
     * @param string $desired Desired name of new object.
1222
     *
1223
     * @return string Shortened name of new object.
1224
     */
1225
    public function _shorten($desired)
1226
    {
1227
        if (strlen($desired) > $this->app->max_name_length
1228
            && $this->app->max_name_length !== false
1229
        ) {
1230
            $len = $this->app->max_name_length - 10;
1231
            if ($len < 5) {
1232
                $len = $this->app->max_name_length;
1233
            }
1234
1235
            $key = substr($desired, 0, $len);
1236
            $rest = substr($desired, $len);
1237
1238
            if (!$this->app->unique_hashes[$key]) {
1239
                $this->app->unique_hashes[$key] = dechex(crc32($key));
1240
            }
1241
            $desired = $this->app->unique_hashes[$key].'__'.$rest;
1242
        };
1243
1244
        return $desired;
1245
    }
1246
1247
    private $_element_name_counts = array();
1248
    public function _unique_element($desired = null)
1249
    {
1250
        $postfix = @++$this->_element_name_counts[$desired];
1251
1252
        return $desired.($postfix > 1 ? ('_'.$postfix) : '');
1253
    }
1254
1255
    /**
1256
     * This funcion given the associative $array and desired new key will return
1257
     * the best matching key which is not yet in the array.
1258
     * For example, if you have array('foo'=>x,'bar'=>x) and $desired is 'foo'
1259
     * function will return 'foo_2'. If 'foo_2' key also exists in that array,
1260
     * then 'foo_3' is returned and so on.
1261
     *
1262
     * @param array  &$array  Reference to array which stores key=>value pairs
1263
     * @param string $desired Desired key for new object
1264
     *
1265
     * @return string unique key for new object
1266
     */
1267
    public function _unique(&$array, $desired = null)
1268
    {
1269
        if (!is_array($array)) {
1270
            throw $this->exception('not array');
1271
        }
1272
        $postfix = count($array);
1273
        $attempted_key = $desired;
1274
        while (array_key_exists($attempted_key, $array)) {
1275
            // already used, move on
1276
            $attempted_key = ($desired ?: 'undef').'_'.(++$postfix);
1277
        }
1278
1279
        return $attempted_key;
1280
    }
1281
1282
    /**
1283
     * Always call parent if you redefine this/.
1284
     */
1285
    public function __destruct()
1286
    {
1287
    }
1288
1289
    /**
1290
     * Do not serialize objects.
1291
     *
1292
     * @return mixed
1293
     */
1294
    public function __sleep()
1295
    {
1296
        return array('name');
1297
    }
1298
}
1299