diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 41c5c1830d3f..dcef743f607f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + Memcache @@ -104,7 +104,9 @@ - + + + diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 222df063196a..67a31728d65c 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -14,6 +14,7 @@ use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Model; use Config\Services; +use InvalidArgumentException; /** * Factories for creating instances. @@ -24,7 +25,7 @@ * instantiation checks. * * @method static BaseConfig|null config(...$arguments) - * @method static Model|null models(string $name, array $options = [], ?ConnectionInterface &$conn = null) + * @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null) */ class Factories { @@ -51,25 +52,65 @@ class Factories ]; /** - * Mapping of class basenames (no namespace) to - * their instances. + * 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> * @phpstan-var array> */ - protected static $basenames = []; + 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> * @phpstan-var array> */ protected static $instances = []; + /** + * 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 string $classname FQCN to be loaded + * @phpstan-param class-string $classname FQCN to be loaded + */ + public static function define(string $component, string $alias, string $classname): void + { + 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; + } + /** * Loads instances based on the method component name. Either * creates a new instance or returns an existing shared instance. @@ -78,86 +119,136 @@ class Factories */ public static function __callStatic(string $component, array $arguments) { - // First argument is the name, second is options - $name = trim(array_shift($arguments), '\\ '); + // 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(strtolower($component)), $options); if (! $options['getShared']) { - if ($class = self::locateClass($options, $name)) { + if (isset(self::$aliases[$component][$alias])) { + $class = self::$aliases[$component][$alias]; + return new $class(...$arguments); } + if ($class = self::locateClass($options, $alias)) { + 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; } - $basename = self::getBasename($name); + self::$instances[$options['component']][$class] = new $class(...$arguments); + self::$aliases[$options['component']][$alias] = $class; + + // If a short classname is specified, also register FQCN to share the instance. + if (! isset(self::$aliases[$options['component']][$class])) { + self::$aliases[$options['component']][$class] = $class; + } + + return self::$instances[$options['component']][$class]; + } - // Check for an existing instance - if (isset(self::$basenames[$options['component']][$basename])) { - $class = self::$basenames[$options['component']][$basename]; + /** + * Gets the defined instance. If not exists, creates new one. + * + * @return object|null + */ + private static function getDefinedInstance(array $options, string $alias, array $arguments) + { + 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::$instances[$options['component']][$class] = new $class(...$arguments); + 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 null; + } - return self::$instances[$options['component']][$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 $name Class name, namespace optional + * @param string $alias Class alias. See the $aliases property. */ - protected static function locateClass(array $options, string $name): ?string + protected static function locateClass(array $options, string $alias): ?string { // Check for low-hanging fruit - if (class_exists($name, false) && self::verifyPreferApp($options, $name) && self::verifyInstanceOf($options, $name)) { - return $name; + 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($name); - $appname = $options['component'] === 'config' + $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 ($options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $name)) { + 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($name) && self::verifyInstanceOf($options, $name)) { - return $name; + if (class_exists($alias) && self::verifyInstanceOf($options, $alias)) { + return $alias; } // 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'])) { + // 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 . $name)) { + elseif (! $files = $locator->search($options['path'] . DIRECTORY_SEPARATOR . $alias)) { return null; } @@ -173,13 +264,23 @@ protected static function locateClass(array $options, string $name): ?string 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 $name Class name, namespace optional + * @param string $alias Class alias. See the $aliases property. */ - protected static function verifyPreferApp(array $options, string $name): bool + protected static function verifyPreferApp(array $options, string $alias): bool { // Anything without that restriction passes if (! $options['preferApp']) { @@ -187,27 +288,27 @@ protected static function verifyPreferApp(array $options, string $name): bool } // Special case for Config since its App namespace is actually \Config - if ($options['component'] === 'config') { - return strpos($name, 'Config') === 0; + if (self::isConfig($options['component'])) { + return strpos($alias, 'Config') === 0; } - return strpos($name, APP_NAMESPACE) === 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 $name Class name, namespace optional + * @param string $alias Class alias. See the $aliases property. */ - protected static function verifyInstanceOf(array $options, string $name): bool + protected static function verifyInstanceOf(array $options, string $alias): bool { // Anything without that restriction passes if (! $options['instanceOf']) { return true; } - return is_a($name, $options['instanceOf'], true); + return is_a($alias, $options['instanceOf'], true); } /** @@ -216,6 +317,9 @@ protected static function verifyInstanceOf(array $options, string $name): bool * @param string $component Lowercase, plural component name * * @return array + * + * @internal For testing only + * @testTag */ public static function getOptions(string $component): array { @@ -226,12 +330,14 @@ public static function getOptions(string $component): array return self::$options[$component]; } - $values = $component === 'config' + $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::class)->{$component} ?? []; + // The setOptions() reset the component. So getOptions() may reset + // the component. return self::setOptions($component, $values); } @@ -239,6 +345,7 @@ public static function getOptions(string $component): array * Normalizes, stores, and returns the configuration for a specific component * * @param string $component Lowercase, plural component name + * @param array $values option values * * @return array The result after applying defaults and normalization */ @@ -273,7 +380,7 @@ public static function reset(?string $component = null) if ($component) { unset( static::$options[$component], - static::$basenames[$component], + static::$aliases[$component], static::$instances[$component] ); @@ -281,7 +388,7 @@ public static function reset(?string $component = null) } static::$options = []; - static::$basenames = []; + static::$aliases = []; static::$instances = []; } @@ -289,31 +396,44 @@ public static function reset(?string $component = null) * Helper method for injecting mock instances * * @param string $component Lowercase, plural component name - * @param string $name The name of the instance + * @param string $alias Class alias. See the $aliases property. + * + * @internal For testing only + * @testTag */ - public static function injectMock(string $component, string $name, object $instance) + public static function injectMock(string $component, string $alias, object $instance) { // Force a configuration to exist for this component $component = strtolower($component); self::getOptions($component); - $class = get_class($instance); - $basename = self::getBasename($name); + $class = get_class($instance); - self::$instances[$component][$class] = $instance; - self::$basenames[$component][$basename] = $class; + 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 name, namespaced or not. + * Gets a basename from a class alias, namespaced or not. + * + * @internal For testing only + * @testTag */ - public static function getBasename(string $name): string + public static function getBasename(string $alias): string { // Determine the basename - if ($basename = strrchr($name, '\\')) { + if ($basename = strrchr($alias, '\\')) { return substr($basename, 1); } - return $name; + return $alias; } } diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 655329ad95de..ffbf8b7b8a83 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -12,9 +12,13 @@ namespace CodeIgniter\Config; use CodeIgniter\Test\CIUnitTestCase; +use InvalidArgumentException; use ReflectionClass; use stdClass; +use Tests\Support\Config\TestRegistrar; +use Tests\Support\Models\EntityModel; use Tests\Support\Models\UserModel; +use Tests\Support\View\SampleClass; use Tests\Support\Widgets\OtherWidget; use Tests\Support\Widgets\SomeWidget; @@ -251,7 +255,53 @@ class_alias(SomeWidget::class, $class); $this->assertInstanceOf(SomeWidget::class, $result); } - public function testpreferAppOverridesClassname() + public function testShortnameReturnsConfigInApp() + { + // Create a config class in App + $file = APPPATH . 'Config/TestRegistrar.php'; + $source = <<<'EOL' + assertInstanceOf('Config\TestRegistrar', $result); + + // Delete the config class in App + unlink($file); + } + + public function testFullClassnameIgnoresPreferApp() + { + // Create a config class in App + $file = APPPATH . 'Config/TestRegistrar.php'; + $source = <<<'EOL' + assertInstanceOf(TestRegistrar::class, $result); + + Factories::setOptions('config', ['preferApp' => false]); + + $result = Factories::config(TestRegistrar::class); + + $this->assertInstanceOf(TestRegistrar::class, $result); + + // Delete the config class in App + unlink($file); + } + + public function testPreferAppIsIgnored() { // Create a fake class in App $class = 'App\Widgets\OtherWidget'; @@ -260,11 +310,92 @@ class_alias(SomeWidget::class, $class); } $result = Factories::widgets(OtherWidget::class); - $this->assertInstanceOf(SomeWidget::class, $result); + $this->assertInstanceOf(OtherWidget::class, $result); + } - Factories::setOptions('widgets', ['preferApp' => false]); + public function testCanLoadTwoCellsWithSameShortName() + { + $cell1 = Factories::cells('\\' . SampleClass::class); + $cell2 = Factories::cells('\\' . \Tests\Support\View\OtherCells\SampleClass::class); - $result = Factories::widgets(OtherWidget::class); - $this->assertInstanceOf(OtherWidget::class, $result); + $this->assertNotSame($cell1, $cell2); + } + + public function testDefineSameAliasTwiceWithDifferentClasses() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Already defined in Factories: models CodeIgniter\Shield\Models\UserModel -> Tests\Support\Models\UserModel' + ); + + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + UserModel::class + ); + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + EntityModel::class + ); + } + + public function testDefineSameAliasAndSameClassTwice() + { + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + UserModel::class + ); + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + UserModel::class + ); + + $model = model('CodeIgniter\Shield\Models\UserModel'); + + $this->assertInstanceOf(UserModel::class, $model); + } + + public function testDefineNonExistentClass() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No such class: App\Models\UserModel'); + + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + 'App\Models\UserModel' + ); + } + + public function testDefineAfterLoading() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Already defined in Factories: models Tests\Support\Models\UserModel -> Tests\Support\Models\UserModel' + ); + + model(UserModel::class); + + Factories::define( + 'models', + UserModel::class, + 'App\Models\UserModel' + ); + } + + public function testDefineAndLoad() + { + Factories::define( + 'models', + UserModel::class, + EntityModel::class + ); + + $model = model(UserModel::class); + + $this->assertInstanceOf(EntityModel::class, $model); } } diff --git a/tests/system/Database/ModelFactoryTest.php b/tests/system/Database/ModelFactoryTest.php index 4ec47a9b5967..1f69ee865bb5 100644 --- a/tests/system/Database/ModelFactoryTest.php +++ b/tests/system/Database/ModelFactoryTest.php @@ -66,12 +66,12 @@ public function testReset() $this->assertNull(ModelFactory::get('Banana')); } - public function testBasenameReturnsExistingNamespaceInstance() + public function testBasenameDoesNotReturnExistingNamespaceInstance() { ModelFactory::injectMock(UserModel::class, new JobModel()); $basenameModel = ModelFactory::get('UserModel'); - $this->assertInstanceOf(JobModel::class, $basenameModel); + $this->assertInstanceOf(UserModel::class, $basenameModel); } } diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 6f92be2aaaad..fdc2d8465a8b 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -11,7 +11,7 @@ namespace CodeIgniter\Test; -use CodeIgniter\Database\ModelFactory; +use CodeIgniter\Config\Factories; use Tests\Support\Models\EntityModel; use Tests\Support\Models\EventModel; use Tests\Support\Models\FabricatorModel; @@ -98,12 +98,16 @@ public function testConstructorUsesProvidedLocale() public function testModelUsesNewInstance() { - // Inject the wrong model for UserModel to show it is ignored by Fabricator + // Inject the wrong model for UserModel $mock = new FabricatorModel(); - ModelFactory::injectMock(UserModel::class, $mock); + Factories::injectMock('models', UserModel::class, $mock); $fabricator = new Fabricator(UserModel::class); - $this->assertInstanceOf(UserModel::class, $fabricator->getModel()); + + // Fabricator gets the instance from Factories, so it is FabricatorModel. + $this->assertInstanceOf(FabricatorModel::class, $fabricator->getModel()); + // But Fabricator creates a new instance. + $this->assertNotSame($mock, $fabricator->getModel()); } public function testGetModelReturnsModel() diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 475d08e9eaea..6eb96418c6ae 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -29,6 +29,35 @@ or more was specified. See :ref:`upgrade-440-uri-setsegment`. The next segment (``+1``) of the current last segment can be set as before. +.. _v440-factories: + +Factories +--------- + +Passing Fully Qualified Classname +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now ``preferApp`` works only when you request +:ref:`a classname without a namespace `. + +For example, when you call ``model(\Myth\Auth\Models\UserModel::class)`` or +``model('Myth\Auth\Models\UserModel')``: + + - before: + + - returns ``App\Models\UserModel`` if exists and ``preferApp`` is true (default) + - returns ``Myth\Auth\Models\UserModel`` if exists and ``preferApp`` is false + + - after: + + - returns ``Myth\Auth\Models\UserModel`` even if ``preferApp`` is true (default) + - returns ``App\Models\UserModel`` if you define ``Factories::define('models', 'Myth\Auth\Models\UserModel', 'App\Models\UserModel')`` before calling the ``model()`` + +Property Name +^^^^^^^^^^^^^ + +The property ``Factories::$basenames`` has been renamed to ``$aliases``. + .. _v440-interface-changes: Interface Changes @@ -164,6 +193,8 @@ Others - **RedirectException:** can also take an object that implements ResponseInterface as its first argument. - **RedirectException:** implements ResponsableInterface. - **DebugBar:** Now :ref:`view-routes` are displayed in *DEFINED ROUTES* on the *Routes* tab. +- **Factories:** You can now define the classname that will actually be loaded. + See :ref:`factories-defining-classname-to-be-loaded`. Message Changes *************** diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index b3ec0a064176..881f0fa1b1f1 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -54,9 +54,18 @@ by using the magic static method of the Factories class, ``Factories::models()`` The static method name is called *component*. -By default, Factories first searches in the ``App`` namespace for the path corresponding to the magic static method name. +.. _factories-passing-classname-without-namespace: + +Passing Classname without Namespace +----------------------------------- + +If you pass a classname without a namespace, Factories first searches in the +``App`` namespace for the path corresponding to the magic static method name. ``Factories::models()`` searches the **app/Models** directory. +Passing Short Classname +^^^^^^^^^^^^^^^^^^^^^^^ + In the following code, if you have ``App\Models\UserModel``, the instance will be returned: .. literalinclude:: factories/001.php @@ -68,31 +77,35 @@ you get back the instance as before: .. literalinclude:: factories/003.php -preferApp option ----------------- +Passing Short Classname with Sub-directories +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You could also request a specific class: +If you want to load a class in sub directories, you use the ``/`` as a separator. +The following code loads **app/Libraries/Sub/SubLib.php** if it exists: -.. literalinclude:: factories/002.php +.. literalinclude:: factories/013.php :lines: 2- -If you have only ``Blog\Models\UserModel``, the instance will be returned. -But if you have both ``App\Models\UserModel`` and ``Blog\Models\UserModel``, -the instance of ``App\Models\UserModel`` will be returned. +Passing Full Qualified Classname +-------------------------------- -If you want to get ``Blog\Models\UserModel``, you need to disable the option ``preferApp``: +You could also request a full qualified classname: -.. literalinclude:: factories/010.php +.. literalinclude:: factories/002.php :lines: 2- -Loading a Class in Sub-directories -================================== +It returns the instance of ``Blog\Models\UserModel`` if it exists. -If you want to load a class in sub directories, you use the ``/`` as a separator. -The following code loads **app/Libraries/Sub/SubLib.php**: +.. note:: Prior to v4.4.0, when you requested a full qualified classname, + if you had only ``Blog\Models\UserModel``, the instance would be returned. + But if you had both ``App\Models\UserModel`` and ``Blog\Models\UserModel``, + the instance of ``App\Models\UserModel`` would be returned. -.. literalinclude:: factories/013.php - :lines: 2- + If you wanted to get ``Blog\Models\UserModel``, you needed to disable the + option ``preferApp``: + + .. literalinclude:: factories/010.php + :lines: 2- Convenience Functions ********************* @@ -115,6 +128,29 @@ The second function, :php:func:`model()` returns a new instance of a Model class .. literalinclude:: factories/009.php +.. _factories-defining-classname-to-be-loaded: + +Defining Classname to be Loaded +******************************* + +.. versionadded:: 4.4.0 + +You could define a classname to be loaded before loading the class with +the ``Factories::define()`` method: + +.. literalinclude:: factories/014.php + :lines: 2- + +The first parameter is a component. The second parameter is a class alias +(the first parameter to Factories magic static method), and the third parameter +is the true full qualified classname to be loaded. + +After that, if you load ``Myth\Auth\Models\UserModel`` with Factories, the +``App\Models\UserModel`` instance will be returned: + +.. literalinclude:: factories/015.php + :lines: 2- + Factory Parameters ****************** @@ -154,6 +190,9 @@ preferApp boolean Whether a class with the same basename in the App name overrides other explicit class requests. ========== ============== ============================================================ =================================================== +.. note:: Since v4.4.0, ``preferApp`` works only when you request + :ref:`a classname without a namespace `. + Factories Behavior ****************** diff --git a/user_guide_src/source/concepts/factories/014.php b/user_guide_src/source/concepts/factories/014.php new file mode 100644 index 000000000000..992b0f7b2236 --- /dev/null +++ b/user_guide_src/source/concepts/factories/014.php @@ -0,0 +1,3 @@ +`. -.. note:: ``config()`` finds the file in **app/Config/** when there is a class with the same shortname, +.. note:: Prior to v4.4.0, ``config()`` finds the file in **app/Config/** when there + is a class with the same shortname, even if you specify a fully qualified class name like ``config(\Acme\Blog\Config\Blog::class)``. - This is because ``config()`` is a wrapper for the ``Factories`` class which uses ``preferApp`` by default. See :ref:`factories-loading-class` for more information. + This behavior has been fixed in v4.4.0, and returns the specified instance. Migrations ========== diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index a64e35813a4a..3fc02a179330 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -73,6 +73,21 @@ This bug was fixed and now URIs for underscores (**foo_bar**) is not accessible. If you have links to URIs for underscores (**foo_bar**), update them with URIs for dashes (**foo-bar**). +When Passing Fully Qualified Classnames to Factories +==================================================== + +The behavior of passing fully qualified classnames to Factories has been changed. +See :ref:`ChangeLog ` for details. + +If you have code like ``model('\Myth\Auth\Models\UserModel::class')`` or +``model('Myth\Auth\Models\UserModel')`` (the code may be in the third-party packages), +and you expect to load your ``App\Models\UserModel``, you need to define the +classname to be loaded before the first loading of that class:: + + Factories::define('models', 'Myth\Auth\Models\UserModel', 'App\Models\UserModel'); + +See :ref:`factories-defining-classname-to-be-loaded` for details. + Interface Changes =================