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