Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

Object::parse_class_spec()   D

Complexity

Conditions 25
Paths 35

Size

Total Lines 80
Code Lines 58

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 80
rs 4.9798
cc 25
eloc 58
nc 35
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * A base class for all 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(),
42
		$extra_methods       = array(),
43
		$built_in_methods    = array();
44
45
	private static
46
		$custom_classes = array(),
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() {
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);
196
				else self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec, $firstArg);
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
		$had_ns = false;
224
225
		foreach($tokens as $token) {
226
			$tName = is_array($token) ? $token[0] : $token;
227
			// Get the class naem
228
			if($class == null && is_array($token) && $token[0] == T_STRING) {
229
				$class = $token[1];
230
			} elseif(is_array($token) && $token[0] == T_NS_SEPARATOR) {
231
				$class .= $token[1];
232
				$had_ns = true;
233
			} elseif ($had_ns && is_array($token) && $token[0] == T_STRING) {
234
				$class .= $token[1];
235
				$had_ns = false;
236
			// Get arguments
237
			} else if(is_array($token)) {
238
				switch($token[0]) {
239
				case T_CONSTANT_ENCAPSED_STRING:
240
					$argString = $token[1];
241
					switch($argString[0]) {
242
					case '"':
243
						$argString = stripcslashes(substr($argString,1,-1));
244
						break;
245
					case "'":
246
						$argString = str_replace(array("\\\\", "\\'"),array("\\", "'"), substr($argString,1,-1));
247
						break;
248
					default:
249
						throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
250
					}
251
					$bucket[] = $argString;
252
					break;
253
254
				case T_DNUMBER:
255
					$bucket[] = (double)$token[1];
256
					break;
257
258
				case T_LNUMBER:
259
					$bucket[] = (int)$token[1];
260
					break;
261
262
				case T_STRING:
263
					switch($token[1]) {
264
						case 'true': $bucket[] = true; break;
265
						case 'false': $bucket[] = false; break;
266
						case 'null': $bucket[] = null; break;
267
						default: throw new Exception("Bad T_STRING arg '{$token[1]}'");
268
					}
269
					break;
270
271
				case T_ARRAY:
272
					// Add an empty array to the bucket
273
					$bucket[] = array();
274
					$bucketStack[] = &$bucket;
275
					$bucket = &$bucket[sizeof($bucket)-1];
276
277
				}
278
279
			} else {
280
				if($tName == '[') {
281
					// Add an empty array to the bucket
282
					$bucket[] = array();
283
					$bucketStack[] = &$bucket;
284
					$bucket = &$bucket[sizeof($bucket)-1];
285
				} elseif($tName == ')' || $tName == ']') {
286
					// Pop-by-reference
287
					$bucket = &$bucketStack[sizeof($bucketStack)-1];
288
					array_pop($bucketStack);
289
				}
290
			}
291
		}
292
293
		return array($class, $args);
294
	}
295
296
	/**
297
	 * Similar to {@link Object::create()}, except that classes are only overloaded if you set the $strong parameter to
298
	 * TRUE when using {@link Object::useCustomClass()}
299
	 *
300
	 * @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...
301
	 * @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...
302
	 * @return static
303
	 */
304
	public static function strong_create() {
305
		$args  = func_get_args();
306
		$class = array_shift($args);
307
308 View Code Duplication
		if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
			$class = self::$strong_classes[$class];
310
		}
311
312
		return Injector::inst()->createWithArgs($class, $args);
313
	}
314
315
	/**
316
	 * This class allows you to overload classes with other classes when they are constructed using the factory method
317
	 * {@link Object::create()}
318
	 *
319
	 * @param string $oldClass the class to replace
320
	 * @param string $newClass the class to replace it with
321
	 * @param bool $strong allows you to enforce a certain class replacement under all circumstances. This is used in
322
	 *        singletons and DB interaction classes
323
	 */
324
	public static function useCustomClass($oldClass, $newClass, $strong = false) {
325
		if($strong) {
326
			self::$strong_classes[$oldClass] = $newClass;
327
		} else {
328
			self::$custom_classes[$oldClass] = $newClass;
329
		}
330
	}
331
332
	/**
333
	 * If a class has been overloaded, get the class name it has been overloaded with - otherwise return the class name
334
	 *
335
	 * @param string $class the class to check
336
	 * @return string the class that would be created if you called {@link Object::create()} with the class
337
	 */
338
	public static function getCustomClass($class) {
339
		if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
340
			return self::$strong_classes[$class];
341 View Code Duplication
		} elseif(isset(self::$custom_classes[$class]) && ClassInfo::exists(self::$custom_classes[$class])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
342
			return self::$custom_classes[$class];
343
		}
344
345
		return $class;
346
	}
347
348
	/**
349
	 * Get the value of a static property of a class, even in that property is declared protected (but not private),
350
	 * without any inheritance, merging or parent lookup if it doesn't exist on the given class.
351
	 *
352
	 * @static
353
	 * @param $class - The class to get the static from
354
	 * @param $name - The property to get from the class
355
	 * @param null $default - The value to return if property doesn't exist on class
356
	 * @return any - The value of the static property $name on class $class, or $default if that property is not
357
	 *               defined
358
	 */
359
	public static function static_lookup($class, $name, $default = null) {
360
		if (is_subclass_of($class, 'Object')) {
361
			if (isset($class::$$name)) {
362
				$parent = get_parent_class($class);
363
				if (!$parent || !isset($parent::$$name) || $parent::$$name !== $class::$$name) return $class::$$name;
364
			}
365
			return $default;
366
		} else {
367
			// TODO: This gets set once, then not updated, so any changes to statics after this is called the first
368
			// time for any class won't be exposed
369
			static $static_properties = array();
370
371 View Code Duplication
			if (!isset($static_properties[$class])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
372
				$reflection = new ReflectionClass($class);
373
				$static_properties[$class] = $reflection->getStaticProperties();
374
			}
375
376
			if (isset($static_properties[$class][$name])) {
377
				$value = $static_properties[$class][$name];
378
379
				$parent = get_parent_class($class);
380
				if (!$parent) return $value;
381
382 View Code Duplication
				if (!isset($static_properties[$parent])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
383
					$reflection = new ReflectionClass($parent);
384
					$static_properties[$parent] = $reflection->getStaticProperties();
385
				}
386
387
				if (!isset($static_properties[$parent][$name]) || $static_properties[$parent][$name] !== $value) {
388
					return $value;
389
				}
390
			}
391
		}
392
393
		return $default;
394
	}
395
396
	/**
397
	 * @deprecated
398
	 */
399
	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...
400
		Deprecation::notice('4.0', 'Replaced by Config#get');
401
		return Config::inst()->get($class, $name, Config::FIRST_SET);
402
	}
403
404
	/**
405
	 * @deprecated
406
	 */
407
	public static function set_static($class, $name, $value) {
408
		Deprecation::notice('4.0', 'Replaced by Config#update');
409
		Config::inst()->update($class, $name, $value);
410
	}
411
412
	/**
413
	 * @deprecated
414
	 */
415
	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...
416
		Deprecation::notice('4.0', 'Replaced by Config#get');
417
		return Config::inst()->get($class, $name, Config::UNINHERITED);
418
	}
419
420
	/**
421
	 * @deprecated
422
	 */
423
	public static function combined_static($class, $name, $ceiling = false) {
424
		if ($ceiling) throw new Exception('Ceiling argument to combined_static is no longer supported');
425
426
		Deprecation::notice('4.0', 'Replaced by Config#get');
427
		return Config::inst()->get($class, $name);
428
	}
429
430
	/**
431
	 * @deprecated
432
	 */
433
	public static function addStaticVars($class, $properties, $replace = false) {
434
		Deprecation::notice('4.0', 'Replaced by Config#update');
435
		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...
436
	}
437
438
	/**
439
	 * @deprecated
440
	 */
441
	public static function add_static_var($class, $name, $value, $replace = false) {
442
		Deprecation::notice('4.0', 'Replaced by Config#remove and Config#update');
443
444
		if ($replace) Config::inst()->remove($class, $name);
445
		Config::inst()->update($class, $name, $value);
446
	}
447
448
	/**
449
	 * Return TRUE if a class has a specified extension.
450
	 * This supports backwards-compatible format (static Object::has_extension($requiredExtension))
451
	 * and new format ($object->has_extension($class, $requiredExtension))
452
	 * @param string $classOrExtension if 1 argument supplied, the class name of the extension to
453
	 *                                 check for; if 2 supplied, the class name to test
454
	 * @param string $requiredExtension used only if 2 arguments supplied
455
	 * @param boolean $strict if the extension has to match the required extension and not be a subclass
456
	 */
457
	public static function has_extension($classOrExtension, $requiredExtension = null, $strict = false) {
458
		//BC support
459 View Code Duplication
		if(func_num_args() > 1){
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
460
			$class = $classOrExtension;
461
			$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...
462
		}
463
		else {
464
			$class = get_called_class();
465
			$requiredExtension = $classOrExtension;
466
		}
467
468
		$requiredExtension = strtolower($requiredExtension);
469
		$extensions = Config::inst()->get($class, 'extensions');
470
471
		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...
472
			$left = strtolower(Extension::get_classname_without_arguments($extension));
473
			$right = strtolower(Extension::get_classname_without_arguments($requiredExtension));
474
			if($left == $right) return true;
475
			if (!$strict && is_subclass_of($left, $right)) return true;
476
		}
477
478
		return false;
479
	}
480
481
	/**
482
	 * Add an extension to a specific class.
483
	 *
484
	 * The preferred method for adding extensions is through YAML config,
485
	 * since it avoids autoloading the class, and is easier to override in
486
	 * more specific configurations.
487
	 *
488
	 * As an alternative, extensions can be added to a specific class
489
	 * directly in the {@link Object::$extensions} array.
490
	 * See {@link SiteTree::$extensions} for examples.
491
	 * Keep in mind that the extension will only be applied to new
492
	 * instances, not existing ones (including all instances created through {@link singleton()}).
493
	 *
494
	 * @see http://doc.silverstripe.org/framework/en/trunk/reference/dataextension
495
	 * @param string $classOrExtension Class that should be extended - has to be a subclass of {@link Object}
496
	 * @param string $extension Subclass of {@link Extension} with optional parameters
497
	 *  as a string, e.g. "Versioned" or "Translatable('Param')"
498
	 */
499
	public static function add_extension($classOrExtension, $extension = null) {
500 View Code Duplication
		if(func_num_args() > 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
501
			$class = $classOrExtension;
502
		} else {
503
			$class = get_called_class();
504
			$extension = $classOrExtension;
505
		}
506
507
		if(!preg_match('/^([^(]*)/', $extension, $matches)) {
508
			return false;
509
		}
510
		$extensionClass = $matches[1];
511
		if(!class_exists($extensionClass)) {
512
			user_error(
513
				sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass),
514
				E_USER_ERROR
515
			);
516
		}
517
518
		if(!is_subclass_of($extensionClass, 'Extension')) {
519
			user_error(
520
				sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass),
521
				E_USER_ERROR
522
			);
523
		}
524
525
		// unset some caches
526
		$subclasses = ClassInfo::subclassesFor($class);
527
		$subclasses[] = $class;
528
529
		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...
530
			unset(self::$classes_constructed[$subclass]);
531
			unset(self::$extra_methods[$subclass]);
532
		}
533
534
		Config::inst()->update($class, 'extensions', array($extension));
535
		Config::inst()->extraConfigSourcesChanged($class);
536
537
		Injector::inst()->unregisterNamedObject($class);
538
539
		// load statics now for DataObject classes
540
		if(is_subclass_of($class, 'DataObject')) {
541
			if(!is_subclass_of($extensionClass, 'DataExtension')) {
542
				user_error("$extensionClass cannot be applied to $class without being a DataExtension", E_USER_ERROR);
543
			}
544
		}
545
	}
546
547
548
	/**
549
	 * Remove an extension from a class.
550
	 *
551
	 * Keep in mind that this won't revert any datamodel additions
552
	 * of the extension at runtime, unless its used before the
553
	 * schema building kicks in (in your _config.php).
554
	 * Doesn't remove the extension from any {@link Object}
555
	 * instances which are already created, but will have an
556
	 * effect on new extensions.
557
	 * Clears any previously created singletons through {@link singleton()}
558
	 * to avoid side-effects from stale extension information.
559
	 *
560
	 * @todo Add support for removing extensions with parameters
561
	 *
562
	 * @param string $extension Classname of an {@link Extension} subclass, without parameters
563
	 */
564
	public static function remove_extension($extension) {
565
		$class = get_called_class();
566
567
		Config::inst()->remove($class, 'extensions', Config::anything(), $extension);
568
569
		// remove any instances of the extension with parameters
570
		$config = Config::inst()->get($class, 'extensions');
571
572
		if($config) {
573
			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...
574
				// extensions with parameters will be stored in config as
575
				// ExtensionName("Param").
576
				if(preg_match(sprintf("/^(%s)\(/", preg_quote($extension, '/')), $v)) {
577
					Config::inst()->remove($class, 'extensions', Config::anything(), $v);
578
				}
579
			}
580
		}
581
582
		Config::inst()->extraConfigSourcesChanged($class);
583
584
		// unset singletons to avoid side-effects
585
		Injector::inst()->unregisterAllObjects();
586
587
		// unset some caches
588
		$subclasses = ClassInfo::subclassesFor($class);
589
		$subclasses[] = $class;
590
		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...
591
			unset(self::$classes_constructed[$subclass]);
592
			unset(self::$extra_methods[$subclass]);
593
		}
594
	}
595
596
	/**
597
	 * @param string $class
598
	 * @param bool $includeArgumentString Include the argument string in the return array,
599
	 *  FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')").
600
	 * @return array Numeric array of either {@link DataExtension} classnames,
601
	 *  or eval'ed classname strings with constructor arguments.
602
	 */
603
	public static function get_extensions($class, $includeArgumentString = false) {
604
		$extensions = Config::inst()->get($class, 'extensions');
605
606
		if($includeArgumentString) {
607
			return $extensions;
608
		} else {
609
			$extensionClassnames = array();
610
			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...
611
				$extensionClassnames[] = Extension::get_classname_without_arguments($extension);
612
			}
613
			return $extensionClassnames;
614
		}
615
	}
616
617
	// --------------------------------------------------------------------------------------------------------------
618
619
	private static $unextendable_classes = array('Object', 'ViewableData', 'RequestHandler');
620
621
	static public function get_extra_config_sources($class = null) {
622
		if($class === null) $class = get_called_class();
623
624
		// If this class is unextendable, NOP
625
		if(in_array($class, self::$unextendable_classes)) return;
626
627
		// Variable to hold sources in
628
		$sources = null;
629
630
		// Get a list of extensions
631
		$extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
632
633
		if($extensions) {
634
			// Build a list of all sources;
635
			$sources = array();
636
637
			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...
638
				list($extensionClass, $extensionArgs) = self::parse_class_spec($extension);
639
				$sources[] = $extensionClass;
640
641
				if(!ClassInfo::has_method_from($extensionClass, 'add_to_class', 'Extension')) {
642
					Deprecation::notice('4.0',
643
						"add_to_class deprecated on $extensionClass. Use get_extra_config instead");
644
				}
645
646
				call_user_func(array($extensionClass, 'add_to_class'), $class, $extensionClass, $extensionArgs);
647
648
				foreach(array_reverse(ClassInfo::ancestry($extensionClass)) as $extensionClassParent) {
649
					if (ClassInfo::has_method_from($extensionClassParent, 'get_extra_config', $extensionClassParent)) {
650
						$extras = $extensionClassParent::get_extra_config($class, $extensionClass, $extensionArgs);
651
						if ($extras) $sources[] = $extras;
652
					}
653
				}
654
			}
655
		}
656
657
		return $sources;
658
	}
659
660
	public function __construct() {
661
		$this->class = get_class($this);
662
663
		foreach(ClassInfo::ancestry(get_called_class()) as $class) {
664
			if(in_array($class, self::$unextendable_classes)) continue;
665
			$extensions = Config::inst()->get($class, 'extensions',
666
				Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
667
668
			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...
669
				$instance = self::create_from_string($extension);
670
				$instance->setOwner(null, $class);
671
				$this->extension_instances[$instance->class] = $instance;
672
			}
673
		}
674
675
		if(!isset(self::$classes_constructed[$this->class])) {
676
			$this->defineMethods();
677
			self::$classes_constructed[$this->class] = true;
678
		}
679
	}
680
681
	/**
682
	 * Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
683
	 *
684
	 * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
685
	 * {@link Object::addWrapperMethod()}
686
	 *
687
	 * @param string $method
688
	 * @param array $arguments
689
	 * @return mixed
690
	 */
691
	public function __call($method, $arguments) {
692
		// If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
693
		// call, then we should rebuild it.
694
		if(empty(self::$extra_methods[get_class($this)])) {
695
			$this->defineMethods();
696
		}
697
698
		$method = strtolower($method);
699
700
		if(isset(self::$extra_methods[$this->class][$method])) {
701
			$config = self::$extra_methods[$this->class][$method];
702
703
			switch(true) {
704
				case isset($config['property']) :
705
					$obj = $config['index'] !== null ?
706
						$this->{$config['property']}[$config['index']] :
707
						$this->{$config['property']};
708
709
					if($obj) {
710
						if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
711
						$retVal = call_user_func_array(array($obj, $method), $arguments);
712
						if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
713
						return $retVal;
714
					}
715
716
					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...
717
						throw new Exception (
718
							"Object->__call(): attempt to call $method on a destroyed $this->class object"
719
						);
720
					} else {
721
						throw new Exception (
722
							"Object->__call(): $this->class cannot pass control to $config[property]($config[index])."
723
								. ' Perhaps this object was mistakenly destroyed?'
724
						);
725
					}
726
727
				case isset($config['wrap']) :
728
					array_unshift($arguments, $config['method']);
729
					return call_user_func_array(array($this, $config['wrap']), $arguments);
730
731
				case isset($config['function']) :
732
					return $config['function']($this, $arguments);
733
734
				default :
735
					throw new Exception (
736
						"Object->__call(): extra method $method is invalid on $this->class:"
737
							. var_export($config, true)
738
					);
739
			}
740
		} else {
741
			// Please do not change the exception code number below.
742
			$class = get_class($this);
743
			throw new Exception("Object->__call(): the method '$method' does not exist on '$class'", 2175);
744
		}
745
	}
746
747
	// --------------------------------------------------------------------------------------------------------------
748
749
	/**
750
	 * Return TRUE if a method exists on this object
751
	 *
752
	 * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
753
	 * extensions
754
	 *
755
	 * @param string $method
756
	 * @return bool
757
	 */
758
	public function hasMethod($method) {
759
		return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
760
	}
761
762
	/**
763
	 * Return the names of all the methods available on this object
764
	 *
765
	 * @param bool $custom include methods added dynamically at runtime
766
	 * @return array
767
	 */
768
	public function allMethodNames($custom = false) {
769
		if(!isset(self::$built_in_methods[$this->class])) {
770
			self::$built_in_methods[$this->class] = array_map('strtolower', get_class_methods($this));
771
		}
772
773
		if($custom && isset(self::$extra_methods[$this->class])) {
774
			return array_merge(self::$built_in_methods[$this->class], array_keys(self::$extra_methods[$this->class]));
775
		} else {
776
			return self::$built_in_methods[$this->class];
777
		}
778
	}
779
780
	/**
781
	 * Adds any methods from {@link Extension} instances attached to this object.
782
	 * All these methods can then be called directly on the instance (transparently
783
	 * mapped through {@link __call()}), or called explicitly through {@link extend()}.
784
	 *
785
	 * @uses addMethodsFrom()
786
	 */
