Initial
This commit is contained in:
316
system/Config/Factories.php
Normal file
316
system/Config/Factories.php
Normal file
@@ -0,0 +1,316 @@
|
||||
<?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\Model;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* 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 config(...$arguments)
|
||||
* @method static Model models(...$arguments)
|
||||
*/
|
||||
class Factories
|
||||
{
|
||||
/**
|
||||
* Store of component-specific options, usually
|
||||
* from CodeIgniter\Config\Factory.
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
protected static $options = [];
|
||||
|
||||
/**
|
||||
* Explicit options for the Config
|
||||
* component to prevent logic loops.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private static $configOptions = [
|
||||
'component' => 'config',
|
||||
'path' => 'Config',
|
||||
'instanceOf' => null,
|
||||
'getShared' => true,
|
||||
'preferApp' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of class basenames (no namespace) to
|
||||
* their instances.
|
||||
*
|
||||
* @var array<string, string[]>
|
||||
*/
|
||||
protected static $basenames = [];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
protected static $instances = [];
|
||||
|
||||
/**
|
||||
* Loads instances based on the method component name. Either
|
||||
* creates a new instance or returns an existing shared instance.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function __callStatic(string $component, array $arguments)
|
||||
{
|
||||
// First argument is the name, second is options
|
||||
$name = trim(array_shift($arguments), '\\ ');
|
||||
$options = array_shift($arguments) ?? [];
|
||||
|
||||
// Determine the component-specific options
|
||||
$options = array_merge(self::getOptions(strtolower($component)), $options);
|
||||
|
||||
if (! $options['getShared']) {
|
||||
if ($class = self::locateClass($options, $name)) {
|
||||
return new $class(...$arguments);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$basename = self::getBasename($name);
|
||||
|
||||
// Check for an existing instance
|
||||
if (isset(self::$basenames[$options['component']][$basename])) {
|
||||
$class = self::$basenames[$options['component']][$basename];
|
||||
|
||||
// Need to verify if the shared instance matches the request
|
||||
if (self::verifyInstanceOf($options, $class)) {
|
||||
return self::$instances[$options['component']][$class];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to locate the class
|
||||
if (! $class = self::locateClass($options, $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
self::$instances[$options['component']][$class] = new $class(...$arguments);
|
||||
self::$basenames[$options['component']][$basename] = $class;
|
||||
|
||||
return self::$instances[$options['component']][$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a component class
|
||||
*
|
||||
* @param array $options The array of component-specific directives
|
||||
* @param string $name Class name, namespace optional
|
||||
*/
|
||||
protected static function locateClass(array $options, string $name): ?string
|
||||
{
|
||||
// Check for low-hanging fruit
|
||||
if (class_exists($name, false) && self::verifyPreferApp($options, $name) && self::verifyInstanceOf($options, $name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
// Determine the relative class names we need
|
||||
$basename = self::getBasename($name);
|
||||
$appname = $options['component'] === 'config'
|
||||
? 'Config\\' . $basename
|
||||
: rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename;
|
||||
|
||||
// If an App version was requested then see if it verifies
|
||||
if ($options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $name)) {
|
||||
return $appname;
|
||||
}
|
||||
|
||||
// If we have ruled out an App version and the class exists then try it
|
||||
if (class_exists($name) && self::verifyInstanceOf($options, $name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
// Have to do this the hard way...
|
||||
$locator = Services::locator();
|
||||
|
||||
// Check if the class was namespaced
|
||||
if (strpos($name, '\\') !== false) {
|
||||
if (! $file = $locator->locateFile($name, $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 . $name)) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a class & config satisfy the "preferApp" option
|
||||
*
|
||||
* @param array $options The array of component-specific directives
|
||||
* @param string $name Class name, namespace optional
|
||||
*/
|
||||
protected static function verifyPreferApp(array $options, string $name): bool
|
||||
{
|
||||
// Anything without that restriction passes
|
||||
if (! $options['preferApp']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Special case for Config since its App namespace is actually \Config
|
||||
if ($options['component'] === 'config') {
|
||||
return strpos($name, 'Config') === 0;
|
||||
}
|
||||
|
||||
return strpos($name, APP_NAMESPACE) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a class & config satisfy the "instanceOf" option
|
||||
*
|
||||
* @param array $options The array of component-specific directives
|
||||
* @param string $name Class name, namespace optional
|
||||
*/
|
||||
protected static function verifyInstanceOf(array $options, string $name): bool
|
||||
{
|
||||
// Anything without that restriction passes
|
||||
if (! $options['instanceOf']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return is_a($name, $options['instanceOf'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component-specific configuration
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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 = $component === 'config'
|
||||
// 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} ?? [];
|
||||
|
||||
return self::setOptions($component, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes, stores, and returns the configuration for a specific component
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
*
|
||||
* @return array<string, mixed> The result after applying defaults and normalization
|
||||
*/
|
||||
public static function setOptions(string $component, array $values): array
|
||||
{
|
||||
// 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 $component Lowercase, plural component name
|
||||
*/
|
||||
public static function reset(?string $component = null)
|
||||
{
|
||||
if ($component) {
|
||||
unset(
|
||||
static::$options[$component],
|
||||
static::$basenames[$component],
|
||||
static::$instances[$component]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static::$options = [];
|
||||
static::$basenames = [];
|
||||
static::$instances = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for injecting mock instances
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
* @param string $name The name of the instance
|
||||
*/
|
||||
public static function injectMock(string $component, string $name, object $instance)
|
||||
{
|
||||
// Force a configuration to exist for this component
|
||||
$component = strtolower($component);
|
||||
self::getOptions($component);
|
||||
|
||||
$class = get_class($instance);
|
||||
$basename = self::getBasename($name);
|
||||
|
||||
self::$instances[$component][$class] = $instance;
|
||||
self::$basenames[$component][$basename] = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a basename from a class name, namespaced or not.
|
||||
*/
|
||||
public static function getBasename(string $name): string
|
||||
{
|
||||
// Determine the basename
|
||||
if ($basename = strrchr($name, '\\')) {
|
||||
return substr($basename, 1);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user