-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Lightweight static constructor initialiser (#7)
* Implement very lightweight static constructor initialiser with custom SPL autoloader w/o reflections. * Remove unneeded dependency on * Update documentation * Covered with tests
- Loading branch information
Showing
20 changed files
with
286 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
|
||
use Dbalabka\Enumeration\Exception\EnumerationException; | ||
use Dbalabka\Enumeration\Exception\InvalidArgumentException; | ||
use Dbalabka\StaticConstructorLoader\StaticConstructorInterface; | ||
use function array_search; | ||
use function get_class_vars; | ||
use function sprintf; | ||
|
@@ -17,7 +18,7 @@ | |
* | ||
* @author Dmitry Balabka <[email protected]> | ||
*/ | ||
abstract class Enumeration | ||
abstract class Enumeration implements StaticConstructorInterface | ||
{ | ||
const INITIAL_ORDINAL = 0; | ||
|
||
|
@@ -28,13 +29,7 @@ abstract class Enumeration | |
|
||
private static $initializedEnums = []; | ||
|
||
/** | ||
* This method should be called right after enumerate class declaration. | ||
* Unfortunately, PHP does not support static initialization. | ||
* See static init RFC: https://wiki.php.net/rfc/static_class_constructor | ||
* Typed Properties will help to control of calling this method. | ||
*/ | ||
final protected static function __constructStatic() : void | ||
final public static function __constructStatic() : void | ||
{ | ||
if (self::class === static::class) { | ||
return; | ||
|
File renamed without changes.
File renamed without changes.
12 changes: 12 additions & 0 deletions
12
src/StaticConstructorLoader/Exception/StaticConstructorLoaderException.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Dbalabka\StaticConstructorLoader\Exception; | ||
|
||
/** | ||
* @author Dmitrijs Balabka <[email protected]> | ||
*/ | ||
class StaticConstructorLoaderException extends \Exception | ||
{ | ||
|
||
} |
17 changes: 17 additions & 0 deletions
17
src/StaticConstructorLoader/StaticConstructorInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Dbalabka\StaticConstructorLoader; | ||
|
||
/** | ||
* @author Dmitrijs Balabka <[email protected]> | ||
*/ | ||
interface StaticConstructorInterface | ||
{ | ||
/** | ||
* This method should be called right after enumerate class declaration. | ||
* Unfortunately, PHP does not support static initialization. | ||
* See static init RFC: https://wiki.php.net/rfc/static_class_constructor | ||
*/ | ||
public static function __constructStatic(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Dbalabka\StaticConstructorLoader; | ||
|
||
use Composer\Autoload\ClassLoader; | ||
use Dbalabka\StaticConstructorLoader\Exception\StaticConstructorLoaderException; | ||
|
||
/** | ||
* Decorates the Composer autoloader to statically initialize the class. | ||
* This is a very lightweight workaround which is described in https://wiki.php.net/rfc/static_class_constructor | ||
* | ||
* @author Dmitrijs Balabka <[email protected]> | ||
*/ | ||
final class StaticConstructorLoader | ||
{ | ||
/** | ||
* @var ClassLoader | ||
*/ | ||
private $classLoader; | ||
|
||
public function __construct(ClassLoader $classLoader) | ||
{ | ||
$this->classLoader = $classLoader; | ||
|
||
// find Composer autoloader | ||
$loaders = spl_autoload_functions(); | ||
$otherLoaders = []; | ||
$composerLoader = null; | ||
foreach ($loaders as $loader) { | ||
if (is_array($loader)) { | ||
if ($loader[0] === $classLoader) { | ||
$composerLoader = $loader; | ||
break; | ||
} | ||
if ($loader[0] instanceof self) { | ||
throw new StaticConstructorLoaderException(sprintf('%s already registered', self::class)); | ||
} | ||
} | ||
$otherLoaders[] = $loader; | ||
} | ||
|
||
if (!$composerLoader) { | ||
throw new StaticConstructorLoaderException(sprintf('%s was not found in registered autoloaders', ClassLoader::class)); | ||
} | ||
|
||
// unregister Composer autoloader and all preceding autoloaders | ||
array_map('spl_autoload_unregister', array_merge($otherLoaders, [$composerLoader])); | ||
|
||
// restore the original queue order | ||
$loadersToRestore = array_merge([[$this, 'loadClass']], array_reverse($otherLoaders)); | ||
$flagTrue = array_fill(0, count($loadersToRestore), true); | ||
array_map('spl_autoload_register', $loadersToRestore, $flagTrue, $flagTrue); | ||
} | ||
|
||
public function loadClass($className) | ||
{ | ||
$result = $this->classLoader->loadClass($className); | ||
if ($result === true && $className !== StaticConstructorInterface::class && is_a($className, StaticConstructorInterface::class, true)) { | ||
$className::__constructStatic(); | ||
} | ||
return $result; | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
|
||
namespace Dbalabka\StaticConstructorLoader\Tests\Fixtures; | ||
|
||
|
||
use Dbalabka\StaticConstructorLoader\StaticConstructorInterface; | ||
|
||
class Action implements StaticConstructorInterface | ||
{ | ||
public static $instance; | ||
|
||
public static function __constructStatic() | ||
{ | ||
static::$instance = new static(); | ||
} | ||
} |
154 changes: 154 additions & 0 deletions
154
tests/StaticConstructorLoader/StaticConstructorLoaderTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
<?php | ||
|
||
namespace Dbalabka\StaticConstructorLoader\Tests; | ||
|
||
use Composer\Autoload\ClassLoader; | ||
use Dbalabka\StaticConstructorLoader\Exception\StaticConstructorLoaderException; | ||
use Dbalabka\StaticConstructorLoader\StaticConstructorLoader; | ||
use Dbalabka\StaticConstructorLoader\Tests\Fixtures\Action; | ||
use PHPUnit\Framework\Exception; | ||
use PHPUnit\Framework\Constraint\Exception as ConstraintException; | ||
use PHPUnit\Framework\TestCase; | ||
use Prophecy\Exception\Prediction\AggregateException; | ||
|
||
/** | ||
* @author Dmitrijs Balabka <[email protected]> | ||
*/ | ||
class StaticConstructorLoaderTest extends TestCase | ||
{ | ||
/** @var callable */ | ||
public static $splAutoloadFunctionsCallback; | ||
|
||
/** @var callable */ | ||
public static $splAutoloadUnregisterCallback; | ||
|
||
/** @var callable */ | ||
public static $splAutoloadRegisterCallback; | ||
|
||
/** | ||
* @var ClassLoader|\Prophecy\Prophecy\ObjectProphecy | ||
*/ | ||
private $classLoader; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $unregisteredAutoloaders = []; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $registeredAutoloaders = []; | ||
|
||
private $oldAutoloadFunctions; | ||
|
||
protected function setUp(): void | ||
{ | ||
$this->classLoader = $this->prophesize(ClassLoader::class); | ||
$this->saveAutoloaders(); | ||
// preload classes | ||
class_exists(StaticConstructorLoader::class); | ||
class_exists(StaticConstructorLoaderException::class); | ||
class_exists(Exception::class); | ||
class_exists(Exception::class); | ||
class_exists(ConstraintException::class); | ||
class_exists(AggregateException::class); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
$this->restoreAutoloaders(); | ||
} | ||
|
||
private function saveAutoloaders() | ||
{ | ||
$this->oldAutoloadFunctions = \spl_autoload_functions(); | ||
} | ||
|
||
private function restoreAutoloaders() | ||
{ | ||
$this->clearAutoloaders(); | ||
array_map('spl_autoload_register', $this->oldAutoloadFunctions); | ||
} | ||
|
||
private function clearAutoloaders() | ||
{ | ||
array_map('spl_autoload_unregister', \spl_autoload_functions()); | ||
} | ||
|
||
public function testConstructWithoutRegisteredAutoloaders() | ||
{ | ||
$this->expectException(StaticConstructorLoaderException::class); | ||
$classLoader = $this->classLoader->reveal(); | ||
|
||
$this->clearAutoloaders(); | ||
new StaticConstructorLoader($classLoader); | ||
} | ||
|
||
public function testConstructWithRegisteredAutoloadersButWithoutComposerAutoloader() | ||
{ | ||
$this->expectException(StaticConstructorLoaderException::class); | ||
$classLoader = $this->classLoader->reveal(); | ||
|
||
array_map('spl_autoload_register', [ | ||
function () {}, | ||
[$this, 'testConstructWithRegisteredAutoloadersButWithoutComposerAutoloader'] | ||
]); | ||
new StaticConstructorLoader($classLoader); | ||
} | ||
|
||
public function testConstructWithAlreadyRegisteredStaticConstructorLoader() | ||
{ | ||
$this->expectException(StaticConstructorLoaderException::class); | ||
$classLoader = $this->classLoader->reveal(); | ||
$staticConstructorLoader = unserialize('O:56:"Dbalabka\StaticConstructorLoader\StaticConstructorLoader":1:{s:67:"Dbalabka\StaticConstructorLoader\StaticConstructorLoaderclassLoader";N;}'); | ||
array_map('spl_autoload_register', [[$staticConstructorLoader, 'loadClass']]); | ||
new StaticConstructorLoader($classLoader); | ||
} | ||
|
||
public function testConstructSuccess() | ||
{ | ||
$classLoader = $this->classLoader->reveal(); | ||
array_map('spl_autoload_unregister', \spl_autoload_functions()); | ||
array_map('spl_autoload_register', [ | ||
$firstCallback = function () {}, | ||
[$classLoader, 'loadClass'], | ||
$lastCallback = function () {}, | ||
]); | ||
self::$splAutoloadFunctionsCallback = 'spl_autoload_functions'; | ||
|
||
$staticConstructorLoader = new StaticConstructorLoader($classLoader); | ||
$autoloaders = \spl_autoload_functions(); | ||
|
||
$this->restoreAutoloaders(); | ||
|
||
$this->assertSame( | ||
[ | ||
$firstCallback, | ||
[$staticConstructorLoader, 'loadClass'], | ||
$lastCallback, | ||
], | ||
$autoloaders | ||
); | ||
} | ||
|
||
public function testClassLoad() | ||
{ | ||
$composerClassLoader = array_filter(spl_autoload_functions(), function ($v) { | ||
return is_array($v) && $v[0] instanceof ClassLoader; | ||
})[0][0]; | ||
new StaticConstructorLoader($composerClassLoader); | ||
class_exists(Action::class); | ||
$this->assertInstanceOf(Action::class, Action::$instance); | ||
} | ||
|
||
public function testNotExistingClassLoad() | ||
{ | ||
$composerClassLoader = array_filter(spl_autoload_functions(), function ($v) { | ||
return is_array($v) && $v[0] instanceof ClassLoader; | ||
})[0][0]; | ||
new StaticConstructorLoader($composerClassLoader); | ||
$this->assertFalse(class_exists('NotExistingClass')); | ||
|
||
} | ||
} |