AC_services_website_design/system/Config/Factories.php

540 lines
16 KiB
PHP

<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Model;
use Config\Services;
use InvalidArgumentException;
/**
* Factories for creating instances.
*
* Factories allow dynamic loading of components by their path
* and name. The "shared instance" implementation provides a
* large performance boost and helps keep code clean of lengthy
* instantiation checks.
*
* @method static BaseConfig|null config(...$arguments)
* @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null)
* @see \CodeIgniter\Config\FactoriesTest
*/
class Factories
{
/**
* Store of component-specific options, usually
* from CodeIgniter\Config\Factory.
*
* @var array<string, array<string, bool|string|null>>
*/
protected static $options = [];
/**
* Explicit options for the Config
* component to prevent logic loops.
*
* @var array<string, bool|string|null>
*/
private static array $configOptions = [
'component' => 'config',
'path' => 'Config',
'instanceOf' => null,
'getShared' => true,
'preferApp' => true,
];
/**
* Mapping of class aliases to their true Fully Qualified Class Name (FQCN).
*
* Class aliases can be:
* - FQCN. E.g., 'App\Lib\SomeLib'
* - short classname. E.g., 'SomeLib'
* - short classname with sub-directories. E.g., 'Sub/SomeLib'
*
* [component => [alias => FQCN]]
*
* @var array<string, array<string, class-string>>
*/
protected static $aliases = [];
/**
* Store for instances of any component that
* has been requested as "shared".
*
* A multi-dimensional array with components as
* keys to the array of name-indexed instances.
*
* [component => [FQCN => instance]]
*
* @var array<string, array<class-string, object>>
*/
protected static $instances = [];
/**
* Whether the component instances are updated?
*
* @var array<string, true> [component => true]
*
* @internal For caching only
*/
protected static $updated = [];
/**
* Define the class to load. You can *override* the concrete class.
*
* @param string $component Lowercase, plural component name
* @param string $alias Class alias. See the $aliases property.
* @param class-string $classname FQCN to be loaded
*/
public static function define(string $component, string $alias, string $classname): void
{
$component = strtolower($component);
if (isset(self::$aliases[$component][$alias])) {
if (self::$aliases[$component][$alias] === $classname) {
return;
}
throw new InvalidArgumentException(
'Already defined in Factories: ' . $component . ' ' . $alias . ' -> ' . self::$aliases[$component][$alias]
);
}
if (! class_exists($classname)) {
throw new InvalidArgumentException('No such class: ' . $classname);
}
// Force a configuration to exist for this component.
// Otherwise, getOptions() will reset the component.
self::getOptions($component);
self::$aliases[$component][$alias] = $classname;
self::$updated[$component] = true;
}
/**
* Loads instances based on the method component name. Either
* creates a new instance or returns an existing shared instance.
*
* @return object|null
*/
public static function __callStatic(string $component, array $arguments)
{
$component = strtolower($component);
// First argument is the class alias, second is options
$alias = trim(array_shift($arguments), '\\ ');
$options = array_shift($arguments) ?? [];
// Determine the component-specific options
$options = array_merge(self::getOptions($component), $options);
if (! $options['getShared']) {
if (isset(self::$aliases[$component][$alias])) {
$class = self::$aliases[$component][$alias];
return new $class(...$arguments);
}
// Try to locate the class
$class = self::locateClass($options, $alias);
if ($class !== null) {
return new $class(...$arguments);
}
return null;
}
// Check for an existing definition
$instance = self::getDefinedInstance($options, $alias, $arguments);
if ($instance !== null) {
return $instance;
}
// Try to locate the class
if (! $class = self::locateClass($options, $alias)) {
return null;
}
self::createInstance($options['component'], $class, $arguments);
self::setAlias($options['component'], $alias, $class);
return self::$instances[$options['component']][$class];
}
/**
* Gets the defined instance. If not exists, creates new one.
*
* @return object|null
*/
private static function getDefinedInstance(array $options, string $alias, array $arguments)
{
// The alias is already defined.
if (isset(self::$aliases[$options['component']][$alias])) {
$class = self::$aliases[$options['component']][$alias];
// Need to verify if the shared instance matches the request
if (self::verifyInstanceOf($options, $class)) {
// Check for an existing instance
if (isset(self::$instances[$options['component']][$class])) {
return self::$instances[$options['component']][$class];
}
self::createInstance($options['component'], $class, $arguments);
return self::$instances[$options['component']][$class];
}
}
// Try to locate the class
if (! $class = self::locateClass($options, $alias)) {
return null;
}
// Check for an existing instance for the class
if (isset(self::$instances[$options['component']][$class])) {
self::setAlias($options['component'], $alias, $class);
return self::$instances[$options['component']][$class];
}
return null;
}
/**
* Creates the shared instance.
*/
private static function createInstance(string $component, string $class, array $arguments): void
{
self::$instances[$component][$class] = new $class(...$arguments);
self::$updated[$component] = true;
}
/**
* Sets alias
*/
private static function setAlias(string $component, string $alias, string $class): void
{
self::$aliases[$component][$alias] = $class;
self::$updated[$component] = true;
// If a short classname is specified, also register FQCN to share the instance.
if (! isset(self::$aliases[$component][$class]) && ! self::isNamespaced($alias)) {
self::$aliases[$component][$class] = $class;
}
}
/**
* Is the component Config?
*
* @param string $component Lowercase, plural component name
*/
private static function isConfig(string $component): bool
{
return $component === 'config';
}
/**
* Finds a component class
*
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
protected static function locateClass(array $options, string $alias): ?string
{
// Check for low-hanging fruit
if (
class_exists($alias, false)
&& self::verifyPreferApp($options, $alias)
&& self::verifyInstanceOf($options, $alias)
) {
return $alias;
}
// Determine the relative class names we need
$basename = self::getBasename($alias);
$appname = self::isConfig($options['component'])
? 'Config\\' . $basename
: rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename;
// If an App version was requested then see if it verifies
if (
// preferApp is used only for no namespaced class.
! self::isNamespaced($alias)
&& $options['preferApp'] && class_exists($appname)
&& self::verifyInstanceOf($options, $alias)
) {
return $appname;
}
// If we have ruled out an App version and the class exists then try it
if (class_exists($alias) && self::verifyInstanceOf($options, $alias)) {
return $alias;
}
// Have to do this the hard way...
$locator = Services::locator();
// Check if the class alias was namespaced
if (self::isNamespaced($alias)) {
if (! $file = $locator->locateFile($alias, $options['path'])) {
return null;
}
$files = [$file];
}
// No namespace? Search for it
// Check all namespaces, prioritizing App and modules
elseif (! $files = $locator->search($options['path'] . DIRECTORY_SEPARATOR . $alias)) {
return null;
}
// Check all files for a valid class
foreach ($files as $file) {
$class = $locator->getClassname($file);
if ($class && self::verifyInstanceOf($options, $class)) {
return $class;
}
}
return null;
}
/**
* Is the class alias namespaced or not?
*
* @param string $alias Class alias. See the $aliases property.
*/
private static function isNamespaced(string $alias): bool
{
return strpos($alias, '\\') !== false;
}
/**
* Verifies that a class & config satisfy the "preferApp" option
*
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
protected static function verifyPreferApp(array $options, string $alias): bool
{
// Anything without that restriction passes
if (! $options['preferApp']) {
return true;
}
// Special case for Config since its App namespace is actually \Config
if (self::isConfig($options['component'])) {
return strpos($alias, 'Config') === 0;
}
return strpos($alias, APP_NAMESPACE) === 0;
}
/**
* Verifies that a class & config satisfy the "instanceOf" option
*
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
protected static function verifyInstanceOf(array $options, string $alias): bool
{
// Anything without that restriction passes
if (! $options['instanceOf']) {
return true;
}
return is_a($alias, $options['instanceOf'], true);
}
/**
* Returns the component-specific configuration
*
* @param string $component Lowercase, plural component name
*
* @return array<string, bool|string|null>
*
* @internal For testing only
* @testTag
*/
public static function getOptions(string $component): array
{
$component = strtolower($component);
// Check for a stored version
if (isset(self::$options[$component])) {
return self::$options[$component];
}
$values = self::isConfig($component)
// Handle Config as a special case to prevent logic loops
? self::$configOptions
// Load values from the best Factory configuration (will include Registrars)
: config('Factory')->{$component} ?? [];
// The setOptions() reset the component. So getOptions() may reset
// the component.
return self::setOptions($component, $values);
}
/**
* Normalizes, stores, and returns the configuration for a specific component
*
* @param string $component Lowercase, plural component name
* @param array $values option values
*
* @return array<string, bool|string|null> The result after applying defaults and normalization
*/
public static function setOptions(string $component, array $values): array
{
$component = strtolower($component);
// Allow the config to replace the component name, to support "aliases"
$values['component'] = strtolower($values['component'] ?? $component);
// Reset this component so instances can be rediscovered with the updated config
self::reset($values['component']);
// If no path was available then use the component
$values['path'] = trim($values['path'] ?? ucfirst($values['component']), '\\ ');
// Add defaults for any missing values
$values = array_merge(Factory::$default, $values);
// Store the result to the supplied name and potential alias
self::$options[$component] = $values;
self::$options[$values['component']] = $values;
return $values;
}
/**
* Resets the static arrays, optionally just for one component
*
* @param string|null $component Lowercase, plural component name
*
* @return void
*/
public static function reset(?string $component = null)
{
if ($component !== null) {
unset(
self::$options[$component],
self::$aliases[$component],
self::$instances[$component],
self::$updated[$component]
);
return;
}
self::$options = [];
self::$aliases = [];
self::$instances = [];
self::$updated = [];
}
/**
* Helper method for injecting mock instances
*
* @param string $component Lowercase, plural component name
* @param string $alias Class alias. See the $aliases property.
*
* @return void
*
* @internal For testing only
* @testTag
*/
public static function injectMock(string $component, string $alias, object $instance)
{
$component = strtolower($component);
// Force a configuration to exist for this component
self::getOptions($component);
$class = get_class($instance);
self::$instances[$component][$class] = $instance;
self::$aliases[$component][$alias] = $class;
if (self::isConfig($component)) {
if (self::isNamespaced($alias)) {
self::$aliases[$component][self::getBasename($alias)] = $class;
} else {
self::$aliases[$component]['Config\\' . $alias] = $class;
}
}
}
/**
* Gets a basename from a class alias, namespaced or not.
*
* @internal For testing only
* @testTag
*/
public static function getBasename(string $alias): string
{
// Determine the basename
if ($basename = strrchr($alias, '\\')) {
return substr($basename, 1);
}
return $alias;
}
/**
* Gets component data for caching.
*
* @internal For caching only
*/
public static function getComponentInstances(string $component): array
{
if (! isset(self::$aliases[$component])) {
return [
'options' => [],
'aliases' => [],
'instances' => [],
];
}
return [
'options' => self::$options[$component],
'aliases' => self::$aliases[$component],
'instances' => self::$instances[$component],
];
}
/**
* Sets component data
*
* @internal For caching only
*/
public static function setComponentInstances(string $component, array $data): void
{
self::$options[$component] = $data['options'];
self::$aliases[$component] = $data['aliases'];
self::$instances[$component] = $data['instances'];
unset(self::$updated[$component]);
}
/**
* Whether the component instances are updated?
*
* @internal For caching only
*/
public static function isUpdated(string $component): bool
{
return isset(self::$updated[$component]);
}
}