787
	protected function defineMethods() {
788
		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...
789
			$this->addMethodsFrom('extension_instances', $key);
790
		}
791
792
		if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$this->class])) {
793
			Debug::require_developer_login();
794
795
			echo '<h2>Methods defined on ' . $this->class . '</h2><ul>';
796
			foreach(self::$built_in_methods[$this->class] as $method) {
797
				echo "<li>$method</li>";
798
			}
799
			echo '</ul>';
800
		}
801
	}
802
803
	/**
804
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
805
	 *
806
	 * @param string $property the property name
807
	 * @param string|int $index an index to use if the property is an array
808
	 */
809
	protected function addMethodsFrom($property, $index = null) {
810
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
811
812
		if(!$extension) {
813
			throw new InvalidArgumentException (
814
				"Object->addMethodsFrom(): could not add methods from {$this->class}->{$property}[$index]"
815
			);
816
		}
817
818
		if(method_exists($extension, 'allMethodNames')) {
819
			if ($extension instanceof Extension) $extension->setOwner($this);
820
			$methods = $extension->allMethodNames(true);
821
			if ($extension instanceof Extension) $extension->clearOwner();
822
823
		} else {
824
			if(!isset(self::$built_in_methods[$extension->class])) {
825
				self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
826
			}
827
			$methods = self::$built_in_methods[$extension->class];
828
		}
829
830
		if($methods) {
831
			$methodInfo = array(
832
				'property' => $property,
833
				'index'    => $index,
834
				'callSetOwnerFirst' => $extension instanceof Extension,
835
			);
836
837
			$newMethods = array_fill_keys($methods, $methodInfo);
838
839
			if(isset(self::$extra_methods[$this->class])) {
840
				self::$extra_methods[$this->class] =
841
					array_merge(self::$extra_methods[$this->class], $newMethods);
842
			} else {
843
				self::$extra_methods[$this->class] = $newMethods;
844
			}
845
		}
846
	}
847
848
	/**
849
	 * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
850
	 * can be wrapped to generateThumbnail(x)
851
	 *
852
	 * @param string $method the method name to wrap
853
	 * @param string $wrap the method name to wrap to
854
	 */
855
	protected function addWrapperMethod($method, $wrap) {
856
		self::$extra_methods[$this->class][strtolower($method)] = array (
857
			'wrap'   => $wrap,
858
			'method' => $method
859
		);
860
	}
861
862
	/**
863
	 * Add an extra method using raw PHP code passed as a string
864
	 *
865
	 * @param string $method the method name
866
	 * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
867
	 *        by using $obj. Note that you cannot call protected methods, as the method is actually an external
868
	 *        function
869
	 */
870
	protected function createMethod($method, $code) {
871
		self::$extra_methods[$this->class][strtolower($method)] = array (
872
			'function' => create_function('$obj, $args', $code)
873
		);
874
	}
875
876
	// --------------------------------------------------------------------------------------------------------------
877
878
	/**
879
	 * @see Object::get_static()
880
	 */
881
	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...
882
		return Config::inst()->get(($this->class ? $this->class : get_class($this)), $name, Config::FIRST_SET);
883
	}
884
885
	/**
886
	 * @see Object::set_static()
887
	 */
888
	public function set_stat($name, $value) {
889
		Config::inst()->update(($this->class ? $this->class : get_class($this)), $name, $value);
890
	}
891
892
	/**
893
	 * @see Object::uninherited_static()
894
	 */
895
	public function uninherited($name) {
896
		return Config::inst()->get(($this->class ? $this->class : get_class($this)), $name, Config::UNINHERITED);
897
	}
898
899
	// --------------------------------------------------------------------------------------------------------------
900
901
	/**
902
	 * Return true if this object "exists" i.e. has a sensible value
903
	 *
904
	 * This method should be overriden in subclasses to provide more context about the classes state. For example, a
905
	 * {@link DataObject} class could return false when it is deleted from the database
906
	 *
907
	 * @return bool
908
	 */
909
	public function exists() {
910
		return true;
911
	}
912
913
	/**
914
	 * @return string this classes parent class
915
	 */
916
	public function parentClass() {
917
		return get_parent_class($this);
918
	}
919
920
	/**
921
	 * Check if this class is an instance of a specific class, or has that class as one of its parents
922
	 *
923
	 * @param string $class
924
	 * @return bool
925
	 */
926
	public function is_a($class) {
927
		return $this instanceof $class;
928
	}
929
930
	/**
931
	 * @return string the class name
932
	 */
933
	public function __toString() {
934
		return $this->class;
935
	}
936
937
	// --------------------------------------------------------------------------------------------------------------
938
939
	/**
940
	 * Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge
941
	 * all results into an array
942
	 *
943
	 * @param string $method the method name to call
944
	 * @param mixed $argument a single argument to pass
945
	 * @return mixed
946
	 * @todo integrate inheritance rules
947
	 */
948
	public function invokeWithExtensions($method, $argument = null) {
949
		$result = method_exists($this, $method) ? array($this->$method($argument)) : array();
950
		$extras = $this->extend($method, $argument);
951
952
		return $extras ? array_merge($result, $extras) : $result;
953
	}
954
955
	/**
956
	 * Run the given function on all of this object's extensions. Note that this method originally returned void, so if
957
	 * you wanted to return results, you're hosed
958
	 *
959
	 * Currently returns an array, with an index resulting every time the function is called. Only adds returns if
960
	 * they're not NULL, to avoid bogus results from methods just defined on the parent extension. This is important for
961
	 * permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't
962
	 * do type checking, an included NULL return would fail the permission checks.
963
	 *
964
	 * The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
965
	 *
966
	 * @param string $method the name of the method to call on each extension
967
	 * @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...
968
	 * @return array
969
	 */
970
	public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) {
971
		$values = array();
972
973 View Code Duplication
		if(!empty($this->beforeExtendCallbacks[$method])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
974
			foreach(array_reverse($this->beforeExtendCallbacks[$method]) as $callback) {
975
				$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
976
				if($value !== null) $values[] = $value;
977
			}
978
			$this->beforeExtendCallbacks[$method] = array();
979
		}
980
981
		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...
982
			if(method_exists($instance, $method)) {
983
				$instance->setOwner($this);
984
				$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
985
				if($value !== null) $values[] = $value;
986
				$instance->clearOwner();
987
			}
988
		}
989
990 View Code Duplication
		if(!empty($this->afterExtendCallbacks[$method])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
991
			foreach(array_reverse($this->afterExtendCallbacks[$method]) as $callback) {
992
				$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
993
				if($value !== null) $values[] = $value;
994
			}
995
			$this->afterExtendCallbacks[$method] = array();
996
		}
997
998
		return $values;
999
	}
1000
1001
	/**
1002
	 * Get an extension instance attached to this object by name.
1003
	 *
1004
	 * @uses hasExtension()
1005
	 *
1006
	 * @param string $extension
1007
	 * @return Extension
1008
	 */
1009
	public function getExtensionInstance($extension) {
1010
		if($this->hasExtension($extension)) return $this->extension_instances[$extension];
1011
	}
1012
1013
	/**
1014
	 * Returns TRUE if this object instance has a specific extension applied
1015
	 * in {@link $extension_instances}. Extension instances are initialized
1016
	 * at constructor time, meaning if you use {@link add_extension()}
1017
	 * afterwards, the added extension will just be added to new instances
1018
	 * of the extended class. Use the static method {@link has_extension()}
1019
	 * to check if a class (not an instance) has a specific extension.
1020
	 * Caution: Don't use singleton(<class>)->hasExtension() as it will
1021
	 * give you inconsistent results based on when the singleton was first
1022
	 * accessed.
1023
	 *
1024
	 * @param string $extension Classname of an {@link Extension} subclass without parameters
1025
	 * @return bool
1026
	 */
