Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autoloading Aliases #3940

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/Config/Autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,22 @@ class Autoload extends AutoloadConfig
* @var array<string, string>
*/
public $classmap = [];

/**
* -------------------------------------------------------------------
* Aliases
* -------------------------------------------------------------------
* Class aliasing allows you to "intercept" a class by providing your
* own (compatible) class instead. Be sure you understand the way
* class_alias() works and its limitations before using this.
*
* Prototype:
*
* $aliases = [
* 'CodeIgniter\Honeypot\Honeypot' => 'App\ThirdParty\CompatibleHoneypotReplacement',
* ];
*
* @var array<string, string>
*/
public $aliases = [];
}
82 changes: 32 additions & 50 deletions system/Autoloader/Autoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@
* Quux.php # Foo\Bar\Qux\Quux
*
* you can add the path to the configuration array that is passed in the constructor.
* The Config array consists of 2 primary keys, both of which are associative arrays:
* 'psr4', and 'classmap'.
* The Config array consists of up to 3 primary keys, all of which are associative arrays:
* 'psr4', 'classmap', and 'aliases'.
*
* $Config = [
* 'psr4' => [
* 'Foo\Bar' => '/path/to/packages/foo-bar'
* 'Foo\Bar' => '/path/to/packages/foo-bar',
* ],
* 'classmap' => [
* 'MyClass' => '/path/to/class/file.php'
* 'MyClass' => '/path/to/class/file.php',
* ]
MGatner marked this conversation as resolved.
Show resolved Hide resolved
* 'aliases' => [
* 'Original' => 'Alias',
* ]
* ];
*
Expand Down Expand Up @@ -67,6 +70,13 @@ class Autoloader
*/
protected $classmap = [];

/**
* Stores alias as key, original as value.
*
* @var array<string, string>
*/
protected $aliases = [];

//--------------------------------------------------------------------

/**
Expand Down Expand Up @@ -97,6 +107,11 @@ public function initialize(Autoload $config, Modules $modules)
$this->classmap = $config->classmap;
}

if (isset($config->aliases))
{
$this->aliases = $config->aliases;
}

// Should we load through Composer's namespaces, also?
if ($modules->discoverInComposer)
{
Expand Down Expand Up @@ -221,7 +236,7 @@ public function removeNamespace(string $namespace)
//--------------------------------------------------------------------

/**
* Loads the class file for a given class name.
* Loads the class file for a given class name or its alias.
*
* @param string $class The fully qualified class name.
*
Expand All @@ -233,16 +248,22 @@ public function loadClass(string $class)
$class = trim($class, '\\');
$class = str_ireplace('.php', '', $class);

$mapped_file = $this->loadInNamespace($class);
// Check if this class is an alias
if (! isset($this->aliases[$class]))
{
return $this->loadInNamespace($class);
}

// Nothing? One last chance by looking
// in common CodeIgniter folders.
if (! $mapped_file)
// Attempt to load the original instead then alias it
$original = $this->aliases[$class];
if ($result = $this->loadInNamespace($original))
{
$mapped_file = $this->loadLegacy($class);
class_alias($original, $class, false);
return $result;
}

return $mapped_file;
// If the original was not found then fail
return false;
}

//--------------------------------------------------------------------
Expand Down Expand Up @@ -295,45 +316,6 @@ protected function loadInNamespace(string $class)

//--------------------------------------------------------------------

/**
MGatner marked this conversation as resolved.
Show resolved Hide resolved
* Attempts to load the class from common locations in previous
* version of CodeIgniter, namely 'app/Libraries', and
* 'app/Models'.
*
* @param string $class The class name. This typically should NOT have a namespace.
*
* @return mixed The mapped file name on success, or boolean false on failure
*/
protected function loadLegacy(string $class)
{
// If there is a namespace on this class, then
// we cannot load it from traditional locations.
if (strpos($class, '\\') !== false)
{
return false;
}

$paths = [
APPPATH . 'Controllers/',
APPPATH . 'Libraries/',
APPPATH . 'Models/',
];

$class = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';

foreach ($paths as $path)
{
if ($file = $this->includeFile($path . $class))
{
return $file;
}
}

return false;
}

