1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Core; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use SilverStripe\Control\RequestHandler; |
7
|
|
|
use SilverStripe\Core\Config\Config; |
8
|
|
|
use SilverStripe\Core\Injector\Injector; |
9
|
|
|
use SilverStripe\Dev\Deprecation; |
10
|
|
|
use SilverStripe\ORM\DataExtension; |
11
|
|
|
use SilverStripe\ORM\DataObject; |
12
|
|
|
use SilverStripe\View\ViewableData; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Allows an object to have extensions applied to it. |
16
|
|
|
*/ |
17
|
|
|
trait Extensible |
18
|
|
|
{ |
19
|
|
|
use CustomMethods { |
20
|
|
|
defineMethods as defineMethodsCustom; |
21
|
|
|
} |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* An array of extension names and parameters to be applied to this object upon construction. |
25
|
|
|
* |
26
|
|
|
* Example: |
27
|
|
|
* <code> |
28
|
|
|
* private static $extensions = array ( |
29
|
|
|
* 'Hierarchy', |
30
|
|
|
* "Version('Stage', 'Live')" |
31
|
|
|
* ); |
32
|
|
|
* </code> |
33
|
|
|
* |
34
|
|
|
* Use {@link Object::add_extension()} to add extensions without access to the class code, |
35
|
|
|
* e.g. to extend core classes. |
36
|
|
|
* |
37
|
|
|
* Extensions are instantiated together with the object and stored in {@link $extension_instances}. |
38
|
|
|
* |
39
|
|
|
* @var array $extensions |
40
|
|
|
* @config |
41
|
|
|
*/ |
42
|
|
|
private static $extensions = []; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Classes that cannot be extended |
46
|
|
|
* |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
private static $unextendable_classes = array( |
50
|
|
|
ViewableData::class, |
51
|
|
|
RequestHandler::class, |
52
|
|
|
); |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var Extension[] all current extension instances, or null if not declared yet. |
56
|
|
|
*/ |
57
|
|
|
protected $extension_instances = null; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* List of callbacks to call prior to extensions having extend called on them, |
61
|
|
|
* each grouped by methodName. |
62
|
|
|
* |
63
|
|
|
* Top level array is method names, each of which is an array of callbacks for that name. |
64
|
|
|
* |
65
|
|
|
* @var callable[][] |
66
|
|
|
*/ |
67
|
|
|
protected $beforeExtendCallbacks = array(); |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* List of callbacks to call after extensions having extend called on them, |
71
|
|
|
* each grouped by methodName. |
72
|
|
|
* |
73
|
|
|
* Top level array is method names, each of which is an array of callbacks for that name. |
74
|
|
|
* |
75
|
|
|
* @var callable[][] |
76
|
|
|
*/ |
77
|
|
|
protected $afterExtendCallbacks = array(); |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Allows user code to hook into Object::extend prior to control |
81
|
|
|
* being delegated to extensions. Each callback will be reset |
82
|
|
|
* once called. |
83
|
|
|
* |
84
|
|
|
* @param string $method The name of the method to hook into |
85
|
|
|
* @param callable $callback The callback to execute |
86
|
|
|
*/ |
87
|
|
|
protected function beforeExtending($method, $callback) |
88
|
|
|
{ |
89
|
|
|
if (empty($this->beforeExtendCallbacks[$method])) { |
90
|
|
|
$this->beforeExtendCallbacks[$method] = array(); |
91
|
|
|
} |
92
|
|
|
$this->beforeExtendCallbacks[$method][] = $callback; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Allows user code to hook into Object::extend after control |
97
|
|
|
* being delegated to extensions. Each callback will be reset |
98
|
|
|
* once called. |
99
|
|
|
* |
100
|
|
|
* @param string $method The name of the method to hook into |
101
|
|
|
* @param callable $callback The callback to execute |
102
|
|
|
*/ |
103
|
|
|
protected function afterExtending($method, $callback) |
104
|
|
|
{ |
105
|
|
|
if (empty($this->afterExtendCallbacks[$method])) { |
106
|
|
|
$this->afterExtendCallbacks[$method] = array(); |
107
|
|
|
} |
108
|
|
|
$this->afterExtendCallbacks[$method][] = $callback; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @deprecated 4.0..5.0 Extensions and methods are now lazy-loaded |
113
|
|
|
*/ |
114
|
|
|
protected function constructExtensions() |
115
|
|
|
{ |
116
|
|
|
Deprecation::notice('5.0', 'constructExtensions does not need to be invoked and will be removed in 5.0'); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
protected function defineMethods() |
120
|
|
|
{ |
121
|
|
|
$this->defineMethodsCustom(); |
|
|
|
|
122
|
|
|
|
123
|
|
|
// Define extension methods |
124
|
|
|
$this->defineExtensionMethods(); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Adds any methods from {@link Extension} instances attached to this object. |
129
|
|
|
* All these methods can then be called directly on the instance (transparently |
130
|
|
|
* mapped through {@link __call()}), or called explicitly through {@link extend()}. |
131
|
|
|
* |
132
|
|
|
* @uses addCallbackMethod() |
133
|
|
|
*/ |
134
|
|
|
protected function defineExtensionMethods() |
135
|
|
|
{ |
136
|
|
|
$extensions = $this->getExtensionInstances(); |
137
|
|
|
foreach ($extensions as $extensionClass => $extensionInstance) { |
138
|
|
|
foreach ($this->findMethodsFromExtension($extensionInstance) as $method) { |
139
|
|
|
$this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) { |
140
|
|
|
/** @var Extensible $inst */ |
141
|
|
|
$extension = $inst->getExtensionInstance($extensionClass); |
142
|
|
|
try { |
143
|
|
|
$extension->setOwner($inst); |
144
|
|
|
return call_user_func_array([$extension, $method], $args); |
145
|
|
|
} finally { |
146
|
|
|
$extension->clearOwner(); |
147
|
|
|
} |
148
|
|
|
}); |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Add an extension to a specific class. |
155
|
|
|
* |
156
|
|
|
* The preferred method for adding extensions is through YAML config, |
157
|
|
|
* since it avoids autoloading the class, and is easier to override in |
158
|
|
|
* more specific configurations. |
159
|
|
|
* |
160
|
|
|
* As an alternative, extensions can be added to a specific class |
161
|
|
|
* directly in the {@link Object::$extensions} array. |
162
|
|
|
* See {@link SiteTree::$extensions} for examples. |
163
|
|
|
* Keep in mind that the extension will only be applied to new |
164
|
|
|
* instances, not existing ones (including all instances created through {@link singleton()}). |
165
|
|
|
* |
166
|
|
|
* @see http://doc.silverstripe.org/framework/en/trunk/reference/dataextension |
167
|
|
|
* @param string $classOrExtension Class that should be extended - has to be a subclass of {@link Object} |
168
|
|
|
* @param string $extension Subclass of {@link Extension} with optional parameters |
169
|
|
|
* as a string, e.g. "Versioned" or "Translatable('Param')" |
170
|
|
|
* @return bool Flag if the extension was added |
171
|
|
|
*/ |
172
|
|
|
public static function add_extension($classOrExtension, $extension = null) |
173
|
|
|
{ |
174
|
|
|
if ($extension) { |
|
|
|
|
175
|
|
|
$class = $classOrExtension; |
176
|
|
|
} else { |
177
|
|
|
$class = get_called_class(); |
178
|
|
|
$extension = $classOrExtension; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
if (!preg_match('/^([^(]*)/', $extension, $matches)) { |
182
|
|
|
return false; |
183
|
|
|
} |
184
|
|
|
$extensionClass = $matches[1]; |
185
|
|
|
if (!class_exists($extensionClass)) { |
186
|
|
|
user_error( |
187
|
|
|
sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass), |
188
|
|
|
E_USER_ERROR |
189
|
|
|
); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
if (!is_subclass_of($extensionClass, 'SilverStripe\\Core\\Extension')) { |
193
|
|
|
user_error( |
194
|
|
|
sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass), |
195
|
|
|
E_USER_ERROR |
196
|
|
|
); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
// unset some caches |
200
|
|
|
$subclasses = ClassInfo::subclassesFor($class); |
201
|
|
|
$subclasses[] = $class; |
202
|
|
|
|
203
|
|
|
if ($subclasses) { |
|
|
|
|
204
|
|
|
foreach ($subclasses as $subclass) { |
205
|
|
|
unset(self::$extra_methods[$subclass]); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
Config::modify() |
210
|
|
|
->merge($class, 'extensions', array( |
211
|
|
|
$extension |
212
|
|
|
)); |
213
|
|
|
|
214
|
|
|
Injector::inst()->unregisterNamedObject($class); |
215
|
|
|
|
216
|
|
|
// load statics now for DataObject classes |
217
|
|
|
if (is_subclass_of($class, DataObject::class)) { |
218
|
|
|
if (!is_subclass_of($extensionClass, DataExtension::class)) { |
219
|
|
|
user_error("$extensionClass cannot be applied to $class without being a DataExtension", E_USER_ERROR); |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
return true; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Remove an extension from a class. |
228
|
|
|
* Note: This will not remove extensions from parent classes, and must be called |
229
|
|
|
* directly on the class assigned the extension. |
230
|
|
|
* |
231
|
|
|
* Keep in mind that this won't revert any datamodel additions |
232
|
|
|
* of the extension at runtime, unless its used before the |
233
|
|
|
* schema building kicks in (in your _config.php). |
234
|
|
|
* Doesn't remove the extension from any {@link Object} |
235
|
|
|
* instances which are already created, but will have an |
236
|
|
|
* effect on new extensions. |
237
|
|
|
* Clears any previously created singletons through {@link singleton()} |
238
|
|
|
* to avoid side-effects from stale extension information. |
239
|
|
|
* |
240
|
|
|
* @todo Add support for removing extensions with parameters |
241
|
|
|
* |
242
|
|
|
* @param string $extension class name of an {@link Extension} subclass, without parameters |
243
|
|
|
*/ |
244
|
|
|
public static function remove_extension($extension) |
245
|
|
|
{ |
246
|
|
|
$class = get_called_class(); |
247
|
|
|
|
248
|
|
|
// Build filtered extension list |
249
|
|
|
$found = false; |
250
|
|
|
$config = Config::inst()->get($class, 'extensions', Config::EXCLUDE_EXTRA_SOURCES | Config::UNINHERITED) ?: []; |
251
|
|
|
foreach ($config as $key => $candidate) { |
252
|
|
|
// extensions with parameters will be stored in config as ExtensionName("Param"). |
253
|
|
|
if (strcasecmp($candidate, $extension) === 0 || |
254
|
|
|
stripos($candidate, $extension.'(') === 0 |
255
|
|
|
) { |
256
|
|
|
$found = true; |
257
|
|
|
unset($config[$key]); |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
// Don't dirty cache if no changes |
261
|
|
|
if (!$found) { |
262
|
|
|
return; |
263
|
|
|
} |
264
|
|
|
Config::modify()->set($class, 'extensions', $config); |
265
|
|
|
|
266
|
|
|
// Unset singletons |
267
|
|
|
Injector::inst()->unregisterObjects($class); |
268
|
|
|
|
269
|
|
|
// unset some caches |
270
|
|
|
$subclasses = ClassInfo::subclassesFor($class); |
271
|
|
|
$subclasses[] = $class; |
272
|
|
|
if ($subclasses) { |
|
|
|
|
273
|
|
|
foreach ($subclasses as $subclass) { |
274
|
|
|
unset(self::$extra_methods[$subclass]); |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @param string $class If omitted, will get extensions for the current class |
281
|
|
|
* @param bool $includeArgumentString Include the argument string in the return array, |
282
|
|
|
* FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')"). |
283
|
|
|
* @return array Numeric array of either {@link DataExtension} class names, |
284
|
|
|
* or eval'ed class name strings with constructor arguments. |
285
|
|
|
*/ |
286
|
|
|
public static function get_extensions($class = null, $includeArgumentString = false) |
287
|
|
|
{ |
288
|
|
|
if (!$class) { |
|
|
|
|
289
|
|
|
$class = get_called_class(); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
$extensions = Config::forClass($class)->get('extensions', Config::EXCLUDE_EXTRA_SOURCES); |
293
|
|
|
if (empty($extensions)) { |
294
|
|
|
return array(); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
// Clean nullified named extensions |
298
|
|
|
$extensions = array_filter(array_values($extensions)); |
299
|
|
|
|
300
|
|
|
if ($includeArgumentString) { |
301
|
|
|
return $extensions; |
302
|
|
|
} else { |
303
|
|
|
$extensionClassnames = array(); |
304
|
|
|
if ($extensions) { |
|
|
|
|
305
|
|
|
foreach ($extensions as $extension) { |
306
|
|
|
$extensionClassnames[] = Extension::get_classname_without_arguments($extension); |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
return $extensionClassnames; |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Get extra config sources for this class |
316
|
|
|
* |
317
|
|
|
* @param string $class Name of class. If left null will return for the current class |
318
|
|
|
* @return array|null |
319
|
|
|
*/ |
320
|
|
|
public static function get_extra_config_sources($class = null) |
321
|
|
|
{ |
322
|
|
|
if (!$class) { |
|
|
|
|
323
|
|
|
$class = get_called_class(); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
// If this class is unextendable, NOP |
327
|
|
|
if (in_array($class, self::$unextendable_classes)) { |
328
|
|
|
return null; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
// Variable to hold sources in |
332
|
|
|
$sources = null; |
|
|
|
|
333
|
|
|
|
334
|
|
|
// Get a list of extensions |
335
|
|
|
$extensions = Config::inst()->get($class, 'extensions', Config::EXCLUDE_EXTRA_SOURCES | Config::UNINHERITED); |
336
|
|
|
|
337
|
|
|
if (!$extensions) { |
338
|
|
|
return null; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
// Build a list of all sources; |
342
|
|
|
$sources = array(); |
343
|
|
|
|
344
|
|
|
foreach ($extensions as $extension) { |
345
|
|
|
list($extensionClass, $extensionArgs) = ClassInfo::parse_class_spec($extension); |
346
|
|
|
// Strip service name specifier |
347
|
|
|
$extensionClass = strtok($extensionClass, '.'); |
348
|
|
|
$sources[] = $extensionClass; |
349
|
|
|
|
350
|
|
|
if (!class_exists($extensionClass)) { |
351
|
|
|
throw new InvalidArgumentException("$class references nonexistent $extensionClass in \$extensions"); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
call_user_func(array($extensionClass, 'add_to_class'), $class, $extensionClass, $extensionArgs); |
355
|
|
|
|
356
|
|
|
foreach (array_reverse(ClassInfo::ancestry($extensionClass)) as $extensionClassParent) { |
357
|
|
|
if (ClassInfo::has_method_from($extensionClassParent, 'get_extra_config', $extensionClassParent)) { |
358
|
|
|
$extras = $extensionClassParent::get_extra_config($class, $extensionClass, $extensionArgs); |
359
|
|
|
if ($extras) { |
360
|
|
|
$sources[] = $extras; |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
return $sources; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Return TRUE if a class has a specified extension. |
372
|
|
|
* This supports backwards-compatible format (static Object::has_extension($requiredExtension)) |
373
|
|
|
* and new format ($object->has_extension($class, $requiredExtension)) |
374
|
|
|
* @param string $classOrExtension Class to check extension for, or the extension name to check |
375
|
|
|
* if the second argument is null. |
376
|
|
|
* @param string $requiredExtension If the first argument is the parent class, this is the extension to check. |
377
|
|
|
* If left null, the first parameter will be treated as the extension. |
378
|
|
|
* @param boolean $strict if the extension has to match the required extension and not be a subclass |
379
|
|
|
* @return bool Flag if the extension exists |
380
|
|
|
*/ |
381
|
|
|
public static function has_extension($classOrExtension, $requiredExtension = null, $strict = false) |
382
|
|
|
{ |
383
|
|
|
if ($requiredExtension) { |
|
|
|
|
384
|
|
|
$class = $classOrExtension; |
385
|
|
|
} else { |
386
|
|
|
$class = get_called_class(); |
387
|
|
|
$requiredExtension = $classOrExtension; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
$requiredExtension = Extension::get_classname_without_arguments($requiredExtension); |
391
|
|
|
$extensions = self::get_extensions($class); |
392
|
|
|
foreach ($extensions as $extension) { |
393
|
|
|
if (strcasecmp($extension, $requiredExtension) === 0) { |
394
|
|
|
return true; |
395
|
|
|
} |
396
|
|
|
if (!$strict && is_subclass_of($extension, $requiredExtension)) { |
397
|
|
|
return true; |
398
|
|
|
} |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
return false; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge |
407
|
|
|
* all results into an array |
408
|
|
|
* |
409
|
|
|
* @param string $method the method name to call |
410
|
|
|
* @param mixed $a1 |
411
|
|
|
* @param mixed $a2 |
412
|
|
|
* @param mixed $a3 |
413
|
|
|
* @param mixed $a4 |
414
|
|
|
* @param mixed $a5 |
415
|
|
|
* @param mixed $a6 |
416
|
|
|
* @param mixed $a7 |
417
|
|
|
* @return array List of results with nulls filtered out |
418
|
|
|
*/ |
419
|
|
|
public function invokeWithExtensions($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null) |
420
|
|
|
{ |
421
|
|
|
$result = array(); |
422
|
|
|
if (method_exists($this, $method)) { |
423
|
|
|
$thisResult = $this->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7); |
424
|
|
|
if ($thisResult !== null) { |
425
|
|
|
$result[] = $thisResult; |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
$extras = $this->extend($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7); |
429
|
|
|
|
430
|
|
|
return $extras ? array_merge($result, $extras) : $result; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Run the given function on all of this object's extensions. Note that this method originally returned void, so if |
435
|
|
|
* you wanted to return results, you're hosed |
436
|
|
|
* |
437
|
|
|
* Currently returns an array, with an index resulting every time the function is called. Only adds returns if |
438
|
|
|
* they're not NULL, to avoid bogus results from methods just defined on the parent extension. This is important for |
439
|
|
|
* permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't |
440
|
|
|
* do type checking, an included NULL return would fail the permission checks. |
441
|
|
|
* |
442
|
|
|
* The extension methods are defined during {@link __construct()} in {@link defineMethods()}. |
443
|
|
|
* |
444
|
|
|
* @param string $method the name of the method to call on each extension |
445
|
|
|
* @param mixed $a1 |
446
|
|
|
* @param mixed $a2 |
447
|
|
|
* @param mixed $a3 |
448
|
|
|
* @param mixed $a4 |
449
|
|
|
* @param mixed $a5 |
450
|
|
|
* @param mixed $a6 |
451
|
|
|
* @param mixed $a7 |
452
|
|
|
* @return array |
453
|
|
|
*/ |
454
|
|
|
public function extend($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null) |
455
|
|
|
{ |
456
|
|
|
$values = array(); |
457
|
|
|
|
458
|
|
|
if (!empty($this->beforeExtendCallbacks[$method])) { |
459
|
|
|
foreach (array_reverse($this->beforeExtendCallbacks[$method]) as $callback) { |
460
|
|
|
$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7)); |
461
|
|
|
if ($value !== null) { |
462
|
|
|
$values[] = $value; |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
$this->beforeExtendCallbacks[$method] = array(); |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
foreach ($this->getExtensionInstances() as $instance) { |
469
|
|
|
if (method_exists($instance, $method)) { |
470
|
|
|
try { |
471
|
|
|
$instance->setOwner($this); |
472
|
|
|
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7); |
473
|
|
|
} finally { |
474
|
|
|
$instance->clearOwner(); |
475
|
|
|
} |
476
|
|
|
if ($value !== null) { |
|
|
|
|
477
|
|
|
$values[] = $value; |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
if (!empty($this->afterExtendCallbacks[$method])) { |
483
|
|
|
foreach (array_reverse($this->afterExtendCallbacks[$method]) as $callback) { |
484
|
|
|
$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7)); |
485
|
|
|
if ($value !== null) { |
486
|
|
|
$values[] = $value; |
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
$this->afterExtendCallbacks[$method] = array(); |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
return $values; |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* Get an extension instance attached to this object by name. |
497
|
|
|
* |
498
|
|
|
* @param string $extension |
499
|
|
|
* @return Extension|null |
500
|
|
|
*/ |
501
|
|
|
public function getExtensionInstance($extension) |
502
|
|
|
{ |
503
|
|
|
$instances = $this->getExtensionInstances(); |
504
|
|
|
if (array_key_exists($extension, $instances)) { |
505
|
|
|
return $instances[$extension]; |
506
|
|
|
} |
507
|
|
|
// in case Injector has been used to replace an extension |
508
|
|
|
foreach ($instances as $instance) { |
509
|
|
|
if (is_a($instance, $extension)) { |
510
|
|
|
return $instance; |
511
|
|
|
} |
512
|
|
|
} |
513
|
|
|
return null; |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Returns TRUE if this object instance has a specific extension applied |
518
|
|
|
* in {@link $extension_instances}. Extension instances are initialized |
519
|
|
|
* at constructor time, meaning if you use {@link add_extension()} |
520
|
|
|
* afterwards, the added extension will just be added to new instances |
521
|
|
|
* of the extended class. Use the static method {@link has_extension()} |
522
|
|
|
* to check if a class (not an instance) has a specific extension. |
523
|
|
|
* Caution: Don't use singleton(<class>)->hasExtension() as it will |
524
|
|
|
* give you inconsistent results based on when the singleton was first |
525
|
|
|
* accessed. |
526
|
|
|
* |
527
|
|
|
* @param string $extension Classname of an {@link Extension} subclass without parameters |
528
|
|
|
* @return bool |
529
|
|
|
*/ |
530
|
|
|
public function hasExtension($extension) |
531
|
|
|
{ |
532
|
|
|
return (bool) $this->getExtensionInstance($extension); |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* Get all extension instances for this specific object instance. |
537
|
|
|
* See {@link get_extensions()} to get all applied extension classes |
538
|
|
|
* for this class (not the instance). |
539
|
|
|
* |
540
|
|
|
* This method also provides lazy-population of the extension_instances property. |
541
|
|
|
* |
542
|
|
|
* @return Extension[] Map of {@link DataExtension} instances, keyed by classname. |
543
|
|
|
*/ |
544
|
|
|
public function getExtensionInstances() |
545
|
|
|
{ |
546
|
|
|
if (isset($this->extension_instances)) { |
547
|
|
|
return $this->extension_instances; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
// Setup all extension instances for this instance |
551
|
|
|
$this->extension_instances = []; |
552
|
|
|
foreach (ClassInfo::ancestry(static::class) as $class) { |
553
|
|
|
if (in_array($class, self::$unextendable_classes)) { |
554
|
|
|
continue; |
555
|
|
|
} |
556
|
|
|
$extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES); |
557
|
|
|
|
558
|
|
|
if ($extensions) { |
559
|
|
|
foreach ($extensions as $extension) { |
560
|
|
|
$name = $extension; |
561
|
|
|
// Allow service names of the form "%$ServiceName" |
562
|
|
|
if (substr($name, 0, 2) == '%$') { |
563
|
|
|
$name = substr($name, 2); |
564
|
|
|
} |
565
|
|
|
$name = trim(strtok($name, '(')); |
566
|
|
|
if (class_exists($name)) { |
567
|
|
|
$name = ClassInfo::class_name($name); |
568
|
|
|
} |
569
|
|
|
$this->extension_instances[$name] = Injector::inst()->get($extension); |
570
|
|
|
} |
571
|
|
|
} |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
return $this->extension_instances; |
575
|
|
|
} |
576
|
|
|
} |
577
|
|
|
|