<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\ORM\Locator;
use Cake\Core\App;
use Cake\Datasource\ConnectionManager;
use Cake\ORM\AssociationCollection;
use Cake\ORM\Table;
use Cake\Utility\Inflector;
use RuntimeException;
/**
* Provides a default registry/factory for Table objects.
*/
class TableLocator implements LocatorInterface
{
/**
* Contains a list of locations where table classes should be looked for.
*
* @var array
*/
protected $locations = [];
/**
* Configuration for aliases.
*
* @var array
*/
protected $_config = [];
/**
* Instances that belong to the registry.
*
* @var \Cake\ORM\Table[]
*/
protected $_instances = [];
/**
* Contains a list of Table objects that were created out of the
* built-in Table class. The list is indexed by table alias
*
* @var \Cake\ORM\Table[]
*/
protected $_fallbacked = [];
/**
* Contains a list of options that were passed to get() method.
*
* @var array
*/
protected $_options = [];
/**
* Constructor.
*
* @param array|null $locations Locations where tables should be looked for.
* If none provided, the default `Model\Table` under your app's namespace is used.
*/
public function __construct(array $locations = null)
{
if ($locations === null) {
$locations = [
'Model/Table',
];
}
foreach ($locations as $location) {
$this->addLocation($location);
}
}
/**
* Stores a list of options to be used when instantiating an object
* with a matching alias.
*
* @param string|array $alias Name of the alias or array to completely overwrite current config.
* @param array|null $options list of options for the alias
* @return $this
* @throws \RuntimeException When you attempt to configure an existing table instance.
*/
public function setConfig($alias, $options = null)
{
if (!is_string($alias)) {
$this->_config = $alias;
return $this;
}
if (isset($this->_instances[$alias])) {
throw new RuntimeException(sprintf(
'You cannot configure "%s", it has already been constructed.',
$alias
));
}
$this->_config[$alias] = $options;
return $this;
}
/**
* Returns configuration for an alias or the full configuration array for all aliases.
*
* @param string|null $alias Alias to get config for, null for complete config.
* @return array The config data.
*/
public function getConfig($alias = null)
{
if ($alias === null) {
return $this->_config;
}
return isset($this->_config[$alias]) ? $this->_config[$alias] : [];
}
/**
* Stores a list of options to be used when instantiating an object
* with a matching alias.
*
* The options that can be stored are those that are recognized by `get()`
* If second argument is omitted, it will return the current settings
* for $alias.
*
* If no arguments are passed it will return the full configuration array for
* all aliases
*
* @deprecated 3.4.0 Use setConfig()/getConfig() instead.
* @param string|array|null $alias Name of the alias
* @param array|null $options list of options for the alias
* @return array The config data.
* @throws \RuntimeException When you attempt to configure an existing table instance.
*/
public function config($alias = null, $options = null)
{
deprecationWarning(
'TableLocator::config() is deprecated. ' .
'Use getConfig()/setConfig() instead.'
);
if ($alias !== null) {
if (is_string($alias) && $options === null) {
return $this->getConfig($alias);
}
$this->setConfig($alias, $options);
}
return $this->getConfig($alias);
}
/**
* Get a table instance from the registry.
*
* Tables are only created once until the registry is flushed.
* This means that aliases must be unique across your application.
* This is important because table associations are resolved at runtime
* and cyclic references need to be handled correctly.
*
* The options that can be passed are the same as in Cake\ORM\Table::__construct(), but the
* `className` key is also recognized.
*
* ### Options
*
* - `className` Define the specific class name to use. If undefined, CakePHP will generate the
* class name based on the alias. For example 'Users' would result in
* `App\Model\Table\UsersTable` being used. If this class does not exist,
* then the default `Cake\ORM\Table` class will be used. By setting the `className`
* option you can define the specific class to use. The className option supports
* plugin short class references {@link Cake\Core\App::shortName()}.
* - `table` Define the table name to use. If undefined, this option will default to the underscored
* version of the alias name.
* - `connection` Inject the specific connection object to use. If this option and `connectionName` are undefined,
* The table class' `defaultConnectionName()` method will be invoked to fetch the connection name.
* - `connectionName` Define the connection name to use. The named connection will be fetched from
* Cake\Datasource\ConnectionManager.
*
* *Note* If your `$alias` uses plugin syntax only the name part will be used as
* key in the registry. This means that if two plugins, or a plugin and app provide
* the same alias, the registry will only store the first instance.
*
* @param string $alias The alias name you want to get.
* @param array $options The options you want to build the table with.
* If a table has already been loaded the options will be ignored.
* @return \Cake\ORM\Table
* @throws \RuntimeException When you try to configure an alias that already exists.
*/
public function get($alias, array $options = [])
{
if (isset($this->_instances[$alias])) {
if (!empty($options) && $this->_options[$alias] !== $options) {
throw new RuntimeException(sprintf(
'You cannot configure "%s", it already exists in the registry.',
$alias
));
}
return $this->_instances[$alias];
}
$this->_options[$alias] = $options;
list(, $classAlias) = pluginSplit($alias);
$options = ['alias' => $classAlias] + $options;
if (isset($this->_config[$alias])) {
$options += $this->_config[$alias];
}
$className = $this->_getClassName($alias, $options);
if ($className) {
$options['className'] = $className;
} else {
if (empty($options['className'])) {
$options['className'] = Inflector::camelize($alias);
}
if (!isset($options['table']) && strpos($options['className'], '\\') === false) {
list(, $table) = pluginSplit($options['className']);
$options['table'] = Inflector::underscore($table);
}
$options['className'] = 'Cake\ORM\Table';
}
if (empty($options['connection'])) {
if (!empty($options['connectionName'])) {
$connectionName = $options['connectionName'];
} else {
/** @var \Cake\ORM\Table $className */
$className = $options['className'];
$connectionName = $className::defaultConnectionName();
}
$options['connection'] = ConnectionManager::get($connectionName);
}
if (empty($options['associations'])) {
$associations = new AssociationCollection($this);
$options['associations'] = $associations;
}
$options['registryAlias'] = $alias;
$this->_instances[$alias] = $this->_create($options);
if ($options['className'] === 'Cake\ORM\Table') {
$this->_fallbacked[$alias] = $this->_instances[$alias];
}
return $this->_instances[$alias];
}
/**
* Gets the table class name.
*
* @param string $alias The alias name you want to get.
* @param array $options Table options array.
* @return string|false
*/
protected function _getClassName($alias, array $options = [])
{
if (empty($options['className'])) {
$options['className'] = Inflector::camelize($alias);
}
if (strpos($options['className'], '\\') !== false && class_exists($options['className'])) {
return $options['className'];
}
foreach ($this->locations as $location) {
$class = App::className($options['className'], $location, 'Table');
if ($class !== false) {
return $class;
}
}
return false;
}
/**
* Wrapper for creating table instances
*
* @param array $options The alias to check for.
* @return \Cake\ORM\Table
*/
protected function _create(array $options)
{
return new $options['className']($options);
}
/**
* {@inheritDoc}
*/
public function exists($alias)
{
return isset($this->_instances[$alias]);
}
/**
* {@inheritDoc}
*/
public function set($alias, Table $object)
{
return $this->_instances[$alias] = $object;
}
/**
* {@inheritDoc}
*/
public function clear()
{
$this->_instances = [];
$this->_config = [];
$this->_fallbacked = [];
}
/**
* Returns the list of tables that were created by this registry that could
* not be instantiated from a specific subclass. This method is useful for
* debugging common mistakes when setting up associations or created new table
* classes.
*
* @return \Cake\ORM\Table[]
*/
public function genericInstances()
{
return $this->_fallbacked;
}
/**
* {@inheritDoc}
*/
public function remove($alias)
{
unset(
$this->_instances[$alias],
$this->_config[$alias],
$this->_fallbacked[$alias]
);
}
/**
* Adds a location where tables should be looked for.
*
* @param string $location Location to add.
* @return $this
*
* @since 3.8.0
*/
public function addLocation($location)
{
$location = str_replace('\\', '/', $location);
$this->locations[] = trim($location, '/');
return $this;
}
}