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