Total Complexity | 74 |
Total Lines | 558 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Extensible often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Extensible, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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) |
||
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) |
||
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) |
||
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) |
||
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() |
||
577 |