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\ORM;
16:
17: use BadMethodCallException;
18: use Cake\Core\App;
19: use Cake\Core\ObjectRegistry;
20: use Cake\Event\EventDispatcherInterface;
21: use Cake\Event\EventDispatcherTrait;
22: use Cake\ORM\Exception\MissingBehaviorException;
23: use LogicException;
24:
25: /**
26: * BehaviorRegistry is used as a registry for loaded behaviors and handles loading
27: * and constructing behavior objects.
28: *
29: * This class also provides method for checking and dispatching behavior methods.
30: */
31: class BehaviorRegistry extends ObjectRegistry implements EventDispatcherInterface
32: {
33:
34: use EventDispatcherTrait;
35:
36: /**
37: * The table using this registry.
38: *
39: * @var \Cake\ORM\Table
40: */
41: protected $_table;
42:
43: /**
44: * Method mappings.
45: *
46: * @var array
47: */
48: protected $_methodMap = [];
49:
50: /**
51: * Finder method mappings.
52: *
53: * @var array
54: */
55: protected $_finderMap = [];
56:
57: /**
58: * Constructor
59: *
60: * @param \Cake\ORM\Table|null $table The table this registry is attached to.
61: */
62: public function __construct($table = null)
63: {
64: if ($table !== null) {
65: $this->setTable($table);
66: }
67: }
68:
69: /**
70: * Attaches a table instance to this registry.
71: *
72: * @param \Cake\ORM\Table $table The table this registry is attached to.
73: * @return void
74: */
75: public function setTable(Table $table)
76: {
77: $this->_table = $table;
78: $eventManager = $table->getEventManager();
79: if ($eventManager !== null) {
80: $this->setEventManager($eventManager);
81: }
82: }
83:
84: /**
85: * Resolve a behavior classname.
86: *
87: * @param string $class Partial classname to resolve.
88: * @return string|null Either the correct classname or null.
89: * @since 3.5.7
90: */
91: public static function className($class)
92: {
93: $result = App::className($class, 'Model/Behavior', 'Behavior');
94: if (!$result) {
95: $result = App::className($class, 'ORM/Behavior', 'Behavior');
96: }
97:
98: return $result ?: null;
99: }
100:
101: /**
102: * Resolve a behavior classname.
103: *
104: * Part of the template method for Cake\Core\ObjectRegistry::load()
105: *
106: * @param string $class Partial classname to resolve.
107: * @return string|false Either the correct classname or false.
108: */
109: protected function _resolveClassName($class)
110: {
111: return static::className($class) ?: false;
112: }
113:
114: /**
115: * Throws an exception when a behavior is missing.
116: *
117: * Part of the template method for Cake\Core\ObjectRegistry::load()
118: * and Cake\Core\ObjectRegistry::unload()
119: *
120: * @param string $class The classname that is missing.
121: * @param string $plugin The plugin the behavior is missing in.
122: * @return void
123: * @throws \Cake\ORM\Exception\MissingBehaviorException
124: */
125: protected function _throwMissingClassError($class, $plugin)
126: {
127: throw new MissingBehaviorException([
128: 'class' => $class . 'Behavior',
129: 'plugin' => $plugin
130: ]);
131: }
132:
133: /**
134: * Create the behavior instance.
135: *
136: * Part of the template method for Cake\Core\ObjectRegistry::load()
137: * Enabled behaviors will be registered with the event manager.
138: *
139: * @param string $class The classname that is missing.
140: * @param string $alias The alias of the object.
141: * @param array $config An array of config to use for the behavior.
142: * @return \Cake\ORM\Behavior The constructed behavior class.
143: */
144: protected function _create($class, $alias, $config)
145: {
146: $instance = new $class($this->_table, $config);
147: $enable = isset($config['enabled']) ? $config['enabled'] : true;
148: if ($enable) {
149: $this->getEventManager()->on($instance);
150: }
151: $methods = $this->_getMethods($instance, $class, $alias);
152: $this->_methodMap += $methods['methods'];
153: $this->_finderMap += $methods['finders'];
154:
155: return $instance;
156: }
157:
158: /**
159: * Get the behavior methods and ensure there are no duplicates.
160: *
161: * Use the implementedEvents() method to exclude callback methods.
162: * Methods starting with `_` will be ignored, as will methods
163: * declared on Cake\ORM\Behavior
164: *
165: * @param \Cake\ORM\Behavior $instance The behavior to get methods from.
166: * @param string $class The classname that is missing.
167: * @param string $alias The alias of the object.
168: * @return array A list of implemented finders and methods.
169: * @throws \LogicException when duplicate methods are connected.
170: */
171: protected function _getMethods(Behavior $instance, $class, $alias)
172: {
173: $finders = array_change_key_case($instance->implementedFinders());
174: $methods = array_change_key_case($instance->implementedMethods());
175:
176: foreach ($finders as $finder => $methodName) {
177: if (isset($this->_finderMap[$finder]) && $this->has($this->_finderMap[$finder][0])) {
178: $duplicate = $this->_finderMap[$finder];
179: $error = sprintf(
180: '%s contains duplicate finder "%s" which is already provided by "%s"',
181: $class,
182: $finder,
183: $duplicate[0]
184: );
185: throw new LogicException($error);
186: }
187: $finders[$finder] = [$alias, $methodName];
188: }
189:
190: foreach ($methods as $method => $methodName) {
191: if (isset($this->_methodMap[$method]) && $this->has($this->_methodMap[$method][0])) {
192: $duplicate = $this->_methodMap[$method];
193: $error = sprintf(
194: '%s contains duplicate method "%s" which is already provided by "%s"',
195: $class,
196: $method,
197: $duplicate[0]
198: );
199: throw new LogicException($error);
200: }
201: $methods[$method] = [$alias, $methodName];
202: }
203:
204: return compact('methods', 'finders');
205: }
206:
207: /**
208: * Check if any loaded behavior implements a method.
209: *
210: * Will return true if any behavior provides a public non-finder method
211: * with the chosen name.
212: *
213: * @param string $method The method to check for.
214: * @return bool
215: */
216: public function hasMethod($method)
217: {
218: $method = strtolower($method);
219:
220: return isset($this->_methodMap[$method]);
221: }
222:
223: /**
224: * Check if any loaded behavior implements the named finder.
225: *
226: * Will return true if any behavior provides a public method with
227: * the chosen name.
228: *
229: * @param string $method The method to check for.
230: * @return bool
231: */
232: public function hasFinder($method)
233: {
234: $method = strtolower($method);
235:
236: return isset($this->_finderMap[$method]);
237: }
238:
239: /**
240: * Invoke a method on a behavior.
241: *
242: * @param string $method The method to invoke.
243: * @param array $args The arguments you want to invoke the method with.
244: * @return mixed The return value depends on the underlying behavior method.
245: * @throws \BadMethodCallException When the method is unknown.
246: */
247: public function call($method, array $args = [])
248: {
249: $method = strtolower($method);
250: if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) {
251: list($behavior, $callMethod) = $this->_methodMap[$method];
252:
253: return call_user_func_array([$this->_loaded[$behavior], $callMethod], $args);
254: }
255:
256: throw new BadMethodCallException(
257: sprintf('Cannot call "%s" it does not belong to any attached behavior.', $method)
258: );
259: }
260:
261: /**
262: * Invoke a finder on a behavior.
263: *
264: * @param string $type The finder type to invoke.
265: * @param array $args The arguments you want to invoke the method with.
266: * @return mixed The return value depends on the underlying behavior method.
267: * @throws \BadMethodCallException When the method is unknown.
268: */
269: public function callFinder($type, array $args = [])
270: {
271: $type = strtolower($type);
272:
273: if ($this->hasFinder($type) && $this->has($this->_finderMap[$type][0])) {
274: list($behavior, $callMethod) = $this->_finderMap[$type];
275:
276: return call_user_func_array([$this->_loaded[$behavior], $callMethod], $args);
277: }
278:
279: throw new BadMethodCallException(
280: sprintf('Cannot call finder "%s" it does not belong to any attached behavior.', $type)
281: );
282: }
283: }
284: