Completed
Push — member-groupset-delete ( a90a9a )
by Loz
11:22
created

Object::set_stat()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * A base class for all SilverStripe objects to inherit from.
4
 *
5
 * This class provides a number of pattern implementations, as well as methods and fixes to add extra psuedo-static
6
 * and method functionality to PHP.
7
 *
8
 * See {@link Extension} on how to implement a custom multiple
9
 * inheritance for object instances based on PHP5 method call overloading.
10
 *
11
 * @todo Create instance-specific removeExtension() which removes an extension from $extension_instances,
12
 * but not from static $extensions, and clears everything added through defineMethods(), mainly $extra_methods.
13
 *
14
 * @package framework
15
 * @subpackage core
16
 */
17
abstract class Object {
18
19
	/**
20
	 * An array of extension names and parameters to be applied to this object upon construction.
21
	 *
22
	 * Example:
23
	 * <code>
24
	 * private static $extensions = array (
25
	 *   'Hierarchy',
26
	 *   "Version('Stage', 'Live')"
27
	 * );
28
	 * </code>
29
	 *
30
	 * Use {@link Object::add_extension()} to add extensions without access to the class code,
31
	 * e.g. to extend core classes.
32
	 *
33
	 * Extensions are instanciated together with the object and stored in {@link $extension_instances}.
34
	 *
35
	 * @var array $extensions
36
	 * @config
37
	 */
38
	private static $extensions = null;
39
40
	private static
41
		$classes_constructed = array(),
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $classes_constructed.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
42
		$extra_methods       = array(),
43
		$built_in_methods    = array();
44
45
	private static
46
		$custom_classes = array(),
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $custom_classes.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
47
		$strong_classes = array();
48
49
	/**#@-*/
50
51
	/**
52
	 * @var string the class name
53
	 */
54
	public $class;
55
56
	/**
57
	 * Get a configuration accessor for this class. Short hand for Config::inst()->get($this->class, .....).
58
	 * @return Config_ForClass|null
59
	 */
60
	static public function config() {
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
61
		return Config::inst()->forClass(get_called_class());
62
	}
63
64
	/**
65
	 * @var array all current extension instances.
66
	 */
67
	protected $extension_instances = array();
68
69
	/**
70
	 * List of callbacks to call prior to extensions having extend called on them,
71
	 * each grouped by methodName.
72
	 *
73
	 * @var array[callable]
74
	 */
75
	protected $beforeExtendCallbacks = array();
76
77
	/**
78
	 * Allows user code to hook into Object::extend prior to control
79
	 * being delegated to extensions. Each callback will be reset
80
	 * once called.
81
	 *
82
	 * @param string $method The name of the method to hook into
83
	 * @param callable $callback The callback to execute
84
	 */
85
	protected function beforeExtending($method, $callback) {
86
		if(empty($this->beforeExtendCallbacks[$method])) {
87
			$this->beforeExtendCallbacks[$method] = array();
88
		}
89
		$this->beforeExtendCallbacks[$method][] = $callback;
90
	}
91
92
	/**
93
	 * List of callbacks to call after extensions having extend called on them,
94
	 * each grouped by methodName.
95
	 *
96
	 * @var array[callable]
97
	 */
98
	protected $afterExtendCallbacks = array();
99
100
	/**
101
	 * Allows user code to hook into Object::extend after control
102
	 * being delegated to extensions. Each callback will be reset
103
	 * once called.
104
	 *
105
	 * @param string $method The name of the method to hook into
106
	 * @param callable $callback The callback to execute
107
	 */
108
	protected function afterExtending($method, $callback) {
109
		if(empty($this->afterExtendCallbacks[$method])) {
110
			$this->afterExtendCallbacks[$method] = array();
111
		}
112
		$this->afterExtendCallbacks[$method][] = $callback;
113
	}
114
115
	/**
116
	 * An implementation of the factory method, allows you to create an instance of a class
117
	 *
118
	 * This method first for strong class overloads (singletons & DB interaction), then custom class overloads. If an
119
	 * overload is found, an instance of this is returned rather than the original class. To overload a class, use
120
	 * {@link Object::useCustomClass()}
121
	 *
122
	 * This can be called in one of two ways - either calling via the class directly,
123
	 * or calling on Object and passing the class name as the first parameter. The following
124
	 * are equivalent:
125
	 *    $list = DataList::create('SiteTree');
126
	 *	  $list = SiteTree::get();
127
	 *
128
	 * @param string $class the class name
0 ignored issues
show
Bug introduced by
There is no parameter named $class. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
129
	 * @param mixed $arguments,... arguments to pass to the constructor
0 ignored issues
show
Bug introduced by
There is no parameter named $arguments,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
130
	 * @return static
131
	 */
132
	public static function create() {
133
		$args = func_get_args();
134
135
		// Class to create should be the calling class if not Object,
136
		// otherwise the first parameter
137
		$class = get_called_class();
138
		if($class == 'Object') $class = array_shift($args);
139
140
		$class = self::getCustomClass($class);
141
142
		return Injector::inst()->createWithArgs($class, $args);
143
	}
144
145
	/**
146
	 * Creates a class instance by the "singleton" design pattern.
147
	 * It will always return the same instance for this class,
148
	 * which can be used for performance reasons and as a simple
149
	 * way to access instance methods which don't rely on instance
150
	 * data (e.g. the custom SilverStripe static handling).
151
	 *
152
	 * @param string $className Optional classname (if called on Object directly)
0 ignored issues
show
Bug introduced by
There is no parameter named $className. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
153
	 * @return static The singleton instance
154
	 */
155
	public static function singleton() {
156
		$args = func_get_args();
157
158
		// Singleton to create should be the calling class if not Object,
159
		// otherwise the first parameter
160
		$class = get_called_class();
161
		if($class === 'Object') $class = array_shift($args);
162
163
		return Injector::inst()->get($class);
164
	}
165
166
	private static $_cache_inst_args = array();
167
168
	/**
169
	 * Create an object from a string representation.  It treats it as a PHP constructor without the
170
	 * 'new' keyword.  It also manages to construct the object without the use of eval().
171
	 *
172
	 * Construction itself is done with Object::create(), so that Object::useCustomClass() calls
173
	 * are respected.
174
	 *
175
	 * `Object::create_from_string("Versioned('Stage','Live')")` will return the result of
176
	 * `Versioned::create('Stage', 'Live);`
177
	 *
178
	 * It is designed for simple, clonable objects.  The first time this method is called for a given
179
	 * string it is cached, and clones of that object are returned.
180
	 *
181
	 * If you pass the $firstArg argument, this will be prepended to the constructor arguments. It's
182
	 * impossible to pass null as the firstArg argument.
183
	 *
184
	 * `Object::create_from_string("Varchar(50)", "MyField")` will return the result of
185
	 * `Vachar::create('MyField', '50');`
186
	 *
187
	 * Arguments are always strings, although this is a quirk of the current implementation rather
188
	 * than something that can be relied upon.
189
	 */
190
	public static function create_from_string($classSpec, $firstArg = null) {
191
		if(!isset(self::$_cache_inst_args[$classSpec.$firstArg])) {
192
			// an $extension value can contain parameters as a string,
193
			// e.g. "Versioned('Stage','Live')"
194
			if(strpos($classSpec,'(') === false) {
195
				if($firstArg === null) self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
196
				else self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec, $firstArg);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
197
198
			} else {
199
				list($class, $args) = self::parse_class_spec($classSpec);
200
201
				if($firstArg !== null) array_unshift($args, $firstArg);
202
				array_unshift($args, $class);
203
204
				self::$_cache_inst_args[$classSpec.$firstArg] = call_user_func_array(array('Object','create'), $args);
205
			}
206
		}
207
208
		return clone self::$_cache_inst_args[$classSpec.$firstArg];
209
	}
210
211
	/**
212
	 * Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
213
	 * Returns a 2-elemnent array, with classname and arguments
214
	 */
215
	public static function parse_class_spec($classSpec) {
216
		$tokens = token_get_all("<?php $classSpec");
217
		$class = null;
218
		$args = array();
219
220
		// Keep track of the current bucket that we're putting data into
221
		$bucket = &$args;
222
		$bucketStack = array();
223
		$hadNamespace = false;
224
		$currentKey = null;
225
226
		foreach($tokens as $token) {
227
			// $forceResult used to allow null result to be detected
228
			$result = $forceResult = null;
229
			$tokenName = is_array($token) ? $token[0] : $token;
230
231
			// Get the class name
232
			if($class === null && is_array($token) && $token[0] === T_STRING) {
233
				$class = $token[1];
234
			} elseif(is_array($token) && $token[0] === T_NS_SEPARATOR) {
235
				$class .= $token[1];
236
				$hadNamespace = true;
237
			} elseif($hadNamespace && is_array($token) && $token[0] === T_STRING) {
238
				$class .= $token[1];
239
				$hadNamespace = false;
240
			// Get arguments
241
			} else if(is_array($token)) {
242
				switch($token[0]) {
243
				case T_CONSTANT_ENCAPSED_STRING:
244
					$argString = $token[1];
245
					switch($argString[0]) {
246
					case '"':
247
								$result = stripcslashes(substr($argString,1,-1));
248
						break;
249
					case "'":
250
								$result = str_replace(array("\\\\", "\\'"),array("\\", "'"), substr($argString,1,-1));
251
						break;
252
					default:
253
						throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
254
					}
255
256
					break;
257
258
				case T_DNUMBER:
259
						$result = (double)$token[1];
260
					break;
261
262
				case T_LNUMBER:
263
						$result = (int)$token[1];
264
						break;
265
266
					case T_DOUBLE_ARROW:
267
						// We've encountered an associative array (the array itself has already been
268
						// added to the bucket), so the previous item added to the bucket is the key
269
						end($bucket);
270
						$currentKey = current($bucket);
271
						array_pop($bucket);
272
					break;
273
274
				case T_STRING:
275
					switch($token[1]) {
276
							case 'true': $result = true; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
277
							case 'false': $result = false; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
278
							case 'null': $result = null; $forceResult = true; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
279
						default: throw new Exception("Bad T_STRING arg '{$token[1]}'");
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
280
					}
281
						
282
					break;
283
284
				case T_ARRAY:
285
						$result = array();
286
						break;
287
				}
288
			} else {
289
				if($tokenName === '[') {
290
					$result = array();
291
				} elseif(($tokenName === ')' || $tokenName === ']') && ! empty($bucketStack)) {
292
					// Store the bucket we're currently working on
293
					$oldBucket = $bucket;
294
					// Fetch the key for the bucket at the top of the stack
295
					end($bucketStack);
296
					$key = key($bucketStack);
297
					reset($bucketStack);
298
					// Re-instate the bucket from the top of the stack
299
					$bucket = &$bucketStack[$key];
300
					// Add our saved, "nested" bucket to the bucket we just popped off the stack
301
					$bucket[$key] = $oldBucket;
302
					// Remove the bucket we just popped off the stack
303
					array_pop($bucketStack);
304
				}
305
			}
306
307
			// If we've got something to add to the bucket, add it
308
			if($result !== null || $forceResult) {
309
				if($currentKey) {
310
					$bucket[$currentKey] = $result;
311
					$currentKey = null;
312
				} else {
313
					$bucket[] = $result;
314
				}
315
316
				// If we've just pushed an array, that becomes our new bucket
317
				if($result === array()) {
318
					// Fetch the key that the array was pushed to
319
					end($bucket);
320
					$key = key($bucket);
321
					reset($bucket);
322
					// Store reference to "old" bucket in the stack
323
					$bucketStack[$key] = &$bucket;
324
					// Set the active bucket to be our newly-pushed, empty array
325
					$bucket = &$bucket[$key];
326
				}
327
			}
328
		}
329
330
		return array($class, $args);
331
	}
332
333
	/**
334
	 * Similar to {@link Object::create()}, except that classes are only overloaded if you set the $strong parameter to
335
	 * TRUE when using {@link Object::useCustomClass()}
336
	 *
337
	 * @param string $class the class name
0 ignored issues
show
Bug introduced by
There is no parameter named $class. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
338
	 * @param mixed $arguments,... arguments to pass to the constructor
0 ignored issues
show
Bug introduced by
There is no parameter named $arguments,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
339
	 * @return static
340
	 */
341
	public static function strong_create() {
342
		$args  = func_get_args();
343
		$class = array_shift($args);
344
345
		if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
346
			$class = self::$strong_classes[$class];
347
		}
348
349
		return Injector::inst()->createWithArgs($class, $args);
350
	}
351
352
	/**
353
	 * This class allows you to overload classes with other classes when they are constructed using the factory method
354
	 * {@link Object::create()}
355
	 *
356
	 * @param string $oldClass the class to replace
357
	 * @param string $newClass the class to replace it with
358
	 * @param bool $strong allows you to enforce a certain class replacement under all circumstances. This is used in
359
	 *        singletons and DB interaction classes
360
	 */
361
	public static function useCustomClass($oldClass, $newClass, $strong = false) {
362
		if($strong) {
363
			self::$strong_classes[$oldClass] = $newClass;
364
		} else {
365
			self::$custom_classes[$oldClass] = $newClass;
366
		}
367
	}
368
369
	/**
370
	 * If a class has been overloaded, get the class name it has been overloaded with - otherwise return the class name
371
	 *
372
	 * @param string $class the class to check
373
	 * @return string the class that would be created if you called {@link Object::create()} with the class
374
	 */
375
	public static function getCustomClass($class) {
376
		if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
377
			return self::$strong_classes[$class];
378
		} elseif(isset(self::$custom_classes[$class]) && ClassInfo::exists(self::$custom_classes[$class])) {
379
			return self::$custom_classes[$class];
380
		}
381
382
		return $class;
383
	}
384
385
	/**
386
	 * Get the value of a static property of a class, even in that property is declared protected (but not private),
387
	 * without any inheritance, merging or parent lookup if it doesn't exist on the given class.
388
	 *
389
	 * @static
390
	 * @param $class - The class to get the static from
391
	 * @param $name - The property to get from the class
392
	 * @param null $default - The value to return if property doesn't exist on class
393
	 * @return any - The value of the static property $name on class $class, or $default if that property is not
394
	 *               defined
395
	 */
396
	public static function static_lookup($class, $name, $default = null) {
397
		if (is_subclass_of($class, 'Object')) {
398
			if (isset($class::$$name)) {
399
				$parent = get_parent_class($class);
400
				if (!$parent || !isset($parent::$$name) || $parent::$$name !== $class::$$name) return $class::$$name;
401
			}
402
			return $default;
403
		} else {
404
			// TODO: This gets set once, then not updated, so any changes to statics after this is called the first
405
			// time for any class won't be exposed
406
			static $static_properties = array();
407
408
			if (!isset($static_properties[$class])) {
409
				$reflection = new ReflectionClass($class);
410
				$static_properties[$class] = $reflection->getStaticProperties();
411
			}
412
413
			if (isset($static_properties[$class][$name])) {
414
				$value = $static_properties[$class][$name];
415
416
				$parent = get_parent_class($class);
417
				if (!$parent) return $value;
418
419
				if (!isset($static_properties[$parent])) {
420
					$reflection = new ReflectionClass($parent);
421
					$static_properties[$parent] = $reflection->getStaticProperties();
422
				}
423
424
				if (!isset($static_properties[$parent][$name]) || $static_properties[$parent][$name] !== $value) {
425
					return $value;
426
				}
427
			}
428
		}
429
430
		return $default;
431
	}
432
433
	/**
434
	 * @deprecated
435
	 */
436
	public static function get_static($class, $name, $uncached = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $uncached is not used and could be removed.

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

Loading history...
437
		Deprecation::notice('4.0', 'Replaced by Config#get');
438
		return Config::inst()->get($class, $name, Config::FIRST_SET);
439
	}
440
441
	/**
442
	 * @deprecated
443
	 */
444
	public static function set_static($class, $name, $value) {
445
		Deprecation::notice('4.0', 'Replaced by Config#update');
446
		Config::inst()->update($class, $name, $value);
447
	}
448
449
	/**
450
	 * @deprecated
451
	 */
452
	public static function uninherited_static($class, $name, $uncached = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $uncached is not used and could be removed.

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

Loading history...
453
		Deprecation::notice('4.0', 'Replaced by Config#get');
454
		return Config::inst()->get($class, $name, Config::UNINHERITED);
455
	}
456
457
	/**
458
	 * @deprecated
459
	 */
460
	public static function combined_static($class, $name, $ceiling = false) {
461
		if ($ceiling) throw new Exception('Ceiling argument to combined_static is no longer supported');
462
463
		Deprecation::notice('4.0', 'Replaced by Config#get');
464
		return Config::inst()->get($class, $name);
465
	}
466
467
	/**
468
	 * @deprecated
469
	 */
470
	public static function addStaticVars($class, $properties, $replace = false) {
471
		Deprecation::notice('4.0', 'Replaced by Config#update');
472
		foreach($properties as $prop => $value) self::add_static_var($class, $prop, $value, $replace);
0 ignored issues
show
Deprecated Code introduced by
The method Object::add_static_var() has been deprecated.

This method has been deprecated.

Loading history...
473
	}
474
475
	/**
476
	 * @deprecated
477
	 */
478
	public static function add_static_var($class, $name, $value, $replace = false) {
479
		Deprecation::notice('4.0', 'Replaced by Config#remove and Config#update');
480
481
		if ($replace) Config::inst()->remove($class, $name);
482
		Config::inst()->update($class, $name, $value);
483
	}
484
485
	/**
486
	 * Return TRUE if a class has a specified extension.
487
	 * This supports backwards-compatible format (static Object::has_extension($requiredExtension))
488
	 * and new format ($object->has_extension($class, $requiredExtension))
489
	 * @param string $classOrExtension if 1 argument supplied, the class name of the extension to
490
	 *                                 check for; if 2 supplied, the class name to test
491
	 * @param string $requiredExtension used only if 2 arguments supplied
492
	 * @param boolean $strict if the extension has to match the required extension and not be a subclass
493
	 */
494
	public static function has_extension($classOrExtension, $requiredExtension = null, $strict = false) {
495
		//BC support
496
		if(func_num_args() > 1){
497
			$class = $classOrExtension;
498
			$requiredExtension = $requiredExtension;
0 ignored issues
show
Bug introduced by
Why assign $requiredExtension to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
499
		}
500
		else {
501
			$class = get_called_class();
502
			$requiredExtension = $classOrExtension;
503
		}
504
505
		$requiredExtension = strtolower($requiredExtension);
506
		$extensions = Config::inst()->get($class, 'extensions');
507
508
		if($extensions) foreach($extensions as $extension) {
0 ignored issues
show
Bug introduced by
The expression $extensions of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
509
			$left = strtolower(Extension::get_classname_without_arguments($extension));
510
			$right = strtolower(Extension::get_classname_without_arguments($requiredExtension));
511
			if($left == $right) return true;
512
			if (!$strict && is_subclass_of($left, $right)) return true;
513
		}
514
515
		return false;
516
	}
517
518
	/**
519
	 * Add an extension to a specific class.
520
	 *
521
	 * The preferred method for adding extensions is through YAML config,
522
	 * since it avoids autoloading the class, and is easier to override in
523
	 * more specific configurations.
524
	 *
525
	 * As an alternative, extensions can be added to a specific class
526
	 * directly in the {@link Object::$extensions} array.
527
	 * See {@link SiteTree::$extensions} for examples.
528
	 * Keep in mind that the extension will only be applied to new
529
	 * instances, not existing ones (including all instances created through {@link singleton()}).
530
	 *
531
	 * @see http://doc.silverstripe.org/framework/en/trunk/reference/dataextension
532
	 * @param string $classOrExtension Class that should be extended - has to be a subclass of {@link Object}
533
	 * @param string $extension Subclass of {@link Extension} with optional parameters
534
	 *  as a string, e.g. "Versioned" or "Translatable('Param')"
535
	 */
536
	public static function add_extension($classOrExtension, $extension = null) {
537
		if(func_num_args() > 1) {
538
			$class = $classOrExtension;
539
		} else {
540
			$class = get_called_class();
541
			$extension = $classOrExtension;
542
		}
543
544
		if(!preg_match('/^([^(]*)/', $extension, $matches)) {
545
			return false;
546
		}
547
		$extensionClass = $matches[1];
548
		if(!class_exists($extensionClass)) {
549
			user_error(
550
				sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass),
551
				E_USER_ERROR
552
			);
553
		}
554
555
		if(!is_subclass_of($extensionClass, 'Extension')) {
556
			user_error(
557
				sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass),
558
				E_USER_ERROR
559
			);
560
		}
561
562
		// unset some caches
563
		$subclasses = ClassInfo::subclassesFor($class);
564
		$subclasses[] = $class;
565
566
		if($subclasses) foreach($subclasses as $subclass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
567
			unset(self::$classes_constructed[$subclass]);
568
			unset(self::$extra_methods[$subclass]);
569
		}
570
571
		Config::inst()->update($class, 'extensions', array($extension));
572
		Config::inst()->extraConfigSourcesChanged($class);
573
574
		Injector::inst()->unregisterNamedObject($class);
575
576
		// load statics now for DataObject classes
577
		if(is_subclass_of($class, 'DataObject')) {
578
			if(!is_subclass_of($extensionClass, 'DataExtension')) {
579
				user_error("$extensionClass cannot be applied to $class without being a DataExtension", E_USER_ERROR);
580
			}
581
		}
582
	}
583
584
585
	/**
586
	 * Remove an extension from a class.
587
	 *
588
	 * Keep in mind that this won't revert any datamodel additions
589
	 * of the extension at runtime, unless its used before the
590
	 * schema building kicks in (in your _config.php).
591
	 * Doesn't remove the extension from any {@link Object}
592
	 * instances which are already created, but will have an
593
	 * effect on new extensions.
594
	 * Clears any previously created singletons through {@link singleton()}
595
	 * to avoid side-effects from stale extension information.
596
	 *
597
	 * @todo Add support for removing extensions with parameters
598
	 *
599
	 * @param string $extension Classname of an {@link Extension} subclass, without parameters
600
	 */
601
	public static function remove_extension($extension) {
602
		$class = get_called_class();
603
604
		Config::inst()->remove($class, 'extensions', Config::anything(), $extension);
605
606
		// remove any instances of the extension with parameters
607
		$config = Config::inst()->get($class, 'extensions');
608
609
		if($config) {
610
			foreach($config as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $config of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
611
				// extensions with parameters will be stored in config as
612
				// ExtensionName("Param").
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
613
				if(preg_match(sprintf("/^(%s)\(/", preg_quote($extension, '/')), $v)) {
614
					Config::inst()->remove($class, 'extensions', Config::anything(), $v);
615
				}
616
			}
617
		}
618
619
		Config::inst()->extraConfigSourcesChanged($class);
620
621
		// unset singletons to avoid side-effects
622
		Injector::inst()->unregisterAllObjects();
623
624
		// unset some caches
625
		$subclasses = ClassInfo::subclassesFor($class);
626
		$subclasses[] = $class;
627
		if($subclasses) foreach($subclasses as $subclass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
628
			unset(self::$classes_constructed[$subclass]);
629
			unset(self::$extra_methods[$subclass]);
630
		}
631
	}
632
633
	/**
634
	 * @param string $class
635
	 * @param bool $includeArgumentString Include the argument string in the return array,
636
	 *  FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')").
637
	 * @return array Numeric array of either {@link DataExtension} classnames,
638
	 *  or eval'ed classname strings with constructor arguments.
639
	 */
640
	public static function get_extensions($class, $includeArgumentString = false) {
641
		$extensions = Config::inst()->get($class, 'extensions');
642
643
		if($includeArgumentString) {
644
			return $extensions;
645
		} else {
646
			$extensionClassnames = array();
647
			if($extensions) foreach($extensions as $extension) {
0 ignored issues
show
Bug introduced by
The expression $extensions of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
648
				$extensionClassnames[] = Extension::get_classname_without_arguments($extension);
649
			}
650
			return $extensionClassnames;
651
		}
652
	}
653
654
	// --------------------------------------------------------------------------------------------------------------
655
656
	private static $unextendable_classes = array('Object', 'ViewableData', 'RequestHandler');
657
658
	static public function get_extra_config_sources($class = null) {
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
659
		if($class === null) $class = get_called_class();
660
661
		// If this class is unextendable, NOP
662
		if(in_array($class, self::$unextendable_classes)) return;
663
664
		// Variable to hold sources in
665
		$sources = null;
666
667
		// Get a list of extensions
668
		$extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
669
670
		if($extensions) {
671
			// Build a list of all sources;
672
			$sources = array();
673
674
			foreach($extensions as $extension) {
0 ignored issues
show
Bug introduced by
The expression $extensions of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
675
				list($extensionClass, $extensionArgs) = self::parse_class_spec($extension);
676
				$sources[] = $extensionClass;
677
678
				if(!ClassInfo::has_method_from($extensionClass, 'add_to_class', 'Extension')) {
679
					Deprecation::notice('4.0',
680
						"add_to_class deprecated on $extensionClass. Use get_extra_config instead");
681
				}
682
683
				call_user_func(array($extensionClass, 'add_to_class'), $class, $extensionClass, $extensionArgs);
684
685
				foreach(array_reverse(ClassInfo::ancestry($extensionClass)) as $extensionClassParent) {
686
					if (ClassInfo::has_method_from($extensionClassParent, 'get_extra_config', $extensionClassParent)) {
687
						$extras = $extensionClassParent::get_extra_config($class, $extensionClass, $extensionArgs);
688
						if ($extras) $sources[] = $extras;
689
					}
690
				}
691
			}
692
		}
693
694
		return $sources;
695
	}
