From 943b0ba436a423c51893be5fe600c4e928b11491 Mon Sep 17 00:00:00 2001 From: Dmitry Balabka Date: Mon, 2 Sep 2019 17:49:15 +0300 Subject: [PATCH] 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 --- README.md | 25 +-- composer.json | 9 +- examples/class_static_construct.php | 3 +- src/{ => Enumeration}/Enumeration.php | 11 +- .../Exception/EnumerationException.php | 0 .../Exception/InvalidArgumentException.php | 0 .../StaticConstructorLoaderException.php | 12 ++ .../StaticConstructorInterface.php | 17 ++ .../StaticConstructorLoader.php | 64 ++++++++ tests/{ => Enumeration}/EnumerationTest.php | 0 tests/{ => Enumeration}/Fixtures/Action.php | 0 .../Fixtures/ActionProperties.php | 0 .../Fixtures/ActionTypedProperties.php | 0 .../ActionWithCustomStaticProperty.php | 0 .../Fixtures/ActionWithPublicConstructor.php | 0 tests/{ => Enumeration}/Fixtures/Flag.php | 0 .../Fixtures/FlagProperties.php | 0 .../Fixtures/FlagTypedProperties.php | 0 .../Fixtures/Action.php | 17 ++ .../StaticConstructorLoaderTest.php | 154 ++++++++++++++++++ 20 files changed, 286 insertions(+), 26 deletions(-) rename src/{ => Enumeration}/Enumeration.php (92%) rename src/{ => Enumeration}/Exception/EnumerationException.php (100%) rename src/{ => Enumeration}/Exception/InvalidArgumentException.php (100%) create mode 100644 src/StaticConstructorLoader/Exception/StaticConstructorLoaderException.php create mode 100644 src/StaticConstructorLoader/StaticConstructorInterface.php create mode 100644 src/StaticConstructorLoader/StaticConstructorLoader.php rename tests/{ => Enumeration}/EnumerationTest.php (100%) rename tests/{ => Enumeration}/Fixtures/Action.php (100%) rename tests/{ => Enumeration}/Fixtures/ActionProperties.php (100%) rename tests/{ => Enumeration}/Fixtures/ActionTypedProperties.php (100%) rename tests/{ => Enumeration}/Fixtures/ActionWithCustomStaticProperty.php (100%) rename tests/{ => Enumeration}/Fixtures/ActionWithPublicConstructor.php (100%) rename tests/{ => Enumeration}/Fixtures/Flag.php (100%) rename tests/{ => Enumeration}/Fixtures/FlagProperties.php (100%) rename tests/{ => Enumeration}/Fixtures/FlagTypedProperties.php (100%) create mode 100644 tests/StaticConstructorLoader/Fixtures/Action.php create mode 100644 tests/StaticConstructorLoader/StaticConstructorLoaderTest.php diff --git a/README.md b/README.md index 2dc24ea..1c5b97b 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,10 @@ final class Action extends Enumeration public static $view; public static $edit; } -// to avoid manual initialization you can setup the "vladimmi/construct-static" custom loader Action::initialize(); ``` +**Note!** You should always call the `Enumeration::initialize()` method right after Enumeration Class declaration. +To avoid manual initialization you can setup the [StaticConstructorLoader](#class-static-initialization) provided in this library. Declaration with Typed Properties support: ```php @@ -129,7 +130,7 @@ multiple Enumeration classes. 3. Implementation is based on assumption that all class static properties are elements of Enum. 4. The method `Dbalabka\Enumeration\Enumeration::initialize()` should be called after each Enumeration class declaration. Please use the -[vladimmi/construct-static](https://github.com/vladimmi/construct-static) custom loader to avoid boilerplate code. +[StaticConstructorLoader](#class-static-initialization) provided in this library to avoid boilerplate code. ## Usage ```php @@ -168,17 +169,19 @@ Action::$view = null; ### Class static initialization This implementation relies on class static initialization which was proposed in [Static Class Constructor](https://wiki.php.net/rfc/static_class_constructor). -The RFC describes possible workarounds. The simplest way is to call the initialization method right after class declaration, +The RFC is still in Draft status but it describes possible workarounds. The simplest way is to call the initialization method right after the class declaration, but it requires the developer to keep this in mind. Thanks to [Typed Properties](https://wiki.php.net/rfc/typed_properties_v2) -we can control uninitialized properties - PHP will throw and error in case of access to an uninitialized property. -It might be automated with custom autoloader implemented in [vladimmi/construct-static](https://github.com/vladimmi/construct-static) library. +we can control uninitialized properties - PHP will throw an error in case of access to an uninitialized property. +It might be automated with custom autoloader [Dbalabka\StaticConstructorLoader\StaticConstructorLoader](./src/StaticConstructorLoader/StaticConstructorLoader.php) +provided in this library: ```php -=7.1" }, "require-dev": { - "vladimmi/construct-static": "dev-master", "myclabs/php-enum": "^1.0", "phpunit/phpunit": "^7.5", "phpbench/phpbench": "^0.16.9" }, - "suggest": { - "vladimmi/construct-static": "Allows to call __constructStatic on class load" - }, "autoload": { "psr-4": { - "Dbalabka\\Enumeration\\": "src" + "Dbalabka\\": "src" } }, "autoload-dev": { "psr-4": { "Dbalabka\\Enumeration\\Examples\\": "examples", - "Dbalabka\\Enumeration\\Tests\\": "tests" + "Dbalabka\\Enumeration\\Tests\\": "tests\\Enumeration", + "Dbalabka\\StaticConstructorLoader\\Tests\\": "tests\\StaticConstructorLoader" } } } diff --git a/examples/class_static_construct.php b/examples/class_static_construct.php index 2dbcbe0..942a53a 100644 --- a/examples/class_static_construct.php +++ b/examples/class_static_construct.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Dbalabka\Enumeration\Examples\Enum\Color; +use Dbalabka\StaticConstructorLoader\StaticConstructorLoader; $composer = require_once(__DIR__ . '/../vendor/autoload.php'); -$loader = new ConstructStatic\Loader($composer); +$loader = new StaticConstructorLoader($composer); assert(Color::$red instanceof Color && Color::$red === Color::$red); diff --git a/src/Enumeration.php b/src/Enumeration/Enumeration.php similarity index 92% rename from src/Enumeration.php rename to src/Enumeration/Enumeration.php index 4a14159..08ceba2 100644 --- a/src/Enumeration.php +++ b/src/Enumeration/Enumeration.php @@ -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 */ -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; diff --git a/src/Exception/EnumerationException.php b/src/Enumeration/Exception/EnumerationException.php similarity index 100% rename from src/Exception/EnumerationException.php rename to src/Enumeration/Exception/EnumerationException.php diff --git a/src/Exception/InvalidArgumentException.php b/src/Enumeration/Exception/InvalidArgumentException.php similarity index 100% rename from src/Exception/InvalidArgumentException.php rename to src/Enumeration/Exception/InvalidArgumentException.php diff --git a/src/StaticConstructorLoader/Exception/StaticConstructorLoaderException.php b/src/StaticConstructorLoader/Exception/StaticConstructorLoaderException.php new file mode 100644 index 0000000..297a7a9 --- /dev/null +++ b/src/StaticConstructorLoader/Exception/StaticConstructorLoaderException.php @@ -0,0 +1,12 @@ + + */ +class StaticConstructorLoaderException extends \Exception +{ + +} diff --git a/src/StaticConstructorLoader/StaticConstructorInterface.php b/src/StaticConstructorLoader/StaticConstructorInterface.php new file mode 100644 index 0000000..2f12960 --- /dev/null +++ b/src/StaticConstructorLoader/StaticConstructorInterface.php @@ -0,0 +1,17 @@ + + */ +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(); +} diff --git a/src/StaticConstructorLoader/StaticConstructorLoader.php b/src/StaticConstructorLoader/StaticConstructorLoader.php new file mode 100644 index 0000000..9738d04 --- /dev/null +++ b/src/StaticConstructorLoader/StaticConstructorLoader.php @@ -0,0 +1,64 @@ + + */ +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; + } +} diff --git a/tests/EnumerationTest.php b/tests/Enumeration/EnumerationTest.php similarity index 100% rename from tests/EnumerationTest.php rename to tests/Enumeration/EnumerationTest.php diff --git a/tests/Fixtures/Action.php b/tests/Enumeration/Fixtures/Action.php similarity index 100% rename from tests/Fixtures/Action.php rename to tests/Enumeration/Fixtures/Action.php diff --git a/tests/Fixtures/ActionProperties.php b/tests/Enumeration/Fixtures/ActionProperties.php similarity index 100% rename from tests/Fixtures/ActionProperties.php rename to tests/Enumeration/Fixtures/ActionProperties.php diff --git a/tests/Fixtures/ActionTypedProperties.php b/tests/Enumeration/Fixtures/ActionTypedProperties.php similarity index 100% rename from tests/Fixtures/ActionTypedProperties.php rename to tests/Enumeration/Fixtures/ActionTypedProperties.php diff --git a/tests/Fixtures/ActionWithCustomStaticProperty.php b/tests/Enumeration/Fixtures/ActionWithCustomStaticProperty.php similarity index 100% rename from tests/Fixtures/ActionWithCustomStaticProperty.php rename to tests/Enumeration/Fixtures/ActionWithCustomStaticProperty.php diff --git a/tests/Fixtures/ActionWithPublicConstructor.php b/tests/Enumeration/Fixtures/ActionWithPublicConstructor.php similarity index 100% rename from tests/Fixtures/ActionWithPublicConstructor.php rename to tests/Enumeration/Fixtures/ActionWithPublicConstructor.php diff --git a/tests/Fixtures/Flag.php b/tests/Enumeration/Fixtures/Flag.php similarity index 100% rename from tests/Fixtures/Flag.php rename to tests/Enumeration/Fixtures/Flag.php diff --git a/tests/Fixtures/FlagProperties.php b/tests/Enumeration/Fixtures/FlagProperties.php similarity index 100% rename from tests/Fixtures/FlagProperties.php rename to tests/Enumeration/Fixtures/FlagProperties.php diff --git a/tests/Fixtures/FlagTypedProperties.php b/tests/Enumeration/Fixtures/FlagTypedProperties.php similarity index 100% rename from tests/Fixtures/FlagTypedProperties.php rename to tests/Enumeration/Fixtures/FlagTypedProperties.php diff --git a/tests/StaticConstructorLoader/Fixtures/Action.php b/tests/StaticConstructorLoader/Fixtures/Action.php new file mode 100644 index 0000000..83cc2bb --- /dev/null +++ b/tests/StaticConstructorLoader/Fixtures/Action.php @@ -0,0 +1,17 @@ + + */ +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')); + + } +}