//--------------------------------------------------------------------

/**
* A central way to include a file. Split out primarily for testing purposes.
*
Expand Down
8 changes: 8 additions & 0 deletions tests/_support/Autoloader/Foobar1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Tests\Support\Autoloader;

class Foobar1
{
public $isFoobar = true;
}
8 changes: 8 additions & 0 deletions tests/_support/Autoloader/Foobar2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Tests\Support\Autoloader;

class Foobar2
{
public $isFoobar = true;
}
8 changes: 8 additions & 0 deletions tests/_support/Autoloader/Replacement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Tests\Support\Autoloader;

class Replacement
{
public $isFoobar = false;
}
72 changes: 72 additions & 0 deletions tests/system/Autoloader/AutoloaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,76 @@ public function testFindsComposerRoutesWithComposerPathNotFound()
$namespaces = $this->loader->getNamespace();
$this->assertArrayNotHasKey('Laminas\\Escaper', $namespaces);
}

public function testAliasReplaceesOriginalClass()
{
$config = new Autoload();
$modules = new Modules();

$config->aliases = [
'Tests\Support\Autoloader\Foobar1' => 'Tests\Support\Autoloader\Replacement',
];

$this->loader = new Autoloader();
$this->loader->initialize($config, $modules)->register();

// Try to load the class
$instance = new \Tests\Support\Autoloader\Foobar1();

$this->assertFalse($instance->isFoobar);
}

public function testAliasReplacementNotFoundUsesFallback()
{
$config = new Autoload();
$modules = new Modules();

$config->aliases = [
'Tests\Support\Autoloader\Foobar2' => 'Tests\Support\Autoloader\Nonexistant',
];

$this->loader = new Autoloader();
$this->loader->initialize($config, $modules)->register();

// Try to load the class
$instance = new \Tests\Support\Autoloader\Foobar2();

$this->assertTrue($instance->isFoobar);
}

public function testAliasNonexistantStillReplaces()
{
$config = new Autoload();
$modules = new Modules();

$config->aliases = [
'Tests\Support\Autoloader\Foobar3' => 'Tests\Support\Autoloader\Replacement',
];

$this->loader = new Autoloader();
$this->loader->initialize($config, $modules)->register();

// Try to load the class
$instance = new \Tests\Support\Autoloader\Foobar3();

$this->assertFalse($instance->isFoobar);
}

public function testAliasNeitherFoundFails()
{
$config = new Autoload();
$modules = new Modules();

$config->aliases = [
'Tests\Support\Autoloader\Foobar4' => 'Tests\Support\Autoloader\Nonexistant',
];

$this->loader = new Autoloader();
$this->loader->initialize($config, $modules)->register();

// Check for the class
$result = class_exists('Tests\Support\Autoloader\Foobar4');

$this->assertFalse($result);
}
}
20 changes: 20 additions & 0 deletions user_guide_src/source/concepts/autoloader.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@ third-party libraries that are not namespaced::

The key of each row is the name of the class that you want to locate. The value is the path to locate it at.

Aliasing
========

A third property of your Autoload Config Class may specify class aliases. This provides a powerful way
to "intercept" requests for an unloaded class and alias it to an alternate name. This option should
only be used if you know how `class aliasing <https://www.php.net/manual/en/function.class-alias.php>`_
works and the limitations of autoloading priority.

Classes in this array are defined with the alias as the key and the original as the value.
Another way to think of this is the key is the class you want to "replace" and the value is
your new class to use instead of it::

$aliases = [
'CodeIgniter\Honeypot\Honeypot' => 'App\ThirdParty\CompatibleHoneypotReplacement',
];

Aliases are "lazy loaded" by the Autoloader instead of processed all at once to increase performance.
Keep this in mind when writing tests or other Autoloaders to ensure your classes get aliased
properly, since ``Autoloader::loadClass()`` will only be called when a class is not known.

Legacy Support
==============

Expand Down