From c866c13512742800eeccf92e98affeefdeaaca51 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Mon, 4 Mar 2024 17:15:10 +0000 Subject: [PATCH] Built initial implementation Signed-off-by: Tom Wright --- .editorconfig | 20 +++ .gitattributes | 11 ++ .github/workflows/integrate.yml | 130 +++++++++++++++++++ .gitignore | 8 ++ CHANGELOG.md | 2 + README.md | 56 +++++++++ composer.json | 27 ++++ ecs.php | 13 ++ phpstan.neon | 4 + src/Wellspring.php | 214 ++++++++++++++++++++++++++++++++ src/Wellspring/Loader.php | 75 +++++++++++ src/Wellspring/Priority.php | 26 ++++ 12 files changed, 586 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/integrate.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 ecs.php create mode 100644 phpstan.neon create mode 100644 src/Wellspring.php create mode 100644 src/Wellspring/Loader.php create mode 100644 src/Wellspring/Priority.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fc134fc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# https://EditorConfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +block_comment_start = /* +block_comment = * +block_comment_end = */ + +[*.yml] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..789106a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +.editorconfig export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore +CHANGELOG.md export-ignore +ecs.php export-ignore +phpstan.neon export-ignore +phpunit.xml.dist export-ignore +docs/ export-ignore +tests/ export-ignore +stubs/ export-ignore diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml new file mode 100644 index 0000000..b495e04 --- /dev/null +++ b/.github/workflows/integrate.yml @@ -0,0 +1,130 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow + +name: "Integrate" + +on: + push: + branches: + - "develop" + pull_request: null + +env: + PHP_EXTENSIONS: "intl" + +jobs: + file_consistency: + name: "1️⃣ File consistency" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.1" + extensions: "${{ env.PHP_EXTENSIONS }}" + ini-values: "post_max_size=256M" + + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: Install Effigy + run: | + composer global config --no-plugins allow-plugins.phpstan/extension-installer true + composer global require decodelabs/effigy + + - name: "Install dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Check file permissions" + run: | + composer global exec effigy check-executable-permissions + + - name: "Check exported files" + run: | + composer global exec effigy check-git-exports + + - name: "Find non-printable ASCII characters" + run: | + composer global exec effigy check-non-ascii + + - name: "Check source code for syntax errors" + run: | + composer global exec effigy lint + + static_analysis: + name: "3️⃣ Static Analysis" + needs: + - "file_consistency" + runs-on: "ubuntu-latest" + strategy: + matrix: + php-version: + - "8.1" + - "8.2" + - "8.3" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + extensions: "${{ env.PHP_EXTENSIONS }}" + ini-values: "post_max_size=256M" + + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: Install Effigy + run: | + composer global config --no-plugins allow-plugins.phpstan/extension-installer true + composer global require decodelabs/effigy + + - name: "Validate Composer configuration" + run: "composer validate --strict" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Execute static analysis" + run: | + composer global exec effigy analyze -- --headless + + + coding_standards: + name: "4️⃣ Coding Standards" + needs: + - "file_consistency" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.1" + extensions: "${{ env.PHP_EXTENSIONS }}" + ini-values: "post_max_size=256M" + + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: "Check EditorConfig configuration" + run: "test -f .editorconfig" + + - name: "Check adherence to EditorConfig" + uses: "greut/eclint-action@v0" + + - name: Install Effigy + run: | + composer global config --no-plugins allow-plugins.phpstan/extension-installer true + composer global require decodelabs/effigy + + - name: "Install dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Check coding style" + run: | + composer global exec effigy format -- --headless + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b0d60a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/vendor +composer.phar +composer.lock +.DS_Store +Thumbs.db +/phpunit.xml +/.idea +/.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2a3ad2c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +## v0.1.0 (2024-03-04) +* Built initial implementation diff --git a/README.md b/README.md new file mode 100644 index 0000000..8681d87 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Wellspring + +[![PHP from Packagist](https://img.shields.io/packagist/php-v/decodelabs/wellspring?style=flat)](https://packagist.org/packages/decodelabs/wellspring) +[![Latest Version](https://img.shields.io/packagist/v/decodelabs/wellspring.svg?style=flat)](https://packagist.org/packages/decodelabs/wellspring) +[![Total Downloads](https://img.shields.io/packagist/dt/decodelabs/wellspring.svg?style=flat)](https://packagist.org/packages/decodelabs/wellspring) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/decodelabs/wellspring/integrate.yml?branch=develop)](https://github.com/decodelabs/wellspring/actions/workflows/integrate.yml) +[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-44CC11.svg?longCache=true&style=flat)](https://github.com/phpstan/phpstan) +[![License](https://img.shields.io/packagist/l/decodelabs/wellspring?style=flat)](https://packagist.org/packages/decodelabs/wellspring) + +### PHP autoload management tools + +Wellspring provides simple tools to manage and configure autoloaders in PHP. + +_Get news and updates on the [DecodeLabs blog](https://blog.decodelabs.com)._ + +--- + +## Installation + +Install via Composer: + +```bash +composer require decodelabs/wellspring +``` + +## Usage + +Use Wellspring to register autoloaders with a Priority level - the higher the priority, the earlier the autoloader will be called. + +The library automatically remaps loaders on the fly when necessary (even when spl_autoload_register() and spl_autoload_unregister() are used directly), ensuring edge-case functionality does not interfere with the intended load order. + +Any loaders registered without a priority default to Priority::Medium, and any with matching priorities will be called in the order they were registered. + +```php +use DecodeLabs\Wellspring; +use DecodeLabs\Wellspring\Priority; + +Wellspring::register(function(string $class) { + // This will get called last +}, Priority::Low); + +Wellspring::register(function(string $class) { + // This will get called first +}, Priority::High); + +spl_autoload_register(function(string $class) { + // This will get called second +}); + +spl_autoload_call('test'); +``` + + +## Licensing + +Wellspring is licensed under the MIT License. See [LICENSE](./LICENSE) for the full license text. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..81f7ba3 --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "decodelabs/wellspring", + "description": "PHP autoload management tools", + "type": "library", + "keywords": [ ], + "license": "MIT", + "authors": [ { + "name": "Tom Wright", + "email": "tom@inflatablecookie.com" + } ], + "require": { + "php": "^8.1" + }, + "require-dev": { + "decodelabs/phpstan-decodelabs": "^0.6.7" + }, + "autoload": { + "psr-4": { + "DecodeLabs\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-develop": "0.1.x-dev" + } + } +} diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..56f5fea --- /dev/null +++ b/ecs.php @@ -0,0 +1,13 @@ +paths([__DIR__.'/src']); + $ecsConfig->sets([SetList::CLEAN_CODE, SetList::PSR_12]); +}; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..16f0d34 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + paths: + - src + level: max diff --git a/src/Wellspring.php b/src/Wellspring.php new file mode 100644 index 0000000..25a3bb2 --- /dev/null +++ b/src/Wellspring.php @@ -0,0 +1,214 @@ + + */ + private static array $loaders = []; + + /** + * @var array + */ + private static ?array $functions = null; + + /** + * Register SPL loader with priority + */ + public static function register( + callable $callback, + string|Priority|null $priority = null + ): void { + if ($priority === null) { + $priority = Priority::Medium; + } elseif (is_string($priority)) { + $priority = Priority::from($priority); + } + + $loader = new Loader($callback, $priority); + + if (isset(self::$loaders[$loader->getId()])) { + return; + } + + self::$loaders[$loader->getId()] = $loader; + + spl_autoload_register( + $loader, + true, + $loader->getPriority() === Priority::High + ); + + + if ( + !self::$initialized || + $loader->getPriority() === Priority::High + ) { + if (self::$initialized) { + spl_autoload_unregister([self::class, 'checkQueue']); + } + + self::$initialized = true; + spl_autoload_register([self::class, 'checkQueue'], true, true); + } + } + + /** + * Unregister SPL loader + */ + public static function unregister( + callable $callback + ): void { + $id = self::identifyCallback($callback); + + if (isset(self::$loaders[$id])) { + spl_autoload_unregister(self::$loaders[$id]); + unset(self::$loaders[$id]); + return; + } + + spl_autoload_unregister($callback); + } + + /** + * Get list of registered loaders + * + * @return array + */ + public static function getLoaders(): array + { + return self::$loaders; + } + + /** + * Create unique ID for callback + */ + public static function identifyCallback( + callable $callback + ): string { + if (is_object($callback)) { + return 'spl:' . spl_object_hash($callback); + } + + if (is_array($callback)) { + return 'pn:' . implode('::', $callback); + } + + if (is_string($callback)) { + return 'pn:' . $callback; + } + + return 'fn:' . md5(serialize($callback)); + } + + /** + * Queue checker loader + */ + private static function checkQueue( + string $class + ): void { + self::$initCall++; + $functions = spl_autoload_functions(); + + if ($functions === self::$functions) { + return; + } + + $resetCheckQueue = false; + $currentPriority = Priority::High; + $resetLows = $resetHighs = false; + $lows = $highs = []; + + foreach ($functions as $i => $function) { + // Check queue + if ($function === [self::class, 'checkQueue']) { + if ($i !== 0) { + $resetCheckQueue = true; + } + continue; + } + + // Check priority + if ($function instanceof Loader) { + $priority = $function->getPriority(); + } else { + $priority = Priority::Medium; + } + + switch ($priority) { + case Priority::High: + if ($currentPriority !== Priority::High) { + $highs[] = $function; + $resetHighs = true; + } + break; + + case Priority::Medium: + if ($currentPriority === Priority::High) { + $currentPriority = Priority::Medium; + } elseif ($currentPriority === Priority::Low) { + $resetLows = true; + } + break; + + case Priority::Low: + $currentPriority = Priority::Low; + $lows[] = $function; + break; + } + } + + + // Reset highs + if ($resetHighs) { + $resetCheckQueue = true; + + foreach ($highs as $function) { + spl_autoload_unregister($function); + spl_autoload_register($function, true, true); + } + } + + // Reset lows + if ($resetLows) { + foreach ($lows as $function) { + spl_autoload_unregister($function); + spl_autoload_register($function); + } + } + + // Check queue + if ($resetCheckQueue) { + spl_autoload_unregister([self::class, 'checkQueue']); + spl_autoload_register([self::class, 'checkQueue'], true, true); + } + + self::$functions = spl_autoload_functions(); + self::$orderCall++; + + if ( + $resetHighs || + $resetLows || + $resetCheckQueue + ) { + // Run from the top again + spl_autoload_call($class); + } + } +} diff --git a/src/Wellspring/Loader.php b/src/Wellspring/Loader.php new file mode 100644 index 0000000..4e22812 --- /dev/null +++ b/src/Wellspring/Loader.php @@ -0,0 +1,75 @@ +id = Wellspring::identifyCallback($callback); + $this->callback = Closure::fromCallable($callback); + $this->priority = $priority; + } + + /** + * Get ID + */ + public function getId(): string + { + return $this->id; + } + + /** + * Does callback match check value? + */ + public function isCallback( + callable $callback + ): bool { + return Wellspring::identifyCallback($callback) === $this->id; + } + + /** + * Get callback + */ + public function getCallback(): Closure + { + return $this->callback; + } + + /** + * Get priority + */ + public function getPriority(): Priority + { + return $this->priority; + } + + /** + * Invoke callback + */ + public function __invoke( + string $class + ): void { + ($this->callback)($class); + } +} diff --git a/src/Wellspring/Priority.php b/src/Wellspring/Priority.php new file mode 100644 index 0000000..3eff527 --- /dev/null +++ b/src/Wellspring/Priority.php @@ -0,0 +1,26 @@ + 0, + self::Medium => 1, + self::High => 2 + }; + } +}