696
697
	public function __construct() {
698
		$this->class = get_class($this);
699
700
		foreach(ClassInfo::ancestry(get_called_class()) as $class) {
701
			if(in_array($class, self::$unextendable_classes)) continue;
702
			$extensions = Config::inst()->get($class, 'extensions',
703
				Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
704
705
			if($extensions) foreach($extensions as $extension) {
0 ignored issues
show
Bug introduced by
The expression $extensions of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
706
				$instance = self::create_from_string($extension);
707
				$instance->setOwner(null, $class);
708
				$this->extension_instances[$instance->class] = $instance;
709
			}
710
		}
711
712
		if(!isset(self::$classes_constructed[$this->class])) {
713
			$this->defineMethods();
714
			self::$classes_constructed[$this->class] = true;
715
		}
716
	}
717
718
	/**
719
	 * Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
720
	 *
721
	 * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
722
	 * {@link Object::addWrapperMethod()}
723
	 *
724
	 * @param string $method
725
	 * @param array $arguments
726
	 * @return mixed
727
	 */
728
	public function __call($method, $arguments) {
729
		// If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
730
		// call, then we should rebuild it.
731
		if(empty(self::$extra_methods[get_class($this)])) {
732
			$this->defineMethods();
733
		}
734
735
		$method = strtolower($method);
736
737
		if(isset(self::$extra_methods[$this->class][$method])) {
738
			$config = self::$extra_methods[$this->class][$method];
739
740
			switch(true) {
741
				case isset($config['property']) :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
742
					$obj = $config['index'] !== null ?
743
						$this->{$config['property']}[$config['index']] :
744
						$this->{$config['property']};
745
746
					if($obj) {
747
						if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
748
						$retVal = call_user_func_array(array($obj, $method), $arguments);
749
						if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
750
						return $retVal;
751
					}
752
753
					if($this->destroyed) {
0 ignored issues
show
Bug introduced by
The property destroyed 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...
754
						throw new Exception (
755
							"Object->__call(): attempt to call $method on a destroyed $this->class object"
756
						);
757
					} else {
758
						throw new Exception (
759
							"Object->__call(): $this->class cannot pass control to $config[property]($config[index])."
760
								. ' Perhaps this object was mistakenly destroyed?'
761
						);
762
					}
763
764
				case isset($config['wrap']) :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
765
					array_unshift($arguments, $config['method']);
766
					return call_user_func_array(array($this, $config['wrap']), $arguments);
767
768
				case isset($config['function']) :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
769
					return $config['function']($this, $arguments);
770
771
				default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
772
					throw new Exception (
773
						"Object->__call(): extra method $method is invalid on $this->class:"
774
							. var_export($config, true)
775
					);
776
			}
777
		} else {
778
			// Please do not change the exception code number below.
779
			$class = get_class($this);
780
			throw new Exception("Object->__call(): the method '$method' does not exist on '$class', or the method is not public.", 2175);
781
		}
782
	}
783
784
	// --------------------------------------------------------------------------------------------------------------
785
786
	/**
787
	 * Return TRUE if a method exists on this object
788
	 *
789
	 * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
790
	 * extensions
791
	 *
792
	 * @param string $method
793
	 * @return bool
794
	 */
795
	public function hasMethod($method) {
796
		return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
797
	}
798
799
	/**
800
	 * Return the names of all the methods available on this object
801
	 *
802
	 * @param bool $custom include methods added dynamically at runtime
803
	 * @return array
804
	 */
805
	public function allMethodNames($custom = false) {
806
		if(!isset(self::$built_in_methods[$this->class])) {
807
			self::$built_in_methods[$this->class] = array_map('strtolower', get_class_methods($this));
808
		}
809
810
		if($custom && isset(self::$extra_methods[$this->class])) {
811
			return array_merge(self::$built_in_methods[$this->class], array_keys(self::$extra_methods[$this->class]));
812
		} else {
813
			return self::$built_in_methods[$this->class];
814
		}
815
	}
816
817
	/**
818
	 * Adds any methods from {@link Extension} instances attached to this object.
819
	 * All these methods can then be called directly on the instance (transparently
820
	 * mapped through {@link __call()}), or called explicitly through {@link extend()}.
821
	 *
822
	 * @uses addMethodsFrom()
823
	 */
824
	protected function defineMethods() {
0 ignored issues
show
Coding Style introduced by
defineMethods uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
825
		if($this->extension_instances) foreach(array_keys($this->extension_instances) as $key) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
826
			$this->addMethodsFrom('extension_instances', $key);
827
		}
828
829
		if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$this->class])) {
830
			Debug::require_developer_login();
831
832
			echo '<h2>Methods defined on ' . $this->class . '</h2><ul>';
833
			foreach(self::$built_in_methods[$this->class] as $method) {
834
				echo "<li>$method</li>";
835
			}
836
			echo '</ul>';
837
		}
838
	}
839
840
	/**
841
	 * @param object $extension
842
	 * @return array
843
	 */
844
	protected function findMethodsFromExtension($extension) {
845
		if (method_exists($extension, 'allMethodNames')) {
846
			if ($extension instanceof Extension) $extension->setOwner($this);
847
			$methods = $extension->allMethodNames(true);
848
			if ($extension instanceof Extension) $extension->clearOwner();
849
		} else {
850
			if (!isset(self::$built_in_methods[$extension->class])) {
851
				self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
852
			}
853
			$methods = self::$built_in_methods[$extension->class];
854
		}
855
856
		return $methods;
857
	}
858
859
	/**
860
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
861
	 *
862
	 * @param string $property the property name
863
	 * @param string|int $index an index to use if the property is an array
864
	 */
865
	protected function addMethodsFrom($property, $index = null) {
866
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
867
868
		if(!$extension) {
869
			throw new InvalidArgumentException (
870
				"Object->addMethodsFrom(): could not add methods from {$this->class}->{$property}[$index]"
871
			);
872
		}
873
874
		$methods = $this->findMethodsFromExtension($extension);
875
		if ($methods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $methods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
876
			$methodInfo = array(
877
				'property' => $property,
878
				'index'    => $index,
879
				'callSetOwnerFirst' => $extension instanceof Extension,
880
			);
881
882
			$newMethods = array_fill_keys($methods, $methodInfo);
883
884
			if(isset(self::$extra_methods[$this->class])) {
885
				self::$extra_methods[$this->class] =
886
					array_merge(self::$extra_methods[$this->class], $newMethods);
887
			} else {
888
				self::$extra_methods[$this->class] = $newMethods;
889
			}
890
		}
891
	}
892
893
	/**
894
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
895
	 *
896
	 * @param string $property the property name
897
	 * @param string|int $index an index to use if the property is an array
898
	 */