1027
	public function hasExtension($extension) {
1028
		return isset($this->extension_instances[$extension]);
1029
	}
1030
1031
	/**
1032
	 * Get all extension instances for this specific object instance.
1033
	 * See {@link get_extensions()} to get all applied extension classes
1034
	 * for this class (not the instance).
1035
	 *
1036
	 * @return array Map of {@link DataExtension} instances, keyed by classname.
1037
	 */
1038
	public function getExtensionInstances() {
1039
		return $this->extension_instances;
1040
	}
1041
1042
	// --------------------------------------------------------------------------------------------------------------
1043
1044
	/**
1045
	 * Cache the results of an instance method in this object to a file, or if it is already cache return the cached
1046
	 * results
1047
	 *
1048
	 * @param string $method the method name to cache
1049
	 * @param int $lifetime the cache lifetime in seconds
1050
	 * @param string $ID custom cache ID to use
1051
	 * @param array $arguments an optional array of arguments
1052
	 * @return mixed the cached data
1053
	 */
1054
	public function cacheToFile($method, $lifetime = 3600, $ID = false, $arguments = array()) {
1055
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1056
1057
		if(!$this->hasMethod($method)) {
1058
			throw new InvalidArgumentException("Object->cacheToFile(): the method $method does not exist to cache");
1059
		}
1060
1061
		$cacheName = $this->class . '_' . $method;
1062
1063
		if(!is_array($arguments)) $arguments = array($arguments);
1064
1065
		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...
1066
		if(count($arguments)) $cacheName .= '_' . md5(serialize($arguments));
1067
1068
		$data = $this->loadCache($cacheName, $lifetime);
1069
1070
		if($data !== false) {
1071
			return $data;
1072
		}
1073
1074
		$data = call_user_func_array(array($this, $method), $arguments);
1075
		$this->saveCache($cacheName, $data);
1076
1077
		return $data;
1078
	}
1079
1080
	/**
1081
	 * Clears the cache for the given cacheToFile call
1082
	 */
1083
	public function clearCache($method, $ID = false, $arguments = array()) {
1084
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1085
1086
		$cacheName = $this->class . '_' . $method;
1087
		if(!is_array($arguments)) $arguments = array($arguments);
1088
		if($ID) $cacheName .= '_' . $ID;
1089
		if(count($arguments)) $cacheName .= '_' . md5(serialize($arguments));
1090
1091
		$file = TEMP_FOLDER . '/' . $this->sanitiseCachename($cacheName);
1092
		if(file_exists($file)) unlink($file);
1093
	}
1094
1095
	/**
1096
	 * Loads a cache from the filesystem if a valid on is present and within the specified lifetime
1097
	 *
1098
	 * @param string $cache the cache name
1099
	 * @param int $lifetime the lifetime (in seconds) of the cache before it is invalid
1100
	 * @return mixed
1101
	 */
1102
	protected function loadCache($cache, $lifetime = 3600) {
1103
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1104
1105
		$path = TEMP_FOLDER . '/' . $this->sanitiseCachename($cache);
1106
1107
		if(!isset($_REQUEST['flush']) && file_exists($path) && (filemtime($path) + $lifetime) > time()) {
1108
			return unserialize(file_get_contents($path));
1109
		}
1110
1111
		return false;
1112
	}
1113
1114
	/**
1115
	 * Save a piece of cached data to the file system
1116
	 *
1117
	 * @param string $cache the cache name
1118
	 * @param mixed $data data to save (must be serializable)
1119
	 */
1120
	protected function saveCache($cache, $data) {
1121
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1122
		file_put_contents(TEMP_FOLDER . '/' . $this->sanitiseCachename($cache), serialize($data));
1123
	}
1124
1125
	/**
1126
	 * Strip a file name of special characters so it is suitable for use as a cache file name
1127
	 *
1128
	 * @param string $name
1129
	 * @return string the name with all special cahracters replaced with underscores
1130
	 */
1131
	protected function sanitiseCachename($name) {
1132
		Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
1133
		return str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $name);
1134
	}
1135
1136
}
1137