Skip to content

Commit

Permalink
Lightweight static constructor initialiser (#7)
Browse files Browse the repository at this point in the history
* 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
dbalabka authored Sep 2, 2019
1 parent 829ef9e commit 943b0ba
Show file tree
Hide file tree
Showing 20 changed files with 286 additions and 26 deletions.
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -129,7 +130,7 @@ multiple Enumeration classes.
3. Implementation is based on assumption that all class static properties are elements of Enum. <!-- If there is a need to declare
any static property that isn't an Enum element then you should override the `\Dbalabka\Enumeration\Enumeration::$notEnumVars` method. -->
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
Expand Down Expand Up @@ -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
<?php
// You should always call the initialize() method right after class declaration
// To avoid manual initialization you can setup the "vladimmi/construct-static" custom loader
Action::initialize();
```
See [examples/class_static_construct.php](examples/class_static_construct.php) for example to overcome this limitation.
<?php
use Dbalabka\StaticConstructorLoader\StaticConstructorLoader;

$composer = require_once(__DIR__ . '/vendor/autoload.php');
$loader = new StaticConstructorLoader($composer);
```
See [examples/class_static_construct.php](examples/class_static_construct.php) for example.

### Serialization
There is no possibility to serialize the singleton. As a result, we have to restrict direct Enum object serialization.
Expand Down
9 changes: 3 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,20 @@
"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"
}
}
}
3 changes: 2 additions & 1 deletion examples/class_static_construct.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
11 changes: 3 additions & 8 deletions src/Enumeration.php → src/Enumeration/Enumeration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,7 +18,7 @@
*
* @author Dmitry Balabka <[email protected]>
*/
abstract class Enumeration
abstract class Enumeration implements StaticConstructorInterface
{
const INITIAL_ORDINAL = 0;

Expand All @@ -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;
Expand Down
File renamed without changes.
File renamed without changes.
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 src/StaticConstructorLoader/StaticConstructorInterface.php
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();
}
64 changes: 64 additions & 0 deletions src/StaticConstructorLoader/StaticConstructorLoader.php
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.
17 changes: 17 additions & 0 deletions tests/StaticConstructorLoader/Fixtures/Action.php
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 tests/StaticConstructorLoader/StaticConstructorLoaderTest.php
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'));

}
}

0 comments on commit 943b0ba

Please sign in to comment.