899
	protected function removeMethodsFrom($property, $index = null) {
900
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
901
902
		if(!$extension) {
903
			throw new InvalidArgumentException (
904
				"Object->removeMethodsFrom(): could not remove methods from {$this->class}->{$property}[$index]"
905
			);
906
		}
907
908
		$methods = $this->findMethodsFromExtension($extension);
909
		if ($methods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $methods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
910
			foreach ($methods as $method) {
911
				$methodInfo = self::$extra_methods[$this->class][$method];
912
913
				if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
914
					unset(self::$extra_methods[$this->class][$method]);
915
				}
916
			}
917
918
			if (empty(self::$extra_methods[$this->class])) {
919
				unset(self::$extra_methods[$this->class]);
920
			}
921
		}
922
	}
923
924
	/**
925
	 * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
926
	 * can be wrapped to generateThumbnail(x)
927
	 *
928
	 * @param string $method the method name to wrap
929
	 * @param string $wrap the method name to wrap to
930
	 */
931
	protected function addWrapperMethod($method, $wrap) {
932
		self::$extra_methods[$this->class][strtolower($method)] = array (
933
			'wrap'   => $wrap,
934
			'method' => $method
935
		);
936
	}
937
938
	/**
939
	 * Add an extra method using raw PHP code passed as a string
940
	 *
941
	 * @param string $method the method name
942
	 * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
943
	 *        by using $obj. Note that you cannot call protected methods, as the method is actually an external
944
	 *        function
945
	 */
946
	protected function createMethod($method, $code) {
947
		self::$extra_methods[$this->class][strtolower($method)] = array (
948
			'function' => create_function('$obj, $args', $code)
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
949
		);
950
	}
951
952
	// --------------------------------------------------------------------------------------------------------------
953
954
	/**
955
	 * @see Object::get_static()
956
	 */
957
	public function stat($name, $uncached = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $uncached is not used and could be removed.

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

Loading history...
958
		return Config::inst()->get(($this->class ? $this->class : get_class($this)), $name, Config::FIRST_SET);
959
	}
960
961
	/**
962
	 * @see Object::set_static()
963
	 */
964
	public function set_stat($name, $value) {
965
		Config::inst()->update(($this->class ? $this->class : get_class($this)), $name, $value);
966
	}
967
968
	/**
969
	 * @see Object::uninherited_static()
970
	 */
971
	public function uninherited($name) {
972
		return Config::inst()->get(($this->class ? $this->class : get_class($this)), $name, Config::UNINHERITED);
973
	}
974
975
	// --------------------------------------------------------------------------------------------------------------
976
977
	/**
978
	 * Return true if this object "exists" i.e. has a sensible value
979
	 *
980
	 * This method should be overriden in subclasses to provide more context about the classes state. For example, a
981
	 * {@link DataObject} class could return false when it is deleted from the database
982
	 *
983
	 * @return bool
984
	 */
985
	public function exists() {
986
		return true;
987
	}
988
989
	/**
990
	 * @return string this classes parent class
991
	 */
992
	public function parentClass() {
993
		return get_parent_class($this);
994
	}
995
996
	/**
997
	 * Check if this class is an instance of a specific class, or has that class as one of its parents
998
	 *
999
	 * @param string $class
1000
	 * @return bool
1001
	 */
1002
	public function is_a($class) {
1003
		return $this instanceof $class;
1004
	}
1005
1006
	/**
1007
	 * @return string the class name
1008
	 */
1009
	public function __toString() {
1010
		return $this->class;
1011
	}
1012
1013
	// --------------------------------------------------------------------------------------------------------------
1014
1015
	/**
1016
	 * Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge
1017
	 * all results into an array
1018
	 *
1019
	 * @param string $method the method name to call
1020
	 * @param mixed $argument a single argument to pass
1021
	 * @return mixed
1022
	 * @todo integrate inheritance rules
1023
	 */
1024
	public function invokeWithExtensions($method, $argument = null) {
1025
		$result = method_exists($this, $method) ? array($this->$method($argument)) : array();
1026
		$extras = $this->extend($method, $argument);
1027
1028
		return $extras ? array_merge($result, $extras) : $result;
1029
	}
1030
1031
	/**
1032
	 * Run the given function on all of this object's extensions. Note that this method originally returned void, so if
1033
	 * you wanted to return results, you're hosed
1034
	 *
1035
	 * Currently returns an array, with an index resulting every time the function is called. Only adds returns if
1036
	 * they're not NULL, to avoid bogus results from methods just defined on the parent extension. This is important for
1037
	 * permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't
1038
	 * do type checking, an included NULL return would fail the permission checks.
1039
	 *
1040
	 * The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
1041
	 *
1042
	 * @param string $method the name of the method to call on each extension
1043
	 * @param mixed $a1,... up to 7 arguments to be passed to the method
0 ignored issues
show
Bug introduced by
There is no parameter named $a1,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1044
	 * @return array
1045
	 */
1046
	public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) {
1047
		$values = array();
1048
1049
		if(!empty($this->beforeExtendCallbacks[$method])) {
1050
			foreach(array_reverse($this->beforeExtendCallbacks[$method]) as $callback) {
1051
				$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
1052
				if($value !== null) $values[] = $value;
1053
			}
1054
			$this->beforeExtendCallbacks[$method] = array();
1055
		}
1056
1057
		if($this->extension_instances) foreach($this->extension_instances as $instance) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1058
			if(method_exists($instance, $method)) {
1059
				$instance->setOwner($this);
1060
				$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
1061
				if($value !== null) $values[] = $value;
1062
				$instance->clearOwner();
1063
			}
1064
		}
1065
1066
		if(!empty($this->afterExtendCallbacks[$method])) {
1067
			foreach(array_reverse($this->afterExtendCallbacks[$method]) as $callback) {
1068
				$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
1069
				if($value !== null) $values[] = $value;
1070
			}
1071
			$this->afterExtendCallbacks[$method] = array();
1072
		}
1073
1074
		return $values;
1075
	}
1076
1077
	/**
1078
	 * Get an extension instance attached to this object by name.
1079
	 *
1080
	 * @uses hasExtension()
1081
	 *
1082
	 * @param string $extension
1083
	 * @return Extension
1084
	 */
1085
	public function getExtensionInstance($extension) {
1086
		if($this->hasExtension($extension)) return $this->extension_instances[$extension];
1087
	}
1088
1089
	/**
1090
	 * Returns TRUE if this object instance has a specific extension applied
1091
	 * in {@link $extension_instances}. Extension instances are initialized
1092
	 * at constructor time, meaning if you use {@link add_extension()}
1093
	 * afterwards, the added extension will just be added to new instances
1094
	 * of the extended class. Use the static method {@link has_extension()}
1095
	 * to check if a class (not an instance) has a specific extension.
1096
	 * Caution: Don't use singleton(<class>)->hasExtension() as it will
1097
	 * give you inconsistent results based on when the singleton was first
1098
	 * accessed.
1099
	 *
1100
	 * @param string $extension Classname of an {@link Extension} subclass without parameters
1101
	 * @return bool
1102
	 */
1103
	public function hasExtension($extension) {
1104
		return isset($this->extension_instances[$extension]);
1105
	}
1106
1107
	/**
1108
	 * Get all extension instances for this specific object instance.
1109
	 * See {@link get_extensions()} to get all applied extension classes
1110
	 * for this class (not the instance).
1111
	 *
1112
	 * @return array Map of {@link DataExtension} instances, keyed by classname.
1113
	 */
1114
	public function getExtensionInstances() {
1115
		return $this->extension_instances;
1116
	}
1117
1118
	// --------------------------------------------------------------------------------------------------------------
1119
1120
	/**
1121
	 * Cache the results of an instance method in this object to a file, or if it is already cache return the cached
1122
	 * results
1123
	 *
1124
	 * @param string $method the method name to cache
1125
	 * @param int $lifetime the cache lifetime in seconds
1126
	 * @param string $ID custom cache ID to use
1127
	 * @param array $arguments an optional array of arguments
1128
	 * @return mixed the cached data
1129
	 */
1130
	public function cacheToFile($method, $lifetime = 3600, $ID = false, $arguments = array()) {
1131
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1132
1133
		if(!$this->hasMethod($method)) {
1134
			throw new InvalidArgumentException("Object->cacheToFile(): the method $method does not exist to cache");
1135
		}
1136
1137
		$cacheName = $this->class . '_' . $method;
1138
1139
		if(!is_array($arguments)) $arguments = array($arguments);
1140
1141
		if($ID) $cacheName .= '_' . $ID;
0 ignored issues
show
Bug Best Practice introduced by
The expression $ID of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1142
		if(count($arguments)) $cacheName .= '_' . md5(serialize($arguments));
1143
1144
		$data = $this->loadCache($cacheName, $lifetime);
1145
1146
		if($data !== false) {
1147
			return $data;
1148
		}
1149
1150
		$data = call_user_func_array(array($this, $method), $arguments);
1151
		$this->saveCache($cacheName, $data);
1152
1153
		return $data;
1154
	}
1155
1156
	/**
1157
	 * Clears the cache for the given cacheToFile call
1158
	 */
1159
	public function clearCache($method, $ID = false, $arguments = array()) {
1160
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1161
1162
		$cacheName = $this->class . '_' . $method;
1163
		if(!is_array($arguments)) $arguments = array($arguments);
1164
		if($ID) $cacheName .= '_' . $ID;
1165
		if(count($arguments)) $cacheName .= '_' . md5(serialize($arguments));
1166
1167
		$file = TEMP_FOLDER . '/' . $this->sanitiseCachename($cacheName);
1168
		if(file_exists($file)) unlink($file);
1169
	}
1170
1171
	/**
1172
	 * Loads a cache from the filesystem if a valid on is present and within the specified lifetime
1173
	 *
1174
	 * @param string $cache the cache name
1175
	 * @param int $lifetime the lifetime (in seconds) of the cache before it is invalid
1176
	 * @return mixed
1177
	 */
1178
	protected function loadCache($cache, $lifetime = 3600) {
0 ignored issues
show
Coding Style introduced by
loadCache uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1179
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1180
1181
		$path = TEMP_FOLDER . '/' . $this->sanitiseCachename($cache);
1182
1183
		if(!isset($_REQUEST['flush']) && file_exists($path) && (filemtime($path) + $lifetime) > time()) {
1184
			return unserialize(file_get_contents($path));
1185
		}
1186
1187
		return false;
1188
	}
1189
1190
	/**
1191
	 * Save a piece of cached data to the file system
1192
	 *
1193
	 * @param string $cache the cache name
1194
	 * @param mixed $data data to save (must be serializable)
1195
	 */
1196
	protected function saveCache($cache, $data) {
1197
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1198
		file_put_contents(TEMP_FOLDER . '/' . $this->sanitiseCachename($cache), serialize($data));
1199
	}
1200
1201
	/**
1202
	 * Strip a file name of special characters so it is suitable for use as a cache file name
1203
	 *
1204
	 * @param string $name
1205
	 * @return string the name with all special cahracters replaced with underscores
1206
	 */
1207
	protected function sanitiseCachename($name) {
1208
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1209
		return str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $name);
1210
	}
1211
1212
}
1213