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.1.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\ORM\Locator;
16:
17: use Cake\Core\App;
18: use Cake\Datasource\ConnectionManager;
19: use Cake\ORM\AssociationCollection;
20: use Cake\ORM\Table;
21: use Cake\Utility\Inflector;
22: use RuntimeException;
23:
24: /**
25: * Provides a default registry/factory for Table objects.
26: */
27: class TableLocator implements LocatorInterface
28: {
29: /**
30: * Contains a list of locations where table classes should be looked for.
31: *
32: * @var array
33: */
34: protected $locations = [];
35:
36: /**
37: * Configuration for aliases.
38: *
39: * @var array
40: */
41: protected $_config = [];
42:
43: /**
44: * Instances that belong to the registry.
45: *
46: * @var \Cake\ORM\Table[]
47: */
48: protected $_instances = [];
49:
50: /**
51: * Contains a list of Table objects that were created out of the
52: * built-in Table class. The list is indexed by table alias
53: *
54: * @var \Cake\ORM\Table[]
55: */
56: protected $_fallbacked = [];
57:
58: /**
59: * Contains a list of options that were passed to get() method.
60: *
61: * @var array
62: */
63: protected $_options = [];
64:
65: /**
66: * Constructor.
67: *
68: * @param array|null $locations Locations where tables should be looked for.
69: * If none provided, the default `Model\Table` under your app's namespace is used.
70: */
71: public function __construct(array $locations = null)
72: {
73: if ($locations === null) {
74: $locations = [
75: 'Model/Table',
76: ];
77: }
78:
79: foreach ($locations as $location) {
80: $this->addLocation($location);
81: }
82: }
83:
84: /**
85: * Stores a list of options to be used when instantiating an object
86: * with a matching alias.
87: *
88: * @param string|array $alias Name of the alias or array to completely overwrite current config.
89: * @param array|null $options list of options for the alias
90: * @return $this
91: * @throws \RuntimeException When you attempt to configure an existing table instance.
92: */
93: public function setConfig($alias, $options = null)
94: {
95: if (!is_string($alias)) {
96: $this->_config = $alias;
97:
98: return $this;
99: }
100:
101: if (isset($this->_instances[$alias])) {
102: throw new RuntimeException(sprintf(
103: 'You cannot configure "%s", it has already been constructed.',
104: $alias
105: ));
106: }
107:
108: $this->_config[$alias] = $options;
109:
110: return $this;
111: }
112:
113: /**
114: * Returns configuration for an alias or the full configuration array for all aliases.
115: *
116: * @param string|null $alias Alias to get config for, null for complete config.
117: * @return array The config data.
118: */
119: public function getConfig($alias = null)
120: {
121: if ($alias === null) {
122: return $this->_config;
123: }
124:
125: return isset($this->_config[$alias]) ? $this->_config[$alias] : [];
126: }
127:
128: /**
129: * Stores a list of options to be used when instantiating an object
130: * with a matching alias.
131: *
132: * The options that can be stored are those that are recognized by `get()`
133: * If second argument is omitted, it will return the current settings
134: * for $alias.
135: *
136: * If no arguments are passed it will return the full configuration array for
137: * all aliases
138: *
139: * @deprecated 3.4.0 Use setConfig()/getConfig() instead.
140: * @param string|array|null $alias Name of the alias
141: * @param array|null $options list of options for the alias
142: * @return array The config data.
143: * @throws \RuntimeException When you attempt to configure an existing table instance.
144: */
145: public function config($alias = null, $options = null)
146: {
147: deprecationWarning(
148: 'TableLocator::config() is deprecated. ' .
149: 'Use getConfig()/setConfig() instead.'
150: );
151: if ($alias !== null) {
152: if (is_string($alias) && $options === null) {
153: return $this->getConfig($alias);
154: }
155:
156: $this->setConfig($alias, $options);
157: }
158:
159: return $this->getConfig($alias);
160: }
161:
162: /**
163: * Get a table instance from the registry.
164: *
165: * Tables are only created once until the registry is flushed.
166: * This means that aliases must be unique across your application.
167: * This is important because table associations are resolved at runtime
168: * and cyclic references need to be handled correctly.
169: *
170: * The options that can be passed are the same as in Cake\ORM\Table::__construct(), but the
171: * `className` key is also recognized.
172: *
173: * ### Options
174: *
175: * - `className` Define the specific class name to use. If undefined, CakePHP will generate the
176: * class name based on the alias. For example 'Users' would result in
177: * `App\Model\Table\UsersTable` being used. If this class does not exist,
178: * then the default `Cake\ORM\Table` class will be used. By setting the `className`
179: * option you can define the specific class to use. The className option supports
180: * plugin short class references {@link Cake\Core\App::shortName()}.
181: * - `table` Define the table name to use. If undefined, this option will default to the underscored
182: * version of the alias name.
183: * - `connection` Inject the specific connection object to use. If this option and `connectionName` are undefined,
184: * The table class' `defaultConnectionName()` method will be invoked to fetch the connection name.
185: * - `connectionName` Define the connection name to use. The named connection will be fetched from
186: * Cake\Datasource\ConnectionManager.
187: *
188: * *Note* If your `$alias` uses plugin syntax only the name part will be used as
189: * key in the registry. This means that if two plugins, or a plugin and app provide
190: * the same alias, the registry will only store the first instance.
191: *
192: * @param string $alias The alias name you want to get.
193: * @param array $options The options you want to build the table with.
194: * If a table has already been loaded the options will be ignored.
195: * @return \Cake\ORM\Table
196: * @throws \RuntimeException When you try to configure an alias that already exists.
197: */
198: public function get($alias, array $options = [])
199: {
200: if (isset($this->_instances[$alias])) {
201: if (!empty($options) && $this->_options[$alias] !== $options) {
202: throw new RuntimeException(sprintf(
203: 'You cannot configure "%s", it already exists in the registry.',
204: $alias
205: ));
206: }
207:
208: return $this->_instances[$alias];
209: }
210:
211: $this->_options[$alias] = $options;
212: list(, $classAlias) = pluginSplit($alias);
213: $options = ['alias' => $classAlias] + $options;
214:
215: if (isset($this->_config[$alias])) {
216: $options += $this->_config[$alias];
217: }
218:
219: $className = $this->_getClassName($alias, $options);
220: if ($className) {
221: $options['className'] = $className;
222: } else {
223: if (empty($options['className'])) {
224: $options['className'] = Inflector::camelize($alias);
225: }
226: if (!isset($options['table']) && strpos($options['className'], '\\') === false) {
227: list(, $table) = pluginSplit($options['className']);
228: $options['table'] = Inflector::underscore($table);
229: }
230: $options['className'] = 'Cake\ORM\Table';
231: }
232:
233: if (empty($options['connection'])) {
234: if (!empty($options['connectionName'])) {
235: $connectionName = $options['connectionName'];
236: } else {
237: /* @var \Cake\ORM\Table $className */
238: $className = $options['className'];
239: $connectionName = $className::defaultConnectionName();
240: }
241: $options['connection'] = ConnectionManager::get($connectionName);
242: }
243: if (empty($options['associations'])) {
244: $associations = new AssociationCollection($this);
245: $options['associations'] = $associations;
246: }
247:
248: $options['registryAlias'] = $alias;
249: $this->_instances[$alias] = $this->_create($options);
250:
251: if ($options['className'] === 'Cake\ORM\Table') {
252: $this->_fallbacked[$alias] = $this->_instances[$alias];
253: }
254:
255: return $this->_instances[$alias];
256: }
257:
258: /**
259: * Gets the table class name.
260: *
261: * @param string $alias The alias name you want to get.
262: * @param array $options Table options array.
263: * @return string|false
264: */
265: protected function _getClassName($alias, array $options = [])
266: {
267: if (empty($options['className'])) {
268: $options['className'] = Inflector::camelize($alias);
269: }
270:
271: if (strpos($options['className'], '\\') !== false && class_exists($options['className'])) {
272: return $options['className'];
273: }
274:
275: foreach ($this->locations as $location) {
276: $class = App::className($options['className'], $location, 'Table');
277: if ($class !== false) {
278: return $class;
279: }
280: }
281:
282: return false;
283: }
284:
285: /**
286: * Wrapper for creating table instances
287: *
288: * @param array $options The alias to check for.
289: * @return \Cake\ORM\Table
290: */
291: protected function _create(array $options)
292: {
293: return new $options['className']($options);
294: }
295:
296: /**
297: * {@inheritDoc}
298: */
299: public function exists($alias)
300: {
301: return isset($this->_instances[$alias]);
302: }
303:
304: /**
305: * {@inheritDoc}
306: */
307: public function set($alias, Table $object)
308: {
309: return $this->_instances[$alias] = $object;
310: }
311:
312: /**
313: * {@inheritDoc}
314: */
315: public function clear()
316: {
317: $this->_instances = [];
318: $this->_config = [];
319: $this->_fallbacked = [];
320: }
321:
322: /**
323: * Returns the list of tables that were created by this registry that could
324: * not be instantiated from a specific subclass. This method is useful for
325: * debugging common mistakes when setting up associations or created new table
326: * classes.
327: *
328: * @return \Cake\ORM\Table[]
329: */
330: public function genericInstances()
331: {
332: return $this->_fallbacked;
333: }
334:
335: /**
336: * {@inheritDoc}
337: */
338: public function remove($alias)
339: {
340: unset(
341: $this->_instances[$alias],
342: $this->_config[$alias],
343: $this->_fallbacked[$alias]
344: );
345: }
346:
347: /**
348: * Adds a location where tables should be looked for.
349: *
350: * @param string $location Location to add.
351: * @return $this
352: *
353: * @since 3.8.0
354: */
355: public function addLocation($location)
356: {
357: $location = str_replace('\\', '/', $location);
358: $this->locations[] = trim($location, '/');
359:
360: return $this;
361: }
362: }
363: