1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Core;
16:
17: use ArrayIterator;
18: use Cake\Event\EventDispatcherInterface;
19: use Cake\Event\EventListenerInterface;
20: use Countable;
21: use IteratorAggregate;
22: use RuntimeException;
23:
24: /**
25: * Acts as a registry/factory for objects.
26: *
27: * Provides registry & factory functionality for object types. Used
28: * as a super class for various composition based re-use features in CakePHP.
29: *
30: * Each subclass needs to implement the various abstract methods to complete
31: * the template method load().
32: *
33: * The ObjectRegistry is EventManager aware, but each extending class will need to use
34: * \Cake\Event\EventDispatcherTrait to attach and detach on set and bind
35: *
36: * @see \Cake\Controller\ComponentRegistry
37: * @see \Cake\View\HelperRegistry
38: * @see \Cake\Console\TaskRegistry
39: */
40: abstract class ObjectRegistry implements Countable, IteratorAggregate
41: {
42:
43: /**
44: * Map of loaded objects.
45: *
46: * @var object[]
47: */
48: protected $_loaded = [];
49:
50: /**
51: * Loads/constructs an object instance.
52: *
53: * Will return the instance in the registry if it already exists.
54: * If a subclass provides event support, you can use `$config['enabled'] = false`
55: * to exclude constructed objects from being registered for events.
56: *
57: * Using Cake\Controller\Controller::$components as an example. You can alias
58: * an object by setting the 'className' key, i.e.,
59: *
60: * ```
61: * public $components = [
62: * 'Email' => [
63: * 'className' => '\App\Controller\Component\AliasedEmailComponent'
64: * ];
65: * ];
66: * ```
67: *
68: * All calls to the `Email` component would use `AliasedEmail` instead.
69: *
70: * @param string $objectName The name/class of the object to load.
71: * @param array $config Additional settings to use when loading the object.
72: * @return mixed
73: * @throws \Exception If the class cannot be found.
74: */
75: public function load($objectName, $config = [])
76: {
77: if (is_array($config) && isset($config['className'])) {
78: $name = $objectName;
79: $objectName = $config['className'];
80: } else {
81: list(, $name) = pluginSplit($objectName);
82: }
83:
84: $loaded = isset($this->_loaded[$name]);
85: if ($loaded && !empty($config)) {
86: $this->_checkDuplicate($name, $config);
87: }
88: if ($loaded) {
89: return $this->_loaded[$name];
90: }
91:
92: $className = $this->_resolveClassName($objectName);
93: if (!$className || (is_string($className) && !class_exists($className))) {
94: list($plugin, $objectName) = pluginSplit($objectName);
95: $this->_throwMissingClassError($objectName, $plugin);
96: }
97: $instance = $this->_create($className, $name, $config);
98: $this->_loaded[$name] = $instance;
99:
100: return $instance;
101: }
102:
103: /**
104: * Check for duplicate object loading.
105: *
106: * If a duplicate is being loaded and has different configuration, that is
107: * bad and an exception will be raised.
108: *
109: * An exception is raised, as replacing the object will not update any
110: * references other objects may have. Additionally, simply updating the runtime
111: * configuration is not a good option as we may be missing important constructor
112: * logic dependent on the configuration.
113: *
114: * @param string $name The name of the alias in the registry.
115: * @param array $config The config data for the new instance.
116: * @return void
117: * @throws \RuntimeException When a duplicate is found.
118: */
119: protected function _checkDuplicate($name, $config)
120: {
121: /** @var \Cake\Core\InstanceConfigTrait $existing */
122: $existing = $this->_loaded[$name];
123: $msg = sprintf('The "%s" alias has already been loaded', $name);
124: $hasConfig = method_exists($existing, 'config');
125: if (!$hasConfig) {
126: throw new RuntimeException($msg);
127: }
128: if (empty($config)) {
129: return;
130: }
131: $existingConfig = $existing->getConfig();
132: unset($config['enabled'], $existingConfig['enabled']);
133:
134: $fail = false;
135: foreach ($config as $key => $value) {
136: if (!array_key_exists($key, $existingConfig)) {
137: $fail = true;
138: break;
139: }
140: if (isset($existingConfig[$key]) && $existingConfig[$key] !== $value) {
141: $fail = true;
142: break;
143: }
144: }
145: if ($fail) {
146: $msg .= ' with the following config: ';
147: $msg .= var_export($existingConfig, true);
148: $msg .= ' which differs from ' . var_export($config, true);
149: throw new RuntimeException($msg);
150: }
151: }
152:
153: /**
154: * Should resolve the classname for a given object type.
155: *
156: * @param string $class The class to resolve.
157: * @return string|bool The resolved name or false for failure.
158: */
159: abstract protected function _resolveClassName($class);
160:
161: /**
162: * Throw an exception when the requested object name is missing.
163: *
164: * @param string $class The class that is missing.
165: * @param string $plugin The plugin $class is missing from.
166: * @return void
167: * @throws \Exception
168: */
169: abstract protected function _throwMissingClassError($class, $plugin);
170:
171: /**
172: * Create an instance of a given classname.
173: *
174: * This method should construct and do any other initialization logic
175: * required.
176: *
177: * @param string $class The class to build.
178: * @param string $alias The alias of the object.
179: * @param array $config The Configuration settings for construction
180: * @return mixed
181: */
182: abstract protected function _create($class, $alias, $config);
183:
184: /**
185: * Get the list of loaded objects.
186: *
187: * @return string[] List of object names.
188: */
189: public function loaded()
190: {
191: return array_keys($this->_loaded);
192: }
193:
194: /**
195: * Check whether or not a given object is loaded.
196: *
197: * @param string $name The object name to check for.
198: * @return bool True is object is loaded else false.
199: */
200: public function has($name)
201: {
202: return isset($this->_loaded[$name]);
203: }
204:
205: /**
206: * Get loaded object instance.
207: *
208: * @param string $name Name of object.
209: * @return object|null Object instance if loaded else null.
210: */
211: public function get($name)
212: {
213: if (isset($this->_loaded[$name])) {
214: return $this->_loaded[$name];
215: }
216:
217: return null;
218: }
219:
220: /**
221: * Provide public read access to the loaded objects
222: *
223: * @param string $name Name of property to read
224: * @return mixed
225: */
226: public function __get($name)
227: {
228: return $this->get($name);
229: }
230:
231: /**
232: * Provide isset access to _loaded
233: *
234: * @param string $name Name of object being checked.
235: * @return bool
236: */
237: public function __isset($name)
238: {
239: return isset($this->_loaded[$name]);
240: }
241:
242: /**
243: * Sets an object.
244: *
245: * @param string $name Name of a property to set.
246: * @param mixed $object Object to set.
247: * @return void
248: */
249: public function __set($name, $object)
250: {
251: $this->set($name, $object);
252: }
253:
254: /**
255: * Unsets an object.
256: *
257: * @param string $name Name of a property to unset.
258: * @return void
259: */
260: public function __unset($name)
261: {
262: $this->unload($name);
263: }
264:
265: /**
266: * Normalizes an object array, creates an array that makes lazy loading
267: * easier
268: *
269: * @param array $objects Array of child objects to normalize.
270: * @return array Array of normalized objects.
271: */
272: public function normalizeArray($objects)
273: {
274: $normal = [];
275: foreach ($objects as $i => $objectName) {
276: $config = [];
277: if (!is_int($i)) {
278: $config = (array)$objectName;
279: $objectName = $i;
280: }
281: list(, $name) = pluginSplit($objectName);
282: if (isset($config['class'])) {
283: $normal[$name] = $config;
284: } else {
285: $normal[$name] = ['class' => $objectName, 'config' => $config];
286: }
287: }
288:
289: return $normal;
290: }
291:
292: /**
293: * Clear loaded instances in the registry.
294: *
295: * If the registry subclass has an event manager, the objects will be detached from events as well.
296: *
297: * @return $this
298: */
299: public function reset()
300: {
301: foreach (array_keys($this->_loaded) as $name) {
302: $this->unload($name);
303: }
304:
305: return $this;
306: }
307:
308: /**
309: * Set an object directly into the registry by name.
310: *
311: * If this collection implements events, the passed object will
312: * be attached into the event manager
313: *
314: * @param string $objectName The name of the object to set in the registry.
315: * @param object $object instance to store in the registry
316: * @return $this
317: */
318: public function set($objectName, $object)
319: {
320: list(, $name) = pluginSplit($objectName);
321:
322: // Just call unload if the object was loaded before
323: if (array_key_exists($objectName, $this->_loaded)) {
324: $this->unload($objectName);
325: }
326: if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
327: $this->getEventManager()->on($object);
328: }
329: $this->_loaded[$name] = $object;
330:
331: return $this;
332: }
333:
334: /**
335: * Remove an object from the registry.
336: *
337: * If this registry has an event manager, the object will be detached from any events as well.
338: *
339: * @param string $objectName The name of the object to remove from the registry.
340: * @return $this
341: */
342: public function unload($objectName)
343: {
344: if (empty($this->_loaded[$objectName])) {
345: list($plugin, $objectName) = pluginSplit($objectName);
346: $this->_throwMissingClassError($objectName, $plugin);
347: }
348:
349: $object = $this->_loaded[$objectName];
350: if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
351: $this->getEventManager()->off($object);
352: }
353: unset($this->_loaded[$objectName]);
354:
355: return $this;
356: }
357:
358: /**
359: * Returns an array iterator.
360: *
361: * @return \ArrayIterator
362: */
363: public function getIterator()
364: {
365: return new ArrayIterator($this->_loaded);
366: }
367:
368: /**
369: * Returns the number of loaded objects.
370: *
371: * @return int
372: */
373: public function count()
374: {
375: return count($this->_loaded);
376: }
377:
378: /**
379: * Debug friendly object properties.
380: *
381: * @return array
382: */
383: public function __debugInfo()
384: {
385: $properties = get_object_vars($this);
386: if (isset($properties['_loaded'])) {
387: $properties['_loaded'] = array_keys($properties['_loaded']);
388: }
389:
390: return $properties;
391: }
392: }
393: