diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..f7708e9c8934c38a4e959932dc41d5cacc404c2b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 +tab_width = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..1c336fbbdf71ee15f4d8f1e995cf1f09d026f04a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/vendor export-ignore diff --git a/.gitignore b/.gitignore index 7579f74311d35aae05dd0f0a54537ea7a0034e89..900cbda43d4461ddda0eb1d46938ce7d0339ec55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.DS_Store vendor composer.lock +.phpunit.result.cache +.phpcs-cache diff --git a/README.md b/README.md index 00b9231caa3e24bfcfcb0cfcf614ea92dc26f494..2ab2a727888fdd2372ecf75e1724346340a74a9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WordPress plugin initializer -Bootstrap for your plugins. +Boot your plugin with superpowers. ## Installation @@ -12,42 +12,17 @@ composer require wpdesk/wp-init ## Creating a Plugin -A plugin is a simple object created to help bootstrap functionality by allowing you to easily -retrieve plugin information, reference internal files and URLs, and, most importantly, register -hooks. +Preferred method of using this library exercise Object Oriented Programming and organizing your +actions and filters in a multiple classes, although it isn't the only way you can interact (and +benefit from this library). -```php -<?php -/** - * Plugin Name: Example Plugin - */ - -use WPDesk\Init\PluginInit; +The plugin initialization consists of the following steps: -require __DIR__ . '/vendor/autoload.php'; - -$plugin = (new PluginInit())->init(); -``` +1. Create a regular main plugin file, following [header requirements](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) +1. Prepare DI container definitions for your services. +1. Declare all classes included in hook binding. -`$plugin` is an instance of `Plugin`, which provides a basic API to access information about the plugin. - -You can find more elaborate examples for plugin configuration in `tests/Fixtures/` directory. - -### Plugin configuration - -Although, you don't need additional configuration to begin work with your plugin in OOP manner, -most of the time you will need a few more steps to make the most of plugin initializer. - -#### Using dependency injection container - -Paired with `php-di/php-di`, you are able to take leverage of dependency injection in your -WordPress plugin. `wp-init` is ready to pick up all of your hook provider classes from special -container entry named `hooks`. Any class registered in this array will be called during -`plugins_loaded` hook. - -```shell -composer require php-di/php-di -``` +The above limits your main plugin file to a short and simple structure. ```php <?php @@ -55,167 +30,58 @@ composer require php-di/php-di * Plugin Name: Example Plugin */ -use WPDesk\Init\PluginInit; +use WPDesk\Init\Init; require __DIR__ . '/vendor/autoload.php'; -$plugin = (new PluginInit()) - ->add_container_definitions([ - 'hooks' => [ - \DI\autowire( \Example\PostType\BookPostType::class ) - ] - ]) - ->init(); +Init::setup('config.php')->boot(); ``` -#### Target environment requirements +### Plugin configuration -`wp-init` also integrates with `wpdesk/wp-basic-requirements` to validate target environment and -detect, whether your plugin can actually instantiate. With this addition, if your plugin fails -to positively validate environment, your hooks are not triggered, and the website admin is -provided with actionable message. +For plugin configuration, you may focus on succinct, declarative configuration. -```shell -composer require wpdesk/wp-basic-requirements -``` +[Supported configuration](docs/configuration.md): ```php <?php -/** - * Plugin Name: Example Plugin - */ - -use WPDesk\Init\PluginInit; -require __DIR__ . '/vendor/autoload.php'; - -$plugin = (new PluginInit()) - ->set_requirements([ - 'wp' => '6.0', - 'php' => '7.2' - ]) - ->init(); -``` - -## Hook Providers - -Related functionality can be encapsulated in a class called a "hook provider" that's registered when bootstrapping the plugin. - -Hook providers allow you to encapsulate related functionality, maintain state without using globals, namespace methods without prefixing functions, limit access to internal methods, and make unit testing easier. +return [ + 'hook_resources_path' => 'config/hook_providers', + 'services' => 'config/services.inc.php', + 'cache_path' => 'generated', -For an example, the `WPDesk\Init\Provider\I18n` class is a default hook provider that automatically -loads the text domain so the plugin can be translated. + 'requirements' => [ + 'plugins' => [ + 'name' => 'woocommerce/woocommerce.php', + 'nice_name' => 'WooCommerce', + ] + ], -The only requirement for a hook provider is that it should implement the `HookProvider` -interface by defining a method called `register_hooks()`. - -Hook providers are registered with the main plugin instance by calling `Plugin::register_hooks()` like this: - -```php -<?php -$plugin->register_hooks( - new \Cedaro\WP\Plugin\Provider\I18n(), - new \Example\PostType\BookPostType() -); + 'plugin_class_name' => 'Example\Plugin', +]; ``` -The `BookPostType` provider might look something like this: +## Usage with `wpdesk/wp-builder` -```php -<?php -namespace Example\PostType; - -use WPDesk\Init\Provider\AbstractHookProvider; - -class BookPostType extends AbstractHookProvider { - const POST_TYPE = 'book'; - - public function register_hooks() { - $this->add_action( 'init', 'register_post_type' ); - $this->add_action( 'init', 'register_meta' ); - } - - protected function register_post_type() { - register_post_type( static::POST_TYPE, $this->get_args() ); - } - - protected function register_meta() { - register_meta( 'post', 'isbn', array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => 'sanitize_text_field', - 'show_in_rest' => true, - ) ); - } - - protected function get_args() { - return array( - 'hierarchical' => false, - 'public' => true, - 'rest_base' => 'books', - 'show_ui' => true, - 'show_in_menu' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => true, - ); - } -} -``` -<!-- -## Protected Hook Callbacks - -In WordPress, it's only possible to use public methods of a class as hook callbacks, but in the `BookPostType` hook provider above, the callbacks are protected methods of the class. - -Locking down the API like that is possible using the `HooksTrait` [developed by John P. Bloch](https://github.com/johnpbloch/wordpress-dev). ---> +As a legacy support, it is possible to power up your existing codebase, which uses +`wpdesk/wp-builder` with this library capabilities, as autowired services. -## Plugin Awareness +The only change, you have to do (besides configuration of services) is adding _hookables_ as class +string, ready for handling by DI container: -A hook provider may implement the `PluginAwareInterface` to automatically receive a reference to the plugin when its hooks are registered. - -For instance, in this class the `enqueue_assets()` method references the internal `$plugin` property to retrieve the URL to a JavaScript file in the plugin. - -```php -<?php -namespace Structure\Provider; - -use Cedaro\WP\Plugin\AbstractHookProvider; - -class Assets extends AbstractHookProvider { - public function register_hooks() { - $this->add_action( 'wp_enqueue_scripts', 'enqueue_assets' ); - } - - protected function enqueue_assets() { - wp_enqueue_script( - 'structure', - $this->plugin->get_url( 'assets/js/structure.js' ) - ); - } -} +```diff +- $this->add_hookable( new \WPDesk\Init\Provider\I18n() ); ++ $this->add_hookable( \WPDesk\Init\Provider\I18n::class ); ``` -Another example is the `I18n` provider mentioned earlier. It receives a reference to the plugin object so that it can use the plugin's base name and slug to load the text domain. - -Classes that extend `AbstractHookProvider` are automatically "plugin aware." - ## Credits -This package is heavily inspired by Cedaro's [`wp-plugin`](https://github.com/cedaro/wp-plugin/) +This package is heavily inspired by Cedaro's [`wp-plugin`](https://github.com/cedaro/wp-plugin/) and Alain Schlesser's [`basic-scaffold`](https://github.com/mwpd/basic-scaffold). -## Roadmap - -1. Add support for path based hook providers discovery similar to Symfony's [controllers resolving](https://github.com/symfony/demo/blob/3787b9f71f6bee24f1ed0718b9a808d824008776/config/routes.yaml#L15-L17) -1. Improve `wpdesk/wp-basic-requirements` library. This is not related directly to this project, - but internals could be rewritten. -1. Scrap plugin data from plugin comment -1. Allow hooks to be called from private and protected methods (in PHP <8.1) -1. Support *bundles* of hook providers. This should be easy to extend plugin capabilities with - shared functions, preserving minimal init system - ## License -Copyright (c) 2023 WPDesk +Copyright (c) 2024 WPDesk This library is licensed under MIT. diff --git a/bin/wpinit b/bin/wpinit new file mode 100755 index 0000000000000000000000000000000000000000..e23eed6e2635b1e9c844ed06214edef00eec0ef7 --- /dev/null +++ b/bin/wpinit @@ -0,0 +1,17 @@ +#!/usr/bin/env php +<?php + +use WPDesk\Init\Util\PhpFileDumper; + +include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; + +$filename = $argv[1] ?? null; + +$parser = new \WPDesk\Init\Plugin\DefaultHeaderParser(); + +$data = $parser->parse( $filename ); + +$dumper = new PhpFileDumper(); +$dumper->dump( $parser->parse( $filename ), $argv[2] ); + +//echo "Compile static plugin resources."; diff --git a/composer.json b/composer.json index fb30e761a07ba9ecca8c38c6758ab66be97081d6..c00e7df202de002bc2ce616cf1bad6db0d7a6de9 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,13 @@ { "name": "wpdesk/wp-init", "description": "Bootstrap for a WordPress plugin", - "minimum-stability": "stable", "license": "MIT", "type": "library", "homepage": "https://gitlab.wpdesk.dev/wpdesk/wp-init", "keywords": [ - "wordpress" + "wordpress", + "plugin", + "bootstrap" ], "authors": [ { @@ -19,6 +20,9 @@ "role": "Developer" } ], + "bin": [ + "bin/wpinit" + ], "autoload": { "psr-4": { "WPDesk\\Init\\": "src" @@ -34,24 +38,31 @@ }, "require": { "php": ">=7.2 | ^8", - "psr/container": "^1 || ^2" + "php-di/php-di": "^6 || ^7", + "psr/container": "~1.0.0 || ^2" }, "require-dev": { - "wpdesk/wp-basic-requirements": "^3", - "php-di/php-di": "^6 || ^7", + "brain/monkey": "^2.6", "phpunit/phpunit": "^8 || ^9", "symfony/filesystem": "^6.2", - "brain/monkey": "^2.6" + "wpdesk/phpstan-rules": "^1.1", + "wpdesk/wp-basic-requirements": "^3", + "wpdesk/wp-builder": "^2.0", + "wpdesk/wp-code-sniffer": "^1.3" }, "suggest": { - "wpdesk/wp-basic-requirements": "Enables your plugin to check an environment requirement before instantiation, e.g. PHP version or active plugins", - "php-di/php-di": "Allows a plugin to seamlessly integrate with a dependency injection container" + "wpdesk/wp-basic-requirements": "Enables your plugin to check an environment requirement before instantiation, e.g. PHP version or active plugins" }, "conflict": { - "wpdesk/wp-basic-requirements": "<3, >=4", - "php-di/php-di": "<6, >=8" + "wpdesk/wp-basic-requirements": "<3 >=4" }, "scripts": { "test": "vendor/bin/phpunit --bootstrap tests/bootstrap.php ./tests" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..3ba49a8a6099fa573a625671f569b99f8d05c853 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,64 @@ +# Configuration + +`wp-init` relies on declarative configuration, which encapsulates process of attaching hooks to +WordPress life cycle and provides some additional features, like filling DI container with your +services definitions. + +## `hook_resources_path` + +Pass a path to the file/directory with your hook actions. Configuration accepts any valid path +string, relative or absolute, either `hook_providers` or `__DIR__ . '/hook_providers/plugins_loaded.php'` + +Files are mapped to hooks by name, so `woocommerce_init.php` is registered inside `woocommerce_init` +action. The exception is `index.php` file which is flushed immediately. + +Example of a hook resource content: + +```php +<?php +// plugins_loaded.php + +return [ + MyCoolTitleChanger::class, + AnotherHookAction::class, + function ( Migarator $migrator ) { + // You can even use a closure, to execute simple actions. + // Arguments are injected by DI container. + $migrator->migrate(); + } +]; +``` + +## `services` + +As you add more services with increasing complexity, you will need to provide some kind of +definitions for a DI container to create objects. Pass a path to a file, which will hold such +definitions. Refer to [php-di documentation](https://php-di.org/doc/definitions.html) for more +information on such file content. + +> *Warning* +> +> If you are using _shortcut_ functions from `php-di/php-di` (e.g. `DI\autowire`, `DI\create`), you +> must load them first. Add `require __DIR__ . '/vendor_prefixed/php-di/php-di/src/functions.php';` +> to your plugin file. + +## `cache_path` + +Plugin header data and compiled DI container is cached in a directory specified by this +setting. Defaults to `generated`. + +## `requirements` + +**This setting only works when `wpdesk/wp-basic-requirements` is installed.** + +Enables your plugin to check an environment requirement before instantiation, e.g. PHP version or +active plugins. Refer to [wp-basic-requirements documentation](https://gitlab.wpdesk.dev/wpdesk/wp-basic-requirements) +for more information on setting structure. + +## `plugin_class_name` + +**This setting only works when `wpdesk/wp-builder` is installed.** + +When a plugin is used in [legacy mode](legacy.md), `plugin_class_name` is used to create an instance +of main plugin class. This setting is required to enable legacy mode. Despite that, +`WPDesk\Init\Plugin\Plugin` is still accessible to your services. diff --git a/docs/legacy.md b/docs/legacy.md new file mode 100644 index 0000000000000000000000000000000000000000..bab5e4a1b6753596c213e82e735a2344f0540f8d --- /dev/null +++ b/docs/legacy.md @@ -0,0 +1,7 @@ +# Migration from `wpdesk/wp-builder` + +Projects using `wpdesk/wp-builder` (or `wpdesk/wp-plugin-flow`) may easily migrate to `wp-init` +in a few steps: + +1. Install `wpdesk/wp-init`. +1. diff --git a/docs/prefixing.md b/docs/prefixing.md new file mode 100644 index 0000000000000000000000000000000000000000..b5e9589d7003ff92d28b4c10a6448d862663da8f --- /dev/null +++ b/docs/prefixing.md @@ -0,0 +1,37 @@ +# Prefixing `wp-init` with `php-scoper` + +When developing plugins, it's worth to prefix all dependencies to avoid version collision between +different plugins loaded during runtime. For this library to enable prefixing you will need to +introduce following configuration to your `scoper.inc.php` file. + +- Whitelist `vendor/php-di/php-di/src/Compiler/Template.php` +- Include `php-di` and it's dependencies in finders + +> *Note* +> +> Pay attention to actual installed `php-di/php-di` version, as it's dependencies may change, +> requiring to update `scoper.inc.php` accordingly. + +## Example configuration + +**`php-di/php-di` up to 6.3.5** + +```php +return [ + 'finder' => Finder::create()->in(['vendor/wpdesk/wp-init', 'vendor/php-di', 'vendor/opis/closure']), + 'files-whitelist' => [ + 'vendor/php-di/php-di/src/Compiler/Template.php' + ], +]; +``` + +**`php-di/php-di` since 6.4.0** + +```php +return [ + 'finder' => Finder::create()->in(['vendor/wpdesk/wp-init', 'vendor/php-di', 'vendor/laravel/serializable-closure']), + 'files-whitelist' => [ + 'vendor/php-di/php-di/src/Compiler/Template.php' + ], +]; +``` diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000000000000000000000000000000000000..05dfc78138a32ed739f2e9ad714552ed440d1438 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<ruleset name="WordPress Coding Standards for WP Desk Plugin"> + + <!-- + ############################################################################# + COMMAND LINE ARGUMENTS + https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml + ############################################################################# + --> + + <!-- Scan all files. --> + <file>./src</file> + + <!-- Only check PHP files. --> + <arg name="extensions" value="php"/> + + <!-- Show progress, show the error codes for each message (source). --> + <arg value="sp"/> + + <!-- Check up to 8 files simultaneously. --> + <arg name="parallel" value="8"/> + + <!-- Cache outcomes for better performance. Remember to add the file to .gitignore. --> + <arg name="cache" value="./.phpcs-cache"/> + + <!-- + ############################################################################# + USE THE WPDeskCS RULESET + ############################################################################# + --> + + <!-- Define plugin text domain for i18n. --> + <config name="text_domain" value="wp-init"/> + + <!-- This value should be aligned with WordPress support version declared in plugin header --> + <config name="minimum_supported_wp_version" value="6.2"/> + + <!-- Set value aligned with supported PHP Version for PHPCompatibilityWP check. --> + <config name="testVersion" value="7.2-"/> + + <rule ref="WPDeskPlugin"/> + + <!-- + ############################################################################## + CUSTOM RULES + ############################################################################## + --> + <rule ref="Squiz.ControlStructures.InlineIfDeclaration"> + <exclude name="Squiz.ControlStructures.InlineIfDeclaration.NoBrackets"/> + </rule> +</ruleset> diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000000000000000000000000000000000..41e0048877bebd4ae3ff207f17c618ad0ee7d30e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,4 @@ +parameters: + level: 6 + paths: + - src/ diff --git a/src/Binding/Binder.php b/src/Binding/Binder.php new file mode 100644 index 0000000000000000000000000000000000000000..0dc334f13f1d4aa581da04fd090154b9dc5e12ed --- /dev/null +++ b/src/Binding/Binder.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding; + +interface Binder { + + public function bind( Definition $def ): void; +} diff --git a/src/Binding/Binder/CallableBinder.php b/src/Binding/Binder/CallableBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..70858003a1e18d85843bd9deb98c58037ee913f8 --- /dev/null +++ b/src/Binding/Binder/CallableBinder.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Binder; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\ComposableBinder; +use WPDesk\Init\Binding\Definition; +use WPDesk\Init\Binding\Definition\CallableDefinition; + +class CallableBinder implements ComposableBinder { + + /** @var ContainerInterface */ + private $container; + + public function __construct( ContainerInterface $c ) { + $this->container = $c; + } + + public function can_bind( Definition $def ): bool { + return $def instanceof CallableDefinition; + } + + public function bind( Definition $def ): void { + $ref = new \ReflectionFunction( $def->value() ); + $parameters = []; + foreach ( $ref->getParameters() as $ref_param ) { + $parameters[] = $this->container->get( $ref_param->getType()->getName() ); + } + $ref->invokeArgs( $parameters ); + } +} diff --git a/src/Binding/Binder/CompositeBinder.php b/src/Binding/Binder/CompositeBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..8b88634687b2fbb7b07c99b710db520398b33c2f --- /dev/null +++ b/src/Binding/Binder/CompositeBinder.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Binder; + +use WPDesk\Init\Binding\Binder; +use WPDesk\Init\Binding\Definition; +use WPDesk\Init\Binding\Definition\HookableDefinition; + +final class CompositeBinder implements Binder { + + /** @var Binder[] */ + private $binders; + + public function __construct( Binder ...$binders ) { + $this->binders = $binders; + } + + public function add( Binder $binder ): void { + $this->binders[] = $binder; + } + + public function bind( Definition $def ): void { + foreach ( $this->binders as $binder ) { + if ( $binder->can_bind( $def ) ) { + $binder->bind( $def ); + break; + } + } + } +} diff --git a/src/Binding/Binder/HookableBinder.php b/src/Binding/Binder/HookableBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..f8426dacddff213d58bdd0be5e9f0d68fb9ffaee --- /dev/null +++ b/src/Binding/Binder/HookableBinder.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Binder; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\ComposableBinder; +use WPDesk\Init\Binding\Definition; +use WPDesk\Init\Binding\Definition\HookableDefinition; + +class HookableBinder implements ComposableBinder { + + /** @var ContainerInterface */ + private $container; + + public function __construct( ContainerInterface $c ) { + $this->container = $c; + } + + public function can_bind( Definition $def ): bool { + return $def instanceof HookableDefinition; + } + + public function bind( Definition $def ): void { + $this->container->get( $def->value() )->hooks(); + } +} diff --git a/src/Binding/Binder/ObservableBinder.php b/src/Binding/Binder/ObservableBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..8012f7ba35179c8e53ca70da1347be5cad746438 --- /dev/null +++ b/src/Binding/Binder/ObservableBinder.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Binder; + +use WPDesk\Init\Binding\Binder; +use WPDesk\Init\Binding\ComposableBinder; +use WPDesk\Init\Binding\Definition; + +/** + * Binder decorator, specifically built for testing purposes. Can naively investigate other binders. + */ +final class ObservableBinder implements ComposableBinder { + + /** @var Binder */ + private $binder; + + private $binds_count = 0; + + public function __construct( Binder $b ) { + $this->binder = $b; + } + + public function bind( Definition $def ): void { + $this->binder->bind( $def ); + $this->binds_count++; + } + + public function can_bind( Definition $def ): bool { + if ( $this->binder instanceof ComposableBinder ) { + return $this->binder->can_bind( $def ); + } + + return true; + } + + public function binds_count(): int { + return $this->binds_count; + } +} diff --git a/src/Binding/Binder/StoppableBinder.php b/src/Binding/Binder/StoppableBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..8917f7867e90c75ac1f20f2809cef78fb07149f9 --- /dev/null +++ b/src/Binding/Binder/StoppableBinder.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Binder; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\ComposableBinder; +use WPDesk\Init\Binding\StoppableBinder as Stop; +use WPDesk\Init\Binding\Definition; +use WPDesk\Init\Binding\Binder as BinderInstance; +use WPDesk\Init\Binding\Definition\HookableDefinition; + +class StoppableBinder implements ComposableBinder { + + /** @var ContainerInterface */ + private $container; + + /** @var Binder */ + private $binder; + + private $should_stop = false; + + public function __construct( BinderInstance $b, ContainerInterface $c ) { + $this->binder = $b; + $this->container = $c; + } + + public function can_bind( Definition $def ): bool { + return $this->binder->can_bind( $def ); + } + + public function bind( Definition $def ): void { + if ( $this->should_stop === true ) { + return; + } + + $this->binder->bind( $def ); + + if ( $this->can_be_stoppable( $def ) ) { + $binding = $this->container->get( $def->value() ); + if ( $binding instanceof Stop && $binding->should_stop() ) { + $this->should_stop = true; + } + } + } + + private function can_be_stoppable( Definition $def ): bool { + return is_string( $def->value() ) && class_exists( $def->value() ); + } +} diff --git a/src/Binding/ComposableBinder.php b/src/Binding/ComposableBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..8498244e65ee2970b27a9e3729dc26e546d4f64c --- /dev/null +++ b/src/Binding/ComposableBinder.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding; + +/** + * Can be composed with other binders within {@see CompositeBinder} class. + */ +interface ComposableBinder extends Binder { + + public function can_bind( Definition $def ): bool; +} diff --git a/src/Binding/Definition.php b/src/Binding/Definition.php new file mode 100644 index 0000000000000000000000000000000000000000..7184bf5f705909688e9b440e7be16abe3a8bf71c --- /dev/null +++ b/src/Binding/Definition.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding; + +/** + * @template T + */ +interface Definition { + + public function hook(): ?string; + + /** @return T */ + public function value(); +} diff --git a/src/Binding/Definition/CallableDefinition.php b/src/Binding/Definition/CallableDefinition.php new file mode 100644 index 0000000000000000000000000000000000000000..9bf3774f733e3c1c074ce4d5614288b1ab8c1da4 --- /dev/null +++ b/src/Binding/Definition/CallableDefinition.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Definition; + +use WPDesk\Init\Binding\Definition; + +/** @implements Definition<callable> */ +class CallableDefinition implements Definition { + + /** @var ?string */ + private $hook; + + /** @var callable */ + private $callable; + + public function __construct( + callable $callable, + ?string $hook = null, + ) { + $this->callable = $callable; + $this->hook = $hook; + } + + public function hook(): ?string { + return $this->hook; + } + + public function value() { + return $this->callable; + } +} diff --git a/src/Binding/Definition/HookableDefinition.php b/src/Binding/Definition/HookableDefinition.php new file mode 100644 index 0000000000000000000000000000000000000000..286d9ba5157f4a2b6fc92d7dda9b5a0eabf4f8f0 --- /dev/null +++ b/src/Binding/Definition/HookableDefinition.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Definition; + +use WPDesk\Init\Binding\Definition; + +/** @implements Definition<class-string<Hookable>> */ +class HookableDefinition implements Definition { + + /** @var ?string */ + private $hook; + + /** @var class-string<Hookable> */ + private $hookable; + + public function __construct( + string $hookable, + ?string $hook = null, + ) { + $this->hook = $hook; + $this->hookable = $hookable; + } + + public function hook(): ?string { + return $this->hook; + } + + public function value() { + return $this->hookable; + } +} diff --git a/src/Binding/Definition/UnknownDefinition.php b/src/Binding/Definition/UnknownDefinition.php new file mode 100644 index 0000000000000000000000000000000000000000..87522547258f4fad569e984d85517b8fef48779f --- /dev/null +++ b/src/Binding/Definition/UnknownDefinition.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding\Definition; + +use WPDesk\Init\Binding\Definition; + +/** @implements Definition<mixed> */ +class UnknownDefinition implements Definition { + + /** @var ?string */ + private $hook; + + /** @var mixed */ + private $value; + + public function __construct( + $value, + ?string $hook = null, + ) { + $this->value = $value; + $this->hook = $hook; + } + + public function hook(): ?string { + return $this->hook; + } + + public function value() { + return $this->value; + } +} diff --git a/src/Binding/DefinitionFactory.php b/src/Binding/DefinitionFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..5b66072e90754f9850266fb160bacbab83e5919b --- /dev/null +++ b/src/Binding/DefinitionFactory.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding; + +use WPDesk\Init\Binding\Definition\CallableDefinition; +use WPDesk\Init\Binding\Definition\HookableDefinition; +use WPDesk\Init\Binding\Definition\UnknownDefinition; + +class DefinitionFactory { + + public function create( $value, ?string $hook ): Definition { + if ( is_string( $value ) && class_exists( $value ) && is_subclass_of( $value, Hookable::class, true ) ) { + return new HookableDefinition( $value, $hook ); + } + + if ( is_callable( $value ) ) { + return new CallableDefinition( $value, $hook ); + } + + return new UnknownDefinition( $value, $hook ); + } +} diff --git a/src/Binding/Hookable.php b/src/Binding/Hookable.php new file mode 100644 index 0000000000000000000000000000000000000000..3fb1e51b46ee5f6e4a0a9c410e5453bce0acb0eb --- /dev/null +++ b/src/Binding/Hookable.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Binding; + +interface Hookable extends \WPDesk\PluginBuilder\Plugin\Hookable { + + public function hooks(): void; +} diff --git a/src/Binding/Loader/ArrayDefinitions.php b/src/Binding/Loader/ArrayDefinitions.php new file mode 100644 index 0000000000000000000000000000000000000000..865b0a3cd46c46c390515b66401d1add22337014 --- /dev/null +++ b/src/Binding/Loader/ArrayDefinitions.php @@ -0,0 +1,47 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Binding\Loader; + +use WPDesk\Init\Binding\Definition; +use WPDesk\Init\Binding\DefinitionFactory; +use WPDesk\Init\Configuration\ReadableConfig; +use WPDesk\Init\Plugin\Plugin; + +class ArrayDefinitions implements BindingDefinitions { + + /** @var array */ + private $bindings; + + /** @var DefinitionFactory */ + private $factory; + + public function __construct( array $bindings, ?DefinitionFactory $factory = null) { + $this->bindings = $bindings; + $this->factory = $factory ?? new DefinitionFactory(); + } + + public function load(): iterable { + yield from $this->normalize( $this->bindings ); + } + + private function normalize( iterable $bindings ): iterable { + foreach ( $bindings as $key => $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $unit ) { + yield $this->create( $unit, $key ); + } + } else { + yield $this->create( $value, $key ); + } + } + } + + /** + * @param mixed $value + * @param int|string $hook + */ + private function create( $value, $hook ): Definition { + return $this->factory->create( $value, is_int( $hook ) ? null : $hook ); + } +} diff --git a/src/Binding/Loader/BindingDefinitions.php b/src/Binding/Loader/BindingDefinitions.php new file mode 100644 index 0000000000000000000000000000000000000000..c253c3154b6f65a3fdce501cf510eb2c777402e0 --- /dev/null +++ b/src/Binding/Loader/BindingDefinitions.php @@ -0,0 +1,14 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Binding\Loader; + +use WPDesk\Init\Binding\Definition; + +interface BindingDefinitions { + + /** + * @return iterable<Definition> + */ + public function load(): iterable; +} diff --git a/src/Binding/Loader/CompositeBindingLoader.php b/src/Binding/Loader/CompositeBindingLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..81f501bdafd2c64bee62e123bdaa2db6369da37b --- /dev/null +++ b/src/Binding/Loader/CompositeBindingLoader.php @@ -0,0 +1,26 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Binding\Loader; + +use WPDesk\Init\Plugin\Plugin; + +class CompositeBindingLoader implements BindingDefinitions { + + /** @var BindingDefinitionLoader[] */ + private $loaders; + + public function __construct( BindingDefinitions ...$loaders ) { + $this->loaders = $loaders; + } + + public function load(): iterable { + foreach ( $this->loaders as $loader ) { + yield from $loader->load(); + } + } + + public function add( BindingDefinitions $loader ): void { + $this->loaders[] = $loader; + } +} diff --git a/src/Binding/Loader/EmptyDefinitions.php b/src/Binding/Loader/EmptyDefinitions.php new file mode 100644 index 0000000000000000000000000000000000000000..9048f2fa1bd6bd562cea83f93faf40d180b1a94e --- /dev/null +++ b/src/Binding/Loader/EmptyDefinitions.php @@ -0,0 +1,11 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Binding\Loader; + +final class EmptyDefinitions implements BindingDefinitions { + + public function load(): iterable { + return []; + } +} diff --git a/src/Binding/Loader/FilesystemDefinitions.php b/src/Binding/Loader/FilesystemDefinitions.php new file mode 100644 index 0000000000000000000000000000000000000000..f86c22e5848685a0e272faa9d207a10fc09b59f5 --- /dev/null +++ b/src/Binding/Loader/FilesystemDefinitions.php @@ -0,0 +1,50 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Binding\Loader; + +use WPDesk\Init\Binding\DefinitionFactory; +use WPDesk\Init\Util\Path; +use WPDesk\Init\Util\PhpFileLoader; + +class FilesystemDefinitions implements BindingDefinitions { + + /** @var Path */ + private $path; + + /** @var PhpFileLoader */ + private $loader; + + /** @var DefinitionFactory */ + private $def_factory; + + public function __construct( $path, ?PhpFileLoader $loader = null, ?DefinitionFactory $def_factory = null ) { + $this->path = new Path( (string) $path ); + $this->loader = $loader ?? new PhpFileLoader(); + $this->def_factory = $def_factory ?? new DefinitionFactory(); + } + + public function load(): iterable { + if ( $this->path->is_directory() ) { + foreach ( $this->path->read_directory() as $filename ) { + yield from $this->load_from_file( $filename ); + } + } else { + yield from $this->load_from_file( $this->path ); + } + } + + private function load_from_file( Path $filename ): iterable { + if ( ! $filename->is_file() ) { + return; + } + + $hooks = $this->loader->load( (string) $filename ); + + if ( $filename->get_basename() !== 'index.php' ) { + $hooks = [ $filename->get_filename_without_extension() => $hooks ]; + } + + yield from (new ArrayDefinitions( $hooks ) )->load(); + } +} diff --git a/src/Binding/StoppableBinder.php b/src/Binding/StoppableBinder.php new file mode 100644 index 0000000000000000000000000000000000000000..daf5852fdc2f9905705c60a0a151a96c67e627be --- /dev/null +++ b/src/Binding/StoppableBinder.php @@ -0,0 +1,9 @@ +<?php +declare(strict_types=1); + +namespace WPDesk\Init\Binding; + +interface StoppableBinder extends Hookable { + + public function should_stop(): bool; +} diff --git a/src/Conditional.php b/src/Conditional.php deleted file mode 100644 index 7851ce41dc48099fe8682a6444c2dc4d33ac08c2..0000000000000000000000000000000000000000 --- a/src/Conditional.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -declare( strict_types=1 ); - -namespace WPDesk\Init; - -/** - * Something that can be instantiated conditionally. - * - * In hook provider context a class marked as being conditional can be asked whether its - * hooks should be fired and integrated into WordPress system. An example would be a service that is - * only available on the admin backend. - * - * This allows for a more systematic and automated optimization of how the - * different parts of the plugin are enabled or disabled. - * - * @author Alain Schlesser <alain.schlesser@gmail.com> - */ -interface Conditional { - - /** - * Check whether the conditional object is currently needed. - * - * @return bool Whether the conditional object is needed. - */ - public function is_needed(): bool; -} diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php new file mode 100644 index 0000000000000000000000000000000000000000..69c5b8cdc637cf6cf58540b3055bbdfb9d056117 --- /dev/null +++ b/src/Configuration/Configuration.php @@ -0,0 +1,60 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Configuration; + +/** + * @implements \ArrayAccess<string, mixed> + */ +class Configuration implements ReadableConfig, \ArrayAccess { + + /** @var array<string, mixed> */ + private $config; + + public function __construct( array $config ) { + $this->config = $config; + } + + /** + * @param string $key + * @param mixed $default + * + * @return mixed|null + */ + public function get( string $key, $default = null ) { + return $this->config[ $key ] ?? $default; + } + + public function has( string $key ): bool { + return isset( $this->config[ $key ] ); + } + + public function set( string $key, $value ): void { + $this->config[ $key ] = $value; + } + + public function remove( string $key ): void { + unset( $this->config[ $key ] ); + } + + public function offsetExists( $offset ): bool { + return $this->has( $offset ); + } + + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + return $this->get( $offset ); + } + + public function offsetSet( $offset, $value ): void { + if ( $offset === null ) { + throw new \InvalidArgumentException( 'Cannot set value without key.' ); + } + + $this->set( $offset, $value ); + } + + public function offsetUnset( $offset ): void { + $this->remove( $offset ); + } +} diff --git a/src/Configuration/ReadableConfig.php b/src/Configuration/ReadableConfig.php new file mode 100644 index 0000000000000000000000000000000000000000..41491e2af09d3736191e3fdc28e928cec05481a6 --- /dev/null +++ b/src/Configuration/ReadableConfig.php @@ -0,0 +1,14 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Configuration; + +/** + * Allows to read configuration. + */ +interface ReadableConfig { + + public function get( string $key, $default = null ); + + public function has( string $key ): bool; +} diff --git a/src/ContainerAwareInterface.php b/src/ContainerAwareInterface.php deleted file mode 100644 index 95b285e1bece984f45e0830fb9cc05e27e5a41b5..0000000000000000000000000000000000000000 --- a/src/ContainerAwareInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace WPDesk\Init; - -use Psr\Container\ContainerInterface; - -interface ContainerAwareInterface { - - public function set_container( ContainerInterface $container ): void; - -} diff --git a/src/ContainerAwareTrait.php b/src/ContainerAwareTrait.php deleted file mode 100644 index 079701426e7f433b578fbeabe739680fdf4adbb6..0000000000000000000000000000000000000000 --- a/src/ContainerAwareTrait.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace WPDesk\Init; - -use Psr\Container\ContainerInterface; - -trait ContainerAwareTrait { - - /** @var ContainerInterface */ - private $container; - - public function set_container( ContainerInterface $container ): void { - $this->container = $container; - } -} diff --git a/src/DependencyInjection/ContainerBuilder.php b/src/DependencyInjection/ContainerBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..c147090d3c406564dd0730a2c3f0d16c74bd5197 --- /dev/null +++ b/src/DependencyInjection/ContainerBuilder.php @@ -0,0 +1,37 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\DependencyInjection; + +use DI\Container; +use DI\ContainerBuilder as DiBuilder; +use DI\Definition\Source\DefinitionSource; + +final class ContainerBuilder { + + /** @var DiBuilder */ + private $original_builder; + + public function __construct( DiBuilder $original_builder ) { + $this->original_builder = $original_builder; + } + + /** + * Add definitions to the container. + * + * @param string|array|DefinitionSource ...$definitions + * Can be an array of definitions, the name of a file containing definitions or + * a DefinitionSource object. + * + * @return $this + */ + public function add_definitions( ...$definitions ): self { + $this->original_builder->addDefinitions( ...$definitions ); + + return $this; + } + + public function build(): Container { + return $this->original_builder->build(); + } +} diff --git a/src/Extension/BuiltinExtension.php b/src/Extension/BuiltinExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..b896babff5997f5879aabbefffb4be7c78803eb1 --- /dev/null +++ b/src/Extension/BuiltinExtension.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Extension; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\Loader\BindingDefinitions; +use WPDesk\Init\Binding\Loader\FilesystemDefinitions; +use WPDesk\Init\Configuration\ReadableConfig; +use WPDesk\Init\DependencyInjection\ContainerBuilder; +use WPDesk\Init\Plugin\Plugin; + +class BuiltinExtension implements Extension { + + public function bindings( ContainerInterface $c ): BindingDefinitions { + return new FilesystemDefinitions( __DIR__ . '/../Resources/bindings' ); + } + + public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void { + $builder->add_definitions( __DIR__ . '/../Resources/services.inc.php' ); + } +} diff --git a/src/Extension/CommonBinding/CustomOrdersTableCompatibility.php b/src/Extension/CommonBinding/CustomOrdersTableCompatibility.php new file mode 100644 index 0000000000000000000000000000000000000000..aa33b90c8ef505b90c8965835ebcb7b8c8909c3a --- /dev/null +++ b/src/Extension/CommonBinding/CustomOrdersTableCompatibility.php @@ -0,0 +1,32 @@ +<?php + +namespace WPDesk\Init\Extension\CommonBinding; + +use WPDesk\Init\Binding\Hookable; +use WPDesk\Init\Plugin\Plugin; + +class CustomOrdersTableCompatibility implements Hookable { + + /** @var Plugin */ + private $plugin; + + public function __construct( Plugin $plugin ) { + $this->plugin = $plugin; + } + + public function hooks(): void { + add_action('before_woocommerce_init', $this); + } + + public function __invoke(): void { + $features_util_class = '\\' . 'Automattic' . '\\' . 'WooCommerce' . '\\' . 'Utilities' . '\\' . 'FeaturesUtil'; + if ( class_exists( $features_util_class ) ) { + $features_util_class::declare_compatibility( + 'custom_order_tables', + $this->plugin->get_basename(), + true + ); + } + + } +} diff --git a/src/Extension/CommonBinding/I18n.php b/src/Extension/CommonBinding/I18n.php new file mode 100644 index 0000000000000000000000000000000000000000..a5221732e4a9c3f5f4fae4e7b76a5259898f6662 --- /dev/null +++ b/src/Extension/CommonBinding/I18n.php @@ -0,0 +1,32 @@ +<?php + +namespace WPDesk\Init\Extension\CommonBinding; + +use WPDesk\Init\Binding\Hookable; +use WPDesk\Init\Plugin\Plugin; + +class I18n implements Hookable { + + /** @var Plugin */ + private $plugin; + + public function __construct( Plugin $plugin ) { + $this->plugin = $plugin; + } + + public function hooks(): void { + if ( did_action( 'plugins_loaded' ) ) { + $this->__invoke(); + } else { + add_action( 'plugins_loaded', $this ); + } + } + + public function __invoke(): void { + \load_plugin_textdomain( + $this->plugin->get_slug(), + false, + $this->plugin->header()->get( 'DomainPath' ) + ); + } +} diff --git a/src/Extension/CommonBinding/RequirementsCheck.php b/src/Extension/CommonBinding/RequirementsCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..37be9ec2817b4bd06230ecb8aa10add583098f83 --- /dev/null +++ b/src/Extension/CommonBinding/RequirementsCheck.php @@ -0,0 +1,32 @@ +<?php + +namespace WPDesk\Init\Extension\CommonBinding; + +use WPDesk\Init\Binding\StoppableBinder; +use WPDesk\Init\Configuration\Configuration; +use WPDesk\Init\Plugin\Plugin; + +class RequirementsCheck implements StoppableBinder { + + private \WPDesk_Requirement_Checker $checker; + + public function __construct( Plugin $plugin, Configuration $config ) { + $this->checker = ( new \WPDesk_Basic_Requirement_Checker_Factory( + ) )->create_from_requirement_array( + $plugin->get_basename(), + $plugin->get_name(), + $config->has( 'requirements' ) ? $config->get( 'requirements' ) : [], + $plugin->get_slug() + ); + } + + public function hooks(): void { + if ( $this->should_stop() ) { + $this->checker->render_notices(); + } + } + + public function should_stop(): bool { + return ! $this->checker->are_requirements_met(); + } +} diff --git a/src/Extension/CommonBinding/WPDeskLicenseBridge.php b/src/Extension/CommonBinding/WPDeskLicenseBridge.php new file mode 100644 index 0000000000000000000000000000000000000000..55db2c7235543e682f261db0aedea8ddd8cb8de3 --- /dev/null +++ b/src/Extension/CommonBinding/WPDeskLicenseBridge.php @@ -0,0 +1,42 @@ +<?php + +namespace WPDesk\Init\Extension\CommonBinding; + +use WPDesk\Init\Binding\Hookable; +use WPDesk\Init\Plugin\Plugin; +use WPDesk\License\PluginRegistrator; + +class WPDeskLicenseBridge implements Hookable { + + /** @var \WPDesk_Plugin_Info */ + private $plugin_info; + + private $registrator; + + public function __construct( \WPDesk_Plugin_Info $plugin_info ) { + $this->plugin_info = $plugin_info; + } + + public function hooks(): void { + $this->registrator = $this->register_plugin(); + // add_action('plugins_loaded', $this); + } + + public function __invoke(): void { + $is_plugin_subscription_active = $this->registrator instanceof PluginRegistrator && $this->registrator->is_active(); + if ( $this->plugin instanceof ActivationAware && $is_plugin_subscription_active ) { + $this->plugin->set_active(); + } + + } + + private function register_plugin() { + if ( apply_filters( 'wpdesk_can_register_plugin', true, $this->plugin_info ) ) { + $registrator = new PluginRegistrator( $this->plugin_info ); + $registrator->initialize_license_manager(); + + return $registrator; + } + } + +} diff --git a/src/Extension/CommonBinding/WPDeskTrackerBridge.php b/src/Extension/CommonBinding/WPDeskTrackerBridge.php new file mode 100644 index 0000000000000000000000000000000000000000..20660f99a7eb0e236805e60ea2c29963763c52a6 --- /dev/null +++ b/src/Extension/CommonBinding/WPDeskTrackerBridge.php @@ -0,0 +1,21 @@ +<?php + +namespace WPDesk\Init\Extension\CommonBinding; + +use WPDesk\Init\Binding\Hookable; +use WPDesk\Init\Plugin\Plugin; + +class WPDeskTrackerBridge implements Hookable { + + /** @var Plugin */ + private $plugin; + + public function __construct( Plugin $plugin ) { + $this->plugin = $plugin; + } + + public function hooks(): void { + $tracker_factory = new \WPDesk_Tracker_Factory_Prefixed(); + $tracker_factory->create_tracker( $this->plugin->get_basename() ); + } +} diff --git a/src/Extension/ConditionalExtension.php b/src/Extension/ConditionalExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..6ecf12ff371a70a8de3b2c2d2000dc80a859b497 --- /dev/null +++ b/src/Extension/ConditionalExtension.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Extension; + +use DI\Definition\Helper\AutowireDefinitionHelper; +use Monolog\Formatter\LineFormatter; +use Monolog\Logger; +use Monolog\Processor\PsrLogMessageProcessor; +use Monolog\Processor\UidProcessor; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use WPDesk\Init\Binding\Loader\ArrayDefinitions; +use WPDesk\Init\Binding\Loader\BindingDefinitions; +use WPDesk\Init\Configuration\ReadableConfig; +use WPDesk\Init\DependencyInjection\ContainerBuilder; +use WPDesk\Init\Extension\CommonBinding\RequirementsCheck; +use WPDesk\Init\Extension\CommonBinding\WPDeskLicenseBridge; +use WPDesk\Init\Extension\CommonBinding\WPDeskTrackerBridge; +use WPDesk\Init\Plugin\Plugin; +use WPDesk\Logger\WC\WooCommerceHandler; + +class ConditionalExtension implements Extension { + + public function bindings( ContainerInterface $c ): BindingDefinitions { + $bindings = []; + + if ( class_exists( \WPDesk_Basic_Requirement_Checker::class ) ) { + $bindings[] = RequirementsCheck::class; + } + + if ( class_exists( \WPDesk\License\PluginRegistrator::class ) ) { + $bindings[] = WPDeskLicenseBridge::class; + } + + if ( class_exists( \WPDesk_Tracker::class ) ) { + $bindings[] = WPDeskTrackerBridge::class; + } + + return new ArrayDefinitions( $bindings ); + } + + public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void { + $definitions = []; + + if ( class_exists( \WPDesk_Basic_Requirement_Checker::class ) ) { + $definitions[ RequirementsCheck::class ] = new AutowireDefinitionHelper(); + } + + if ( class_exists( \WPDesk\License\PluginRegistrator::class ) ) { + $definitions[ WPDeskLicenseBridge::class ] = new AutowireDefinitionHelper(); + } + + if ( class_exists( \WPDesk_Tracker::class ) ) { + $definitions[ WPDeskTrackerBridge::class ] = new AutowireDefinitionHelper(); + } + + if ( class_exists( \WPDesk\Logger\WC\WooCommerceHandler::class ) ) { + $definitions[ LoggerInterface::class ] = static function () use ( $plugin ) { + $logger = new Logger( + $plugin->get_slug(), + [], + [ new PsrLogMessageProcessor( null, true ), new UidProcessor() ] + ); + + $attach_handler = function () use ( $logger, $plugin ) { + $handler = new WooCommerceHandler( wc_get_logger(), $plugin->get_slug() ); + $handler->setFormatter( + new LineFormatter( '%channel%.%level_name% [%extra.uid%]: %message% %context% %extra%' ) + ); + $logger->pushHandler( $handler ); + }; + + if ( \function_exists( 'wc_get_logger' ) ) { + $attach_handler(); + } else { + \add_action( 'woocommerce_init', $attach_handler ); + } + + return $logger; + }; + } + + $builder->add_definitions( $definitions ); + } +} diff --git a/src/Extension/ConfigExtension.php b/src/Extension/ConfigExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..303c514b2b45b664140fd11ae754d79a2e74931d --- /dev/null +++ b/src/Extension/ConfigExtension.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Extension; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\Loader\ArrayDefinitions; +use WPDesk\Init\Binding\Loader\BindingDefinitions; +use WPDesk\Init\Binding\Loader\FilesystemDefinitions; +use WPDesk\Init\Configuration\Configuration; +use WPDesk\Init\Configuration\ReadableConfig; +use WPDesk\Init\DependencyInjection\ContainerBuilder; +use WPDesk\Init\Plugin\Plugin; +use WPDesk\Init\Util\Path; + +class ConfigExtension implements Extension { + + public function bindings( ContainerInterface $c ): BindingDefinitions { + $config = $c->get( Configuration::class ); + if ( $config->has( 'hook_resources_path' ) ) { + return new FilesystemDefinitions( + ( new Path( $config->get( 'hook_resources_path' ) ) )->absolute( $c->get( Plugin::class )->get_path() ) + ); + } + + return new ArrayDefinitions( [] ); + } + + public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void { + $services = array_map( + function ( $service ) use ( $plugin ) { + return (string) ( new Path( $service ) )->absolute( $plugin->get_path() ); + }, + (array) $config->get( 'services', [] ) + ); + + $builder->add_definitions( ...$services ); + } +} diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php new file mode 100644 index 0000000000000000000000000000000000000000..a4dace150fbf41298dd8d195e16ae7f804f7f766 --- /dev/null +++ b/src/Extension/Extension.php @@ -0,0 +1,17 @@ +<?php +declare(strict_types=1); + +namespace WPDesk\Init\Extension; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\Loader\BindingDefinitions; +use WPDesk\Init\Configuration\ReadableConfig; +use WPDesk\Init\DependencyInjection\ContainerBuilder; +use WPDesk\Init\Plugin\Plugin; + +interface Extension { + + public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void; + + public function bindings(ContainerInterface $c): BindingDefinitions; +} diff --git a/src/Extension/ExtensionsSet.php b/src/Extension/ExtensionsSet.php new file mode 100644 index 0000000000000000000000000000000000000000..650560e9e1418d636b274c24f68f02e2683e1519 --- /dev/null +++ b/src/Extension/ExtensionsSet.php @@ -0,0 +1,29 @@ +<?php + +declare( strict_types=1 ); + +namespace WPDesk\Init\Extension; + +/** + * @implements \IteratorAggregate<class-string, Extension> + */ +class ExtensionsSet implements \IteratorAggregate { + + /** @var array<class-string<Extension>, Extension> */ + private $extensions = []; + + public function __construct( Extension ...$extensions ) { + foreach ( $extensions as $extension ) { + $this->add( $extension ); + } + } + + public function add( Extension $extension ): void { + $class = \get_class( $extension ); + $this->extensions[ $class ] = $extension; + } + + public function getIterator(): \Traversable { + return new \ArrayIterator( $this->extensions ); + } +} diff --git a/src/Extension/LegacyExtension.php b/src/Extension/LegacyExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..2f8a0bb94b0842cff25e1144ef5ca36a2310d0db --- /dev/null +++ b/src/Extension/LegacyExtension.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Extension; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\Loader\BindingDefinitions; +use WPDesk\Init\Binding\Loader\EmptyDefinitions; +use WPDesk\Init\Configuration\ReadableConfig; +use WPDesk\Init\DependencyInjection\ContainerBuilder; +use WPDesk\Init\Plugin\Plugin; + +class LegacyExtension implements Extension { + + public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void { + if ( ! $config->has( 'plugin_class_name' ) ) { + throw new \LogicException( 'To use legacy driver you must set "plugin_class_name" in your config pointing to the class name of your plugin.' ); + } + + $builder->add_definitions( + [ + \WPDesk_Plugin_Info::class => $this->as_plugin_info( $plugin, $config ), + ] + ); + } + + private function as_plugin_info( Plugin $plugin, ReadableConfig $config ): \WPDesk_Plugin_Info { + $plugin_info = new \WPDesk_Plugin_Info(); + $plugin_info->set_plugin_file_name( $plugin->get_basename() ); + $plugin_info->set_plugin_name( $plugin->get_name() ); + $plugin_info->set_plugin_dir( $plugin->get_path() ); + $plugin_info->set_version( $plugin->get_version() ); + $plugin_info->set_text_domain( $plugin->get_slug() ); + $plugin_info->set_plugin_url( $plugin->get_url() ); + + $plugin_info->set_class_name( $config->get( 'plugin_class_name' ) ); + + $plugin_info->set_product_id( $config->get( 'product_id' ) ); + $plugin_info->set_plugin_shops( $config->get( 'plugin_shops' ) ); + + return $plugin_info; + } + + public function bindings( ContainerInterface $c ): BindingDefinitions { + return new EmptyDefinitions(); + } +} diff --git a/src/HookDriver/CompositeDriver.php b/src/HookDriver/CompositeDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..ea17e1fc452da40e0af7aec9757b62dfbef2f724 --- /dev/null +++ b/src/HookDriver/CompositeDriver.php @@ -0,0 +1,24 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\HookDriver; + +final class CompositeDriver implements HookDriver { + + /** @var HookDriver[] */ + private $drivers; + + public function __construct( HookDriver ...$drivers ) { + $this->drivers = $drivers; + } + + public function register_hooks(): void { + foreach ( $this->drivers as $driver ) { + $driver->register_hooks(); + } + } + + public function add( HookDriver $driver ): void { + $this->drivers[] = $driver; + } +} diff --git a/src/HookDriver/GenericDriver.php b/src/HookDriver/GenericDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..9602623755893ac92cb5e6c0ed6f0cae64311596 --- /dev/null +++ b/src/HookDriver/GenericDriver.php @@ -0,0 +1,36 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\HookDriver; + +use WPDesk\Init\Binding\Binder; +use WPDesk\Init\Binding\Loader\BindingDefinitions; + +class GenericDriver implements HookDriver { + + /** @var BindingDefinitions */ + private $definitions; + + /** @var Binder */ + private $binder; + + public function __construct( BindingDefinitions $definitions, Binder $binder ) { + $this->definitions = $definitions; + $this->binder = $binder; + } + + public function register_hooks(): void { + foreach ( $this->definitions->load() as $definition ) { + if ( $definition->hook() ) { + add_action( + $definition->hook(), + function () use ( $definition ) { + $this->binder->bind( $definition ); + } + ); + } else { + $this->binder->bind( $definition ); + } + } + } +} diff --git a/src/HookDriver/HookDriver.php b/src/HookDriver/HookDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..f65d802eb4a0851a72693e3f11561db379407a9f --- /dev/null +++ b/src/HookDriver/HookDriver.php @@ -0,0 +1,19 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\HookDriver; + +/** + * Hook can be attached to WordPress in different ways, and this + * interface decouples possible methods from our initialization system, + * to make it more flexible. + * + * Even though hook registration is sort of the main purpose of this + * library, it's better to encapsulate hook registration in a separate + * class, so that it can be easily replaced with a different + * implementation and composition. + */ +interface HookDriver { + + public function register_hooks(): void; +} diff --git a/src/HookDriver/Legacy/HookableParent.php b/src/HookDriver/Legacy/HookableParent.php new file mode 100644 index 0000000000000000000000000000000000000000..f193a49b4a177924032d8ae8c75472b24cb7de7c --- /dev/null +++ b/src/HookDriver/Legacy/HookableParent.php @@ -0,0 +1,65 @@ +<?php + +namespace WPDesk\Init\HookDriver\Legacy; + +use WPDesk\PluginBuilder\Plugin\Conditional; +use WPDesk\PluginBuilder\Plugin\Hookable; +use WPDesk\PluginBuilder\Plugin\HookablePluginDependant; + +trait HookableParent { + + /** @var HooksRegistry|null */ + private $registry; + + /** + * @param class-string<Hookable>|Hookable $hookable_object + */ + public function add_hookable( $hookable_object ) { + if ( $this->registry === null ) { + $this->registry = HooksRegistry::instance(); + } + + $this->registry->add( $hookable_object ); + } + + /** + * @param class-string<Hookable> $class_name + * + * @return false|Hookable + */ + public function get_hookable_instance_by_class_name( $class_name ) { + if ( $this->registry === null ) { + return false; + } + + foreach ( $this->registry as $hookable_object ) { + if ( $hookable_object instanceof $class_name ) { + return $hookable_object; + } + } + return false; + } + + /** + * Run hooks method on all hookable objects. + */ + protected function hooks_on_hookable_objects() { + if ( $this->registry === null ) { + return; + } + + foreach ( $this->registry as $hookable_object ) { + if ( + $hookable_object instanceof Conditional && + ! $hookable_object::is_needed() + ) { + continue; + } + + if ( $hookable_object instanceof HookablePluginDependant ) { + $hookable_object->set_plugin( $this ); + } + $hookable_object->hooks(); + } + } +} diff --git a/src/HookDriver/Legacy/HooksRegistry.php b/src/HookDriver/Legacy/HooksRegistry.php new file mode 100644 index 0000000000000000000000000000000000000000..1ef8730d9370d3d82d5fd34cbf74f435b7c8db47 --- /dev/null +++ b/src/HookDriver/Legacy/HooksRegistry.php @@ -0,0 +1,53 @@ +<?php + +namespace WPDesk\Init\HookDriver\Legacy; + +use Psr\Container\ContainerInterface; +use Traversable; +use WPDesk\PluginBuilder\Plugin\Hookable; + +/** + * @implements IteratorAggregate<int,Hookable> + */ +final class HooksRegistry implements \IteratorAggregate { + + private static $instance; + + /** @var array<class-string<Hookable>|Hookable> */ + private $callbacks = []; + + private $container; + + private function __construct() {} + + public function inject_container( ContainerInterface $c ) { + $this->container = $c; + } + + public static function instance(): HooksRegistry { + if ( self::$instance === null ) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function getIterator(): Traversable { + return new \ArrayIterator( + array_map( + function ( $hookable ) { + if ( is_string( $hookable ) ) { + return $this->container->get( $hookable ); + } + + return $hookable; + }, + $this->callbacks + ) + ); + } + + public function add( $hookable ) { + $this->callbacks[] = $hookable; + } +} diff --git a/src/HookDriver/LegacyDriver.php b/src/HookDriver/LegacyDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..ff0e7e36657bcfe16da8d6fc3329d6897d8e4570 --- /dev/null +++ b/src/HookDriver/LegacyDriver.php @@ -0,0 +1,29 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\HookDriver; + +use Psr\Container\ContainerInterface; +use WPDesk\Init\HookDriver\Legacy\HooksRegistry; + +final class LegacyDriver implements HookDriver { + + /** @var ContainerInterface */ + private $container; + + public function __construct( ContainerInterface $container ) { + if ( ! class_exists( \WPDesk_Plugin_Info::class ) ) { + throw new \LogicException( 'Legacy driver cannot be used as the plugin builder component is unavailable. Try running "composer require wpdesk/wp-builder".' ); + } + $this->container = $container; + } + + public function register_hooks(): void { + HooksRegistry::instance()->inject_container( $this->container ); + + $info = $this->container->get( \WPDesk_Plugin_Info::class ); + $class_name = $info->get_class_name(); + $p = new $class_name( $info ); + add_action( 'plugins_loaded', [ $p, 'init' ], -50 ); + } +} diff --git a/src/HookProvider/AbstractHookProvider.php b/src/HookProvider/AbstractHookProvider.php deleted file mode 100644 index 2f87292718077dc336d7297a5c14f0bad70e8211..0000000000000000000000000000000000000000 --- a/src/HookProvider/AbstractHookProvider.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -use WPDesk\Init\HooksProvider; -use WPDesk\Init\PluginAwareInterface; -use WPDesk\Init\PluginAwareTrait; - -abstract class AbstractHookProvider implements HooksProvider, PluginAwareInterface { - use PluginAwareTrait; - -} \ No newline at end of file diff --git a/src/HookProvider/ActivationDate.php b/src/HookProvider/ActivationDate.php deleted file mode 100644 index 49772bb9570c7968e0ac1376b33354ae7f4f88e6..0000000000000000000000000000000000000000 --- a/src/HookProvider/ActivationDate.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -use WPDesk\Init\HooksProvider; -use WPDesk\Init\PluginAwareInterface; -use WPDesk\Init\PluginAwareTrait; - -class ActivationDate implements HooksProvider, PluginAwareInterface { - use PluginAwareTrait; - - public function register_hooks(): void { - add_action( - 'activated_plugin', - function ( $plugin_file, $network_wide = false ) { - if ( ! $network_wide && $this->plugin->get_basename() === $plugin_file ) { - $option_name = 'plugin_activation_' . $plugin_file; - $activation_date = get_option( $option_name, '' ); - if ( '' === $activation_date ) { - $activation_date = current_time( 'mysql' ); - update_option( $option_name, $activation_date ); - } - } - } - ); - - } - -} diff --git a/src/HookProvider/ActivationHook.php b/src/HookProvider/ActivationHook.php deleted file mode 100644 index 290b9808d4bb489227e4ee1f0584e98f5d2bb9c1..0000000000000000000000000000000000000000 --- a/src/HookProvider/ActivationHook.php +++ /dev/null @@ -1,16 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -abstract class ActivationHook extends AbstractHookProvider { - - public function register_hooks(): void { - register_activation_hook( - $this->plugin->get_basename(), - [$this, 'activate'] - ); - } - - abstract public function activate(): void; -} \ No newline at end of file diff --git a/src/HookProvider/ContainerHookProvider.php b/src/HookProvider/ContainerHookProvider.php deleted file mode 100644 index 2a6681e277f6fd9feb8b41dae367f229dcbfd0e8..0000000000000000000000000000000000000000 --- a/src/HookProvider/ContainerHookProvider.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -use WPDesk\Init\ContainerAwareInterface; -use WPDesk\Init\ContainerAwareTrait; -use WPDesk\Init\HooksProvider; -use WPDesk\Init\PluginAwareInterface; -use WPDesk\Init\PluginAwareTrait; - -class ContainerHookProvider implements HooksProvider, PluginAwareInterface, ContainerAwareInterface { - use PluginAwareTrait; - use ContainerAwareTrait; - - public function register_hooks(): void { - add_action( - 'plugins_loaded', - function () { - if ( $this->container->has( 'hooks' ) ) { - foreach ( $this->container->get( 'hooks' ) as $hook_provider ) { - $this->plugin->register_hooks( $hook_provider ); - } - } - } - ); - } - -} diff --git a/src/HookProvider/DeactivationHook.php b/src/HookProvider/DeactivationHook.php deleted file mode 100644 index 7a21a84c1e06ab08bfc0de184ba782bf524c38dd..0000000000000000000000000000000000000000 --- a/src/HookProvider/DeactivationHook.php +++ /dev/null @@ -1,16 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -abstract class DeactivationHook extends AbstractHookProvider { - - public function register_hooks(): void { - register_deactivation_hook( - $this->plugin->get_basename(), - [$this, 'deactivate'] - ); - } - - abstract public function deactivate(): void; -} \ No newline at end of file diff --git a/src/HookProvider/I18n.php b/src/HookProvider/I18n.php deleted file mode 100644 index 7781e13c16223612722f58c0f7948addb36395e2..0000000000000000000000000000000000000000 --- a/src/HookProvider/I18n.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -use WPDesk\Init\HooksProvider; -use WPDesk\Init\PluginAwareInterface; -use WPDesk\Init\PluginAwareTrait; - -class I18n implements HooksProvider, PluginAwareInterface { - use PluginAwareTrait; - - public function register_hooks(): void { - if ( did_action( 'plugins_loaded' ) ) { - $this->load_textdomain(); - } else { - add_action( 'plugins_loaded', [ $this, 'load_textdomain' ] ); - } - } - - public function load_textdomain(): void { - $plugin_rel_path = dirname( $this->plugin->get_basename() ) . '/lang'; - \load_plugin_textdomain( $this->plugin->get_slug(), false, $plugin_rel_path ); - } -} diff --git a/src/HookProvider/WooCommerceHPOSCompatibility.php b/src/HookProvider/WooCommerceHPOSCompatibility.php deleted file mode 100644 index 33e442706214a1020e3dedeed67997949ebe7d72..0000000000000000000000000000000000000000 --- a/src/HookProvider/WooCommerceHPOSCompatibility.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -declare( strict_types=1 ); - -namespace WPDesk\Init\HookProvider; - -use WPDesk\Init\HooksProvider; -use WPDesk\Init\PluginAwareInterface; -use WPDesk\Init\PluginAwareTrait; - -class WooCommerceHPOSCompatibility implements HooksProvider, PluginAwareInterface { - use PluginAwareTrait; - - public function register_hooks(): void { - add_action( - 'before_woocommerce_init', - function () { - $features_util_class = '\\' . 'Automattic' . '\\' . 'WooCommerce' . '\\' . 'Utilities' . '\\' . 'FeaturesUtil'; - if ( class_exists( $features_util_class ) ) { - $features_util_class::declare_compatibility( 'custom_order_tables', $this->plugin->get_basename(), true ); - } - } - ); - } -} diff --git a/src/HooksProvider.php b/src/HooksProvider.php deleted file mode 100644 index 2266e86fb7ad2807a21d6cd8502e473f24dfeb41..0000000000000000000000000000000000000000 --- a/src/HooksProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init; - -/** - * Hook is a special kind of service which lives only to integrate with WordPress lifecycle - * system. By design, hook providers should be lightweight classes which focus its main logic on - * integration with actions and filters. - * - * @author Brady Vercher - */ -interface HooksProvider { - - /** - * Register hooks for the plugin. - */ - public function register_hooks(): void; - -} \ No newline at end of file diff --git a/src/HooksTrait.php b/src/HooksTrait.php deleted file mode 100644 index 2318b0bf554569479dcb147433e0ee5a08e3e6fa..0000000000000000000000000000000000000000 --- a/src/HooksTrait.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init; - -/** - * Hooks trait. - * - * Allows protected and private methods to be used as hook callbacks in PHP <8.1. Since PHP 8.1 - * you are able to take advantage of first class callable and register private methods in hooks - * without any workarounds. - * - * @author John P. Bloch - * @link https://github.com/johnpbloch/wordpress-dev/blob/master/src/Hooks.php - */ -trait HooksTrait { - - /** - * Add a WordPress filter. - * - * @param callable $method - * - * @return true - */ - protected function add_filter( - string $hook, - $method, - int $priority = 10, - int $arg_count = 1 - ): bool { - return add_filter( - $hook, - $this->map_filter( $method, $arg_count ), - $priority, - $arg_count - ); - } - - /** - * Add a WordPress action. - * - * This is an alias of add_filter(). - * - * @param callable $method - * - * @return true - */ - protected function add_action( string $hook, $method, int $priority = 10, int $arg_count = 1 ): bool { - return $this->add_filter( $hook, $method, $priority, $arg_count ); - } - - /** - * Remove a WordPress filter. - * - * @param callable $method - * - * @return bool Whether the function existed before it was removed. - */ - protected function remove_filter( - string $hook, - $method, - int $priority = 10, - int $arg_count = 1 - ): bool { - return remove_filter( - $hook, - $this->map_filter( $method, $arg_count ), - $priority - ); - } - - /** - * Remove a WordPress action. - * - * This is an alias of remove_filter(). - * - * @param callable $method - * - * @return bool Whether the function is removed. - */ - protected function remove_action( - string $hook, - $method, - int $priority = 10, - int $arg_count = 1 - ): bool { - return $this->remove_filter( $hook, $method, $priority, $arg_count ); - } - - /** - * Map a filter to a closure that inherits the class' internal scope. - * - * This allows hooks to use protected and private methods. - * - * @param string $callable - * @param int $arg_count - * - * @return \Closure The callable actually attached to a WP hook - */ - private function map_filter( $callable, int $arg_count ): \Closure { - if ( is_string( $callable ) && method_exists( $this, $callable ) ) { - $object = $this; - $method = $callable; - } - - if ( is_array( $callable ) ) { - [ $object, $method ] = $callable; - } - - return static function () use ( $object, $method, $arg_count ) { - return $object->{$method}( ...array_slice( func_get_args(), 0, $arg_count ) ); - }; - } -} diff --git a/src/Init.php b/src/Init.php new file mode 100644 index 0000000000000000000000000000000000000000..7d8e37e7bdf59de3b0333988beae7df3e0fa0e97 --- /dev/null +++ b/src/Init.php @@ -0,0 +1,78 @@ +<?php +/** + * This file have to be compatible with PHP >=7.0 to gracefully handle outdated client's websites. + */ + +namespace WPDesk\Init; + +use WPDesk\Init\Extension\LegacyExtension; +use WPDesk\Init\Extension\BuiltinExtension; +use WPDesk\Init\Extension\ConfigExtension; +use WPDesk\Init\Extension\ExtensionsSet; +use WPDesk\Init\Util\PhpFileLoader; +use WPDesk\Init\Configuration\Configuration; +use WPDesk\Init\Extension\ConditionalExtension; + +final class Init { + + private static $bootable = true; + + /** @var Configuration */ + private $config; + + /** + * @param string|array|Configuration $config + */ + public static function setup( $config ) { + $result = require __DIR__ . '/platform_check.php'; + + if ( $result === false ) { + self::$bootable = false; + } + + return new self( $config ); + } + + /** + * @param string|array|Configuration $config + */ + public function __construct( $config ) { + if ( $config instanceof Configuration ) { + $this->config = $config; + } elseif ( \is_array( $config ) ) { + $this->config = new Configuration( $config ); + } elseif ( \is_string( $config ) ) { + $loader = new PhpFileLoader(); + $this->config = new Configuration( $loader->load( $config ) ); + } else { + throw new \InvalidArgumentException( sprintf( 'Configuration must be either path to configuration file, array of configuration data or %s instance', Configuration::class ) ); + } + } + + /** + * @param string|null $filename Filename of the booted plugin. May be null, if called from plugin's main file. + */ + public function boot( ?string $filename = null ) { + if ( self::$bootable === false ) { + return; + } + + if ( $filename === null ) { + $backtrace = \debug_backtrace( \DEBUG_BACKTRACE_IGNORE_ARGS, 1 ); + $filename = $backtrace[0]['file']; + } + + $extensions = new ExtensionsSet( + new BuiltinExtension(), + new ConfigExtension(), + new ConditionalExtension() + ); + + if ( class_exists( \WPDesk_Plugin_Info::class ) ) { + $extensions->add( new LegacyExtension() ); + } + + $kernel = new Kernel( $filename, $this->config, $extensions ); + $kernel->boot(); + } +} diff --git a/src/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000000000000000000000000000000000000..7e9ed9ba405ce22d92a7c29463a38f18fba58248 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,137 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init; + +use DI\Container; +use DI\ContainerBuilder as DiBuilder; +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\Binder\CallableBinder; +use WPDesk\Init\Binding\Binder\CompositeBinder; +use WPDesk\Init\Binding\Binder\HookableBinder; +use WPDesk\Init\Binding\Binder\StoppableBinder; +use WPDesk\Init\Binding\Loader\CompositeBindingLoader; +use WPDesk\Init\Configuration\Configuration; +use WPDesk\Init\DependencyInjection\ContainerBuilder; +use WPDesk\Init\Extension\ExtensionsSet; +use WPDesk\Init\HookDriver\CompositeDriver; +use WPDesk\Init\HookDriver\GenericDriver; +use WPDesk\Init\HookDriver\HookDriver; +use WPDesk\Init\HookDriver\LegacyDriver; +use WPDesk\Init\Util\PhpFileLoader; +use WPDesk\Init\Plugin\Header; +use WPDesk\Init\Util\Path; +use WPDesk\Init\Plugin\DefaultHeaderParser; +use WPDesk\Init\Plugin\HeaderParser; +use WPDesk\Init\Plugin\Plugin; + +final class Kernel { + + /** @var string|null Plugin filename. */ + private $filename; + + /** @var Configuration */ + private $config; + + /** @var PhpFileLoader */ + private $loader; + + /** @var HeaderParser */ + private $parser; + + /** @var ExtensionsSet */ + private $extensions; + + public function __construct( + string $filename, + Configuration $config, + ExtensionsSet $extensions + ) { + $this->filename = $filename; + $this->config = $config; + $this->extensions = $extensions; + $this->loader = new PhpFileLoader(); + $this->parser = new DefaultHeaderParser(); + } + + public function boot(): void { + $cache_path = $this->get_cache_path( 'plugin.php' ); + try { + $plugin_data = $this->loader->load( $cache_path ); + } catch ( \Exception $e ) { + // If cache not found, load data from memory. + // Avoid writing files on host environment. + // Generate cache with command instead. + $plugin_data = $this->parser->parse( $this->filename ); + } + + $plugin = new Plugin( $this->filename, new Header( $plugin_data ) ); + + $container = $this->initialize_container( $plugin ); + $container->set( Plugin::class, $plugin ); + $container->set( Configuration::class, $this->config ); + + $this->prepare_driver( $container )->register_hooks(); + } + + private function get_cache_path( string $path = '' ): string { + return (string) ( new Path( $this->config->get( 'cache_path', 'generated' ) ) )->join( $path )->absolute( + rtrim( plugin_dir_path( $this->filename ), '/' ) . '/' + ); + } + + private function get_container_name( Plugin $plugin ): string { + return str_replace( '-', '_', $plugin->get_slug() ) . '_container'; + } + + private function initialize_container( Plugin $plugin ): Container { + $original_builder = new DiBuilder(); + + // If there's a cache file, use it as we are in production environment. + // Otherwise, build the container from scratch and use live version, without compilation. + if ( file_exists( $this->get_cache_path( $this->get_container_name( $plugin ) . '.php' ) ) ) { + $original_builder->enableCompilation( + $this->get_cache_path(), + $this->get_container_name( $plugin ) + ); + return $original_builder->build(); + } + + $builder = new ContainerBuilder( $original_builder ); + + foreach ( $this->extensions as $extension ) { + $extension->build( $builder, $plugin, $this->config ); + } + + return $builder->build(); + } + + private function prepare_driver( ContainerInterface $container ): HookDriver { + $loader = new CompositeBindingLoader(); + foreach ($this->extensions as $extension) { + foreach ($extension->bindings($container) as $bindings) { + $loader->add($bindings); + } + } + + $driver = new GenericDriver( + $loader, + new StoppableBinder( + new CompositeBinder( + new HookableBinder( $container ), + new CallableBinder( $container ) + ), + $container + ) + ); + + if ( class_exists( \WPDesk_Plugin_Info::class ) ) { + $driver = new CompositeDriver( + $driver, + new LegacyDriver( $container ) + ); + } + + return $driver; + } +} diff --git a/src/Plugin.php b/src/Plugin.php deleted file mode 100644 index 3e070ba42ee6ad982859ab7d5151b5633748caec..0000000000000000000000000000000000000000 --- a/src/Plugin.php +++ /dev/null @@ -1,219 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init; - -final class Plugin implements ContainerAwareInterface { - use ContainerAwareTrait; - - /** - * Plugin basename. - * - * Ex: plugin-name/plugin-name.php - * - * @var string - */ - protected $basename; - - /** - * Absolute path to the main plugin directory. - * - * @var string - */ - protected $directory; - - /** - * Plugin name to display. - * - * @var string - */ - private $name; - - /** - * Absolute path to the main plugin file. - * - * @var string - */ - protected $file; - - /** - * Plugin identifier. - * - * @var string - */ - protected $slug; - - /** - * URL to the main plugin directory. - * - * @var string - */ - protected $url; - - /** - * Retrieve the absolute path for the main plugin file. - * - * @return string - */ - public function get_basename(): string { - return $this->basename; - } - - /** - * Set the plugin basename. - * - * @param string $basename Relative path from the main plugin directory. - * - * @return $this - */ - public function set_basename( string $basename ): self { - $this->basename = $basename; - - return $this; - } - - /** - * Retrieve the plugin directory. - * - * @return string - */ - public function get_directory(): string { - return $this->directory; - } - - /** - * Set the plugin's directory. - * - * @param string $directory Absolute path to the main plugin directory. - * - * @return $this - */ - public function set_directory( string $directory ): self { - $this->directory = rtrim( $directory, '/' ) . '/'; - - return $this; - } - - /** - * Retrieve the path to a file in the plugin. - * - * @param string $path Optional. Path relative to the plugin root. - * - * @return string - */ - public function get_path( string $path = '' ): string { - return $this->directory . ltrim( $path, '/' ); - } - - /** - * Retrieve the absolute path for the main plugin file. - * - * @return string - */ - public function get_file(): string { - return $this->file; - } - - /** - * Set the path to the main plugin file. - * - * @param string $file Absolute path to the main plugin file. - * - * @return $this - */ - public function set_file( string $file ): self { - $this->file = $file; - - return $this; - } - - /** - * @param string $name - * - * @return self - */ - public function set_name( string $name ): self { - $this->name = $name; - - return $this; - } - - /** - * @return string - */ - public function get_name(): string { - return $this->name ?? $this->get_slug(); - } - - /** - * Retrieve the plugin identifier. - * - * @return string - */ - public function get_slug(): string { - return $this->slug; - } - - /** - * Set the plugin identifier. - * - * @param string $slug Plugin identifier. - * - * @return $this - */ - public function set_slug( string $slug ) { - $this->slug = $slug; - - return $this; - } - - /** - * Retrieve the URL for a file in the plugin. - * - * @param string $path Optional. Path relative to the plugin root. - * - * @return string - */ - public function get_url( string $path = '' ) { - return $this->url . ltrim( $path, '/' ); - } - - /** - * Set the URL for plugin directory root. - * - * @param string $url URL to the root of the plugin directory. - * - * @return $this - */ - public function set_url( string $url ) { - $this->url = rtrim( $url, '/' ) . '/'; - - return $this; - } - - /** - * Register hooks for the plugin. - * - * @param HooksProvider ...$providers - * - * @return void - */ - public function register_hooks( HooksProvider ...$providers ): void { - foreach ( $providers as $provider ) { - if ( $provider instanceof PluginAwareInterface ) { - $provider->set_plugin( $this ); - } - - if ( $provider instanceof ContainerAwareInterface ) { - $provider->set_container( $this->container ); - } - - if ( $provider instanceof Conditional && ! $provider->is_needed() ) { - continue; - } - - $provider->register_hooks(); - } - } - -} diff --git a/src/Plugin/DefaultHeaderParser.php b/src/Plugin/DefaultHeaderParser.php new file mode 100644 index 0000000000000000000000000000000000000000..7cc48dcb15a4b11c256e2f4d4794601a75b8c708 --- /dev/null +++ b/src/Plugin/DefaultHeaderParser.php @@ -0,0 +1,142 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Plugin; + +class DefaultHeaderParser implements HeaderParser { + + private const KB_IN_BYTES = 1024; + private const HEADERS = [ + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresWC' => 'WC requires at least', + 'RequiresPHP' => 'Requires PHP', + 'TestedWP' => 'Tested up to', + 'TestedWC' => 'WC tested up to', + 'UpdateURI' => 'Update URI', + 'RequiresPlugins' => 'Requires Plugins', + ]; + + /** + * Parses the plugin contents to retrieve plugin's metadata. + * All plugin headers must be on their own line. Plugin description must not have + * any newlines, otherwise only parts of the description will be displayed. + * The below is formatted for printing. + * /* + * Plugin Name: Name of the plugin. + * Plugin URI: The home page of the plugin. + * Author: Plugin author's name. + * Author URI: Link to the author's website. + * Version: Plugin version. + * Text Domain: Optional. Unique identifier, should be same as the one used in + * load_plugin_textdomain(). + * Domain Path: Optional. Only useful if the translations are located in a + * folder above the plugin's base path. For example, if .mo files are + * located in the locale folder then Domain Path will be "/locale/" and + * must have the first slash. Defaults to the base folder the plugin is + * located in. + * Network: Optional. Specify "Network: true" to require that a plugin is activated + * across all sites in an installation. This will prevent a plugin from being + * activated on a single site when Multisite is enabled. + * Requires at least: Optional. Specify the minimum required WordPress version. + * Requires PHP: Optional. Specify the minimum required PHP version. + * * / # Remove the space to close comment. + * The first 8 KB of the file will be pulled in and if the plugin data is not + * within that first 8 KB, then the plugin author should correct their plugin + * and move the plugin data headers to the top. + * The plugin file is assumed to have permissions to allow for scripts to read + * the file. This is not checked however and the file is only opened for + * reading. + * + * @param string $plugin_file Absolute path to the main plugin file. + * + * @return array{ + * Name: string, + * PluginURI?: string, + * Version?: string, + * Author?: string, + * AuthorURI?: string, + * TextDomain?: string, + * DomainPath?: string, + * Network?: bool, + * RequiresWP?: string, + * RequiresWC?: string, + * RequiresPHP?: string, + * TestedWP?: string, + * TestedWC?: string, + * UpdateURI?: string, + * } + */ + public function parse( string $plugin_file ): array { + + $plugin_data = $this->get_file_data( $plugin_file, self::HEADERS ); + + if ( isset( $plugin_data['Network'] ) ) { + $plugin_data['Network'] = filter_var( $plugin_data['Network'], \FILTER_VALIDATE_BOOLEAN ); + } + + // If no text domain is defined fall back to the plugin slug. + if ( empty( $plugin_data['TextDomain'] ) ) { + $plugin_slug = dirname( $plugin_file ); + if ( '.' !== $plugin_slug && false === strpos( $plugin_slug, '/' ) ) { + $plugin_data['TextDomain'] = $plugin_slug; + } + } + + return $plugin_data; + } + + /** + * Retrieves metadata from a file. + * Searches for metadata in the first 8 KB of a file, such as a plugin or theme. + * Each piece of metadata must be on its own line. Fields can not span multiple + * lines, the value will get cut at the end of the first line. + * If the file data is not within that first 8 KB, then the author should correct + * their plugin file and move the data headers to the top. + * + * @link https://codex.wordpress.org/File_Header + * @since 2.9.0 + * + * @param string $file Absolute path to the file. + * @param array $default_headers List of headers, in the format `array( 'HeaderKey' => 'Header + * Name' )`. + * + * @return string[] Array of file header values keyed by header name. + */ + private function get_file_data( string $file, array $default_headers ): array { + // Pull only the first 8 KB of the file in. + $file_data = file_get_contents( $file, false, null, 0, 8 * self::KB_IN_BYTES ); + + if ( false === $file_data ) { + $file_data = ''; + } + + // Make sure we catch CR-only line endings. + $file_data = \str_replace( "\r", "\n", $file_data ); + + $headers = []; + foreach ( $default_headers as $field => $regex ) { + if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) { + $headers[ $field ] = $this->_cleanup_header_comment( $match[1] ); + } + } + + return $headers; + } + + /** + * Strips close comment and close php tags from file headers used by WP. + * + * @see https://core.trac.wordpress.org/ticket/8497 + */ + private function _cleanup_header_comment( string $str ): string { + return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) ); + } +} diff --git a/src/Plugin/Header.php b/src/Plugin/Header.php new file mode 100644 index 0000000000000000000000000000000000000000..97beba37909f75e3e31baafcb886fe1bf8945a9c --- /dev/null +++ b/src/Plugin/Header.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Plugin; + +final class Header implements \ArrayAccess { + + /** @var array */ + private $header_data; + + public function __construct( array $header_data ) { + $this->header_data = $header_data; + } + + public function offsetExists( $offset ): bool { + if ( ! is_string( $offset ) ) { + throw new \InvalidArgumentException( 'Header key must be a string' ); + } + + return $this->has( $offset ); + } + + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + if ( ! is_string( $offset ) ) { + throw new \InvalidArgumentException( 'Header key must be a string' ); + } + + return $this->get( $offset ); + } + + public function offsetSet( $offset, $value ): void { + throw new \BadMethodCallException( 'Header cannot be modified' ); + } + + public function offsetUnset( $offset ): void { + throw new \BadMethodCallException( 'Header cannot be modified' ); + } + + public function get( string $key ) { + return $this->header_data[ $key ]; + } + + public function has( string $key ): bool { + return isset( $this->header_data[ $key ] ); + } +} diff --git a/src/Plugin/HeaderParser.php b/src/Plugin/HeaderParser.php new file mode 100644 index 0000000000000000000000000000000000000000..6b265046b6ab6dfb8e82d1e1b57d17ea297faa20 --- /dev/null +++ b/src/Plugin/HeaderParser.php @@ -0,0 +1,8 @@ +<?php + +namespace WPDesk\Init\Plugin; + +interface HeaderParser { + + public function parse( string $plugin_file ): array; +} diff --git a/src/Plugin/Plugin.php b/src/Plugin/Plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..5fd3c2a450b8d242c7ec154e45878427f05ba8e3 --- /dev/null +++ b/src/Plugin/Plugin.php @@ -0,0 +1,125 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Plugin; + +final class Plugin { + + /** + * Plugin basename. + * + * Ex: plugin-name/plugin-name.php + * + * @var string + */ + private $basename; + + /** + * Absolute path to the main plugin directory. + * + * @var string + */ + private $directory; + + /** + * Plugin name to display. + * + * @var string + */ + private $name; + + /** + * Absolute path to the main plugin file. + * + * @var string + */ + private $file; + + /** + * Plugin identifier. + * + * @var string + */ + private $slug; + + /** + * URL to the main plugin directory. + * + * @var string + */ + private $url; + + /** + * Plugin version string. + * + * @var string + */ + private $version; + + /** + * @var Header + */ + private $header; + + public function __construct( string $file, Header $header ) { + $this->file = $file; + $this->name = $header['Name']; + $this->version = $header['Version'] ?? '0.0.0'; + $this->basename = plugin_basename( $file ); + $this->directory = rtrim( plugin_dir_path( $file ), '/' ) . '/'; + $this->url = rtrim( plugin_dir_url( $file ), '/' ) . '/'; + $this->slug = $header['TextDomain'] ?? basename( $this->directory ); + $this->header = $header; + } + + /** + * Retrieve the absolute path for the main plugin file. + */ + public function get_basename(): string { + return $this->basename; + } + + /** + * Retrieve the path to a file in the plugin. + * + * @param string $path Optional. Path relative to the plugin root. + */ + public function get_path( string $path = '' ): string { + return $this->directory . ltrim( $path, '/' ); + } + + /** + * Retrieve the absolute path for the main plugin file. + */ + public function get_file(): string { + return $this->file; + } + + public function get_name(): string { + return $this->name; + } + + /** + * Retrieve the plugin identifier. + */ + public function get_slug(): string { + return $this->slug; + } + + /** + * Retrieve the URL for a file in the plugin. + * + * @param string $path Optional. Path relative to the plugin root. + */ + public function get_url( string $path = '' ): string { + return $this->url . ltrim( $path, '/' ); + } + + public function get_version(): string { + return $this->version; + } + + public function header(): Header { + return $this->header; + } +} diff --git a/src/PluginAwareInterface.php b/src/PluginAwareInterface.php deleted file mode 100644 index 4b6c9895cd05ea41191a119bbc6a5c5431424b0b..0000000000000000000000000000000000000000 --- a/src/PluginAwareInterface.php +++ /dev/null @@ -1,16 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init; - -/** - * Plugin aware interface. - * - * @author Brady Vercher - */ -interface PluginAwareInterface { - /** - * Set the main plugin instance. - */ - public function set_plugin( Plugin $plugin ): void; -} \ No newline at end of file diff --git a/src/PluginAwareTrait.php b/src/PluginAwareTrait.php deleted file mode 100644 index 558688066e2bdc13ca8aa07b0b848613346d459d..0000000000000000000000000000000000000000 --- a/src/PluginAwareTrait.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init; - -/** - * @author Brady Vercher - */ -trait PluginAwareTrait { - /** @var Plugin */ - protected $plugin; - - /** - * Set the main plugin instance. - */ - public function set_plugin( Plugin $plugin ): void { - $this->plugin = $plugin; - } -} \ No newline at end of file diff --git a/src/PluginInit.php b/src/PluginInit.php deleted file mode 100644 index ed6f2fa6d5e18b23d5a54d43be7b6ac190130f3e..0000000000000000000000000000000000000000 --- a/src/PluginInit.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init; - -use DI\ContainerBuilder; -use DI\Definition\Source\DefinitionSource; -use Psr\Container\ContainerInterface; - -/** - * Plugin builder class responsible for our initialization system. - */ -final class PluginInit { - - /** @var array */ - private $requirements = []; - - /** @var array Dependency injection container definitions */ - private $definitions = []; - - /** @var string|null Plugin filename. */ - private $filename; - - /** @var string|null Plugin identifier. */ - private $slug; - - /** - * Define environment constraints required by a plugin. If requirements are not fulfilled, - * the plugin will not be able to instantiate. - * - * @param array $requirements Validation rules are defined according to `wp-basic-requirements` - * documentation. - * - * @see https://gitlab.wpdesk.dev/wpdesk/wp-basic-requirements - */ - public function set_requirements( array $requirements ): self { - $this->requirements = $requirements; - - return $this; - } - - /** - * Define plugin slug. This value will be used as main plugin identifier and as translation - * text domain. By default, plugin slug is set to plugin directory name. - */ - public function set_slug( string $slug ): self { - $this->slug = $slug; - - return $this; - } - - /** - * Explicitly set name of main plugin file used for reference. This value should be set with - * caution, as filename is the most important identifier, which allows us to retreive further - * plugin data, such as basename, plugin dir and url. By default, this value is set to - * original caller's file name. - */ - public function set_filename( string $filename ): self { - $this->filename = $filename; - - return $this; - } - - /** - * Add definition sources to use in dependency injection container. - * If plugin doesn't receive any definitions, then container build is skipped. - * - * @see https://php-di.org/doc/container-configuration.html - * - * @param array|DefinitionSource|string $definitions - */ - public function add_container_definitions( ...$definitions ): self { - $this->definitions = array_merge( $this->definitions, $definitions ); - - return $this; - } - - /** - * Build and return a plugin. - * - * @return Plugin|null If plugin failed to build (e.g. requirements are not fulfilled), - * initialization process returns null. There are no exceptions thrown on foreseeable issues - * as those cases should be handled gracefully, by displaying admin notice if possible and - * preventing to initialize plugin functions without disrupting a website. - */ - public function init(): ?Plugin { - if ( empty( $this->filename ) ) { - $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 ); - $this->filename = $backtrace[0]['file']; - } - - $plugin = $this->create_plugin(); - - if ( - ! empty( $this->requirements ) && - class_exists( \WPDesk_Basic_Requirement_Checker_Factory::class ) && - ! $this->requirements_met( $plugin ) - ) { - return null; - } - - if ( ! empty( $this->definitions ) && class_exists( ContainerBuilder::class ) ) { - $plugin->set_container( $this->build_container( $plugin ) ); - } - - return $this->register_default_providers( $plugin ); - } - - private function build_container( Plugin $plugin ): ContainerInterface { - $builder = new ContainerBuilder(); - - if ( ! getenv( 'WPDESK_DEVELOPMENT' ) ) { - $builder->enableCompilation( - $plugin->get_path( '/cache' ), - str_replace( '-', '_', $plugin->get_slug() ) . '_container' - ); - } - - $builder->addDefinitions( ...$this->definitions ); - - return $builder->build(); - } - - private function register_default_providers( Plugin $plugin ): Plugin { - $plugin->register_hooks( - new HookProvider\I18n(), - new HookProvider\WooCommerceHPOSCompatibility(), - new HookProvider\ActivationDate() - ); - - if ( ! empty( $this->definitions ) ) { - $plugin->register_hooks( new HookProvider\ContainerHookProvider() ); - } - - return $plugin; - } - - - private function create_plugin(): Plugin { - $plugin = new Plugin(); - $plugin->set_file( $this->filename ) - ->set_basename( plugin_basename( $this->filename ) ) - ->set_directory( plugin_dir_path( $this->filename ) ) - ->set_url( plugin_dir_url( $this->filename ) ) - ->set_slug( $this->slug ?? basename( $plugin->get_directory() ) ); - - return $plugin; - } - - /** - * @param Plugin $plugin - * - * @return bool - */ - private function requirements_met( Plugin $plugin ): bool { - $requirements_factory = new \WPDesk_Basic_Requirement_Checker_Factory(); - $requirements = $requirements_factory->create_from_requirement_array( - $plugin->get_basename(), - $plugin->get_name(), - $this->requirements, - $plugin->get_slug() - ); - - if ( ! $requirements->are_requirements_met() ) { - $requirements->render_notices(); - - return false; - } - - return true; - } - -} diff --git a/src/Resources/bindings/index.php b/src/Resources/bindings/index.php new file mode 100644 index 0000000000000000000000000000000000000000..511616180dc2e65c555f55efd70441aa6c036ad3 --- /dev/null +++ b/src/Resources/bindings/index.php @@ -0,0 +1,9 @@ +<?php + +use WPDesk\Init\Extension\CommonBinding\I18n; +use WPDesk\Init\Extension\CommonBinding\CustomOrderTableCompatibility; + +return [ + I18n::class, + CustomOrderTableCompatibility::class, +]; diff --git a/src/Resources/services.inc.php b/src/Resources/services.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..276657c7c550d1ac829ffb9cebc78604a2604b55 --- /dev/null +++ b/src/Resources/services.inc.php @@ -0,0 +1,16 @@ +<?php + +use DI\Definition\Helper\AutowireDefinitionHelper; +use WPDesk\Init\Extension\CommonBinding\CustomOrdersTableCompatibility; +use WPDesk\Init\Extension\CommonBinding\I18n; + +return [ + wpdb::class => static function () { + global $wpdb; + + return $wpdb; + }, + + I18n::class => new AutowireDefinitionHelper(), + CustomOrdersTableCompatibility::class => new AutowireDefinitionHelper(), +]; diff --git a/src/Util/Path.php b/src/Util/Path.php new file mode 100644 index 0000000000000000000000000000000000000000..a7cf66fde392accc8bc0ec0cdd45eaf12e6b1859 --- /dev/null +++ b/src/Util/Path.php @@ -0,0 +1,97 @@ +<?php + +declare(strict_types=1); + +namespace WPDesk\Init\Util; + +final class Path { + + /** @var string */ + private $path; + + public function __construct( string $path ) { + $this->path = $path; + } + + public function canonical(): self { + $root = str_starts_with( $this->path, '/' ) ? '/' : ''; + return new self( $root . implode( '/', $this->find_canonical_parts() ) ); + } + + public function absolute( ?string $base_path = null ): self { + $base_path = $base_path ?? getcwd(); + return ( new self( rtrim( $base_path, '/\\' ) . '/' . $this->path ) )->canonical(); + } + + private function find_canonical_parts(): array { + $parts = explode( '/', $this->path ); + $root = str_starts_with( $this->path, '/' ) ? '/' : ''; + + $canonical_parts = []; + + // Collapse "." and "..", if possible + foreach ( $parts as $part ) { + if ( '.' === $part || '' === $part ) { + continue; + } + + // Collapse ".." with the previous part, if one exists + // Don't collapse ".." if the previous part is also ".." + if ( '..' === $part && \count( $canonical_parts ) > 0 && '..' !== $canonical_parts[ \count( $canonical_parts ) - 1 ] ) { + array_pop( $canonical_parts ); + + continue; + } + + // Only add ".." prefixes for relative paths + if ( '..' !== $part || '' === $root ) { + $canonical_parts[] = $part; + } + } + + return $canonical_parts; + } + + public function is_directory(): bool { + return is_dir( $this->path ); + } + + public function is_file(): bool { + return is_file( $this->path ); + } + + public function get_basename(): string { + return basename( $this->path ); + } + + public function get_filename_without_extension(): string { + return pathinfo( $this->path, \PATHINFO_FILENAME ); + } + + public function join( string ...$parts ): self { + return new self( $this->path . '/' . implode( '/', $parts ) ); + } + + /** @return self[] */ + public function read_directory(): array { + if ( ! $this->is_directory() ) { + throw new \InvalidArgumentException( sprintf( 'Path "%s" is not a directory', $this->path ) ); + } + + return array_map( + function ( $file ) { + return ( new self( $file ) )->absolute( $this->path ); + }, + array_values( + array_diff( + scandir( $this->path ), + [ '.', '..' ] + ) + ) + ); + } + + public function __toString(): string { + return $this->path; + } +} diff --git a/src/Util/PhpFileDumper.php b/src/Util/PhpFileDumper.php new file mode 100644 index 0000000000000000000000000000000000000000..8bc45f9644b4a3bebca72143d0dae353395972b7 --- /dev/null +++ b/src/Util/PhpFileDumper.php @@ -0,0 +1,51 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Util; + +class PhpFileDumper { + + public function dump( array $config, string $filename ): void { + $directory = dirname( $filename ); + $this->createCompilationDirectory( $directory ); + + $content = '<?php' . PHP_EOL . PHP_EOL; + $content .= 'declare(strict_types=1);' . PHP_EOL . PHP_EOL; + $content .= 'return ' . var_export( $config, true ) . ';' . PHP_EOL; + + $this->writeFileAtomic( $filename, $content ); + } + + private function createCompilationDirectory( string $directory ): void { + if ( ! is_dir( $directory ) && ! @mkdir( $directory, 0777, true ) && ! is_dir( $directory ) ) { + throw new \InvalidArgumentException( sprintf( 'Compilation directory does not exist and cannot be created: %s.', $directory ) ); + } + if ( ! is_writable( $directory ) ) { + throw new \InvalidArgumentException( sprintf( 'Compilation directory is not writable: %s.', $directory ) ); + } + } + + private function writeFileAtomic( string $fileName, string $content ): void { + $tmpFile = @tempnam( dirname( $fileName ), 'swap-compile' ); + if ( $tmpFile === false ) { + throw new \InvalidArgumentException( + sprintf( 'Error while creating temporary file in %s', dirname( $fileName ) ) + ); + } + @chmod( $tmpFile, 0666 ); + + $written = file_put_contents( $tmpFile, $content ); + if ( $written === false ) { + @unlink( $tmpFile ); + + throw new \InvalidArgumentException( sprintf( 'Error while writing to %s', $tmpFile ) ); + } + + @chmod( $tmpFile, 0666 ); + $renamed = @rename( $tmpFile, $fileName ); + if ( ! $renamed ) { + @unlink( $tmpFile ); + throw new \InvalidArgumentException( sprintf( 'Error while renaming %s to %s', $tmpFile, $fileName ) ); + } + } +} diff --git a/src/Util/PhpFileLoader.php b/src/Util/PhpFileLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..81209c46c0eff17b968e557eff3dfeeb0c79de03 --- /dev/null +++ b/src/Util/PhpFileLoader.php @@ -0,0 +1,27 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Util; + +class PhpFileLoader { + + /** + * @param string|Path $resource + * + * @return mixed + */ + public function load( $resource ) { + return ( static function () use ( $resource ) { + if ( ! is_readable( (string) $resource ) ) { + throw new \RuntimeException( "Could not load $resource" ); + } + + $data = include (string) $resource; + if ( $data === false ) { + throw new \RuntimeException( "Could not load $resource" ); + } + + return $data; + } )(); + } +} diff --git a/src/platform_check.php b/src/platform_check.php new file mode 100644 index 0000000000000000000000000000000000000000..c90ebb7b3fec272bba3d7b947c215216f527cf09 --- /dev/null +++ b/src/platform_check.php @@ -0,0 +1,16 @@ +<?php +if ( ! ( PHP_VERSION_ID >= 70200 ) ) { + add_action( + 'admin_notices', + function () { + printf( + '<div class="notice notice-error"><p>%s</p></div>', + __( 'The plugin cannot run on PHP versions older than 7.2. Please, contact your host and ask them to upgrade.', 'wp-init' ) + ); + } + ); + + return false; +} + +return true; diff --git a/tests/Binding/ArrayDefinitionsTest.php b/tests/Binding/ArrayDefinitionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c6f965d17e432691a0e6405f015f165dd18a9fbe --- /dev/null +++ b/tests/Binding/ArrayDefinitionsTest.php @@ -0,0 +1,67 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests\Binding; + +use WPDesk\Init\Binding\Definition\UnknownDefinition; +use WPDesk\Init\Binding\Loader\ArrayDefinitions; +use WPDesk\Init\Tests\TestCase; + +class ArrayDefinitionsTest extends TestCase { + + public function test_loading_empty_bindings(): void { + $a = new ArrayDefinitions([]); + $this->assertEquals(0, iterator_count($a->load())); + } + + public function test_loading_structured_bindings(): void { + $a = new ArrayDefinitions([ + 'hook' => [ + 'bind1', + 'bind2', + ], + 'hook2' => [ + 'bind3', + ] + ]); + $this->assertEquals( + [ + new UnknownDefinition('bind1', 'hook'), + new UnknownDefinition('bind2', 'hook'), + new UnknownDefinition('bind3', 'hook2'), + ], + iterator_to_array($a->load()) + ); + } + + public function test_loading_unstructured_bindings(): void { + $a = new ArrayDefinitions([ + 'bind1', + 'bind2', + 'hook' => 'bind3', + ]); + $this->assertEquals( + [ + new UnknownDefinition('bind1', null), + new UnknownDefinition('bind2', null), + new UnknownDefinition('bind3', 'hook'), + ], + iterator_to_array($a->load()) + ); + + $a = new ArrayDefinitions([ + 'bind1', + 'not_a_hook' => 'bind2', + 'hook' => ['bind3'], + ]); + $this->assertEquals( + [ + new UnknownDefinition('bind1', null), + new UnknownDefinition('bind2', 'not_a_hook'), + new UnknownDefinition('bind3', 'hook'), + ], + iterator_to_array($a->load()) + ); + } + +} diff --git a/tests/Binding/CompositeBindingLoaderTest.php b/tests/Binding/CompositeBindingLoaderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b8e2a992fd3e0f786c6f6f27eb5caed3f877d7dc --- /dev/null +++ b/tests/Binding/CompositeBindingLoaderTest.php @@ -0,0 +1,87 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests\Binding; + +use WPDesk\Init\Binding\Definition\UnknownDefinition; +use WPDesk\Init\Binding\Loader\ArrayDefinitions; +use WPDesk\Init\Binding\Loader\CompositeBindingLoader; +use WPDesk\Init\Tests\TestCase; + +class CompositeBindingLoaderTest extends TestCase { + + public function test_loading_empty_bindings(): void { + $a = new CompositeBindingLoader(new ArrayDefinitions([])); + $this->assertEquals(0, iterator_count($a->load())); + } + + public function test_loading_structured_bindings(): void { + $a = new CompositeBindingLoader( + new ArrayDefinitions( + [ + 'hook' => [ + 'bind1', + 'bind2', + ], + ] + ), + new ArrayDefinitions( + [ + 'hook2' => [ + 'bind3', + ] + ] + ) + ); + $this->assertEquals( + [ + new UnknownDefinition('bind1', 'hook'), + new UnknownDefinition('bind2', 'hook'), + new UnknownDefinition('bind3', 'hook2'), + ], + iterator_to_array($a->load(), false) + ); + } + + public function test_loading_unstructured_bindings(): void { + $a = new CompositeBindingLoader( + new ArrayDefinitions([ + 'bind1', + ]), + new ArrayDefinitions([ + 'bind2', + ]), + new ArrayDefinitions([ + 'hook' => 'bind3', + ]) + ); + $this->assertEquals( + [ + new UnknownDefinition('bind1', null), + new UnknownDefinition('bind2', null), + new UnknownDefinition('bind3', 'hook'), + ], + iterator_to_array($a->load(), false) + ); + + $a = new CompositeBindingLoader( + new ArrayDefinitions([ + 'bind1', + ]), + new ArrayDefinitions([ + 'not_a_hook' => 'bind2', + ]), + new ArrayDefinitions([ + 'hook' => ['bind3'], + ]), + ); + $this->assertEquals( + [ + new UnknownDefinition('bind1', null), + new UnknownDefinition('bind2', 'not_a_hook'), + new UnknownDefinition('bind3', 'hook'), + ], + iterator_to_array($a->load(), false) + ); + } +} diff --git a/tests/Binding/DirectoryBasedLoaderTest.php b/tests/Binding/DirectoryBasedLoaderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..68cacc30567e70ee3d9ca90770e442dc4eefcc4c --- /dev/null +++ b/tests/Binding/DirectoryBasedLoaderTest.php @@ -0,0 +1,48 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests\Binding; + +use WPDesk\Init\Binding\Definition\UnknownDefinition; +use WPDesk\Init\Binding\Loader\FilesystemDefinitions; +use WPDesk\Init\Configuration\Configuration; +use WPDesk\Init\Tests\TestCase; + +class DirectoryBasedLoaderTest extends TestCase { + + public function xtest_throws_when_configuration_entry_is_missing(): void { + $this->expectException(\InvalidArgumentException::class); + $a = new FilesystemDefinitions(new Configuration([])); + $a->load(); + } + + public function test_loading_empty_bindings(): void { + $this->initTempPlugin('hook-bindings'); + $a = new FilesystemDefinitions('./'); + $actual = iterator_to_array($a->load(), false); + $this->assertEquals( + [ + new UnknownDefinition('binding', 'hook1'), + new UnknownDefinition('binding1', 'plugins_loaded'), + new UnknownDefinition('binding2', 'plugins_loaded'), + ], + $actual + ); + } + + public function test_load_illogical_bindings(): void { + $this->initTempPlugin('borked-bindings'); + $a = new FilesystemDefinitions('./'); + + $actual = iterator_to_array($a->load(), false); + $this->assertEquals( + [ + new UnknownDefinition('binding', 'hook1'), + new UnknownDefinition('binding1', 'plugins_loaded'), + new UnknownDefinition('binding2', 'plugins_loaded'), + ], + $actual + ); + } + +} diff --git a/tests/DefaultHeaderParserTest.php b/tests/DefaultHeaderParserTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f3f2a40ff7c2679cb88788a8664858d1c5eebe80 --- /dev/null +++ b/tests/DefaultHeaderParserTest.php @@ -0,0 +1,67 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests; + +use WPDesk\Init\Plugin\DefaultHeaderParser; + +class DefaultHeaderParserTest extends TestCase { + + /** @dataProvider provider */ + public function test_should_parse_plugin_data_from_file( $name, string $content, array $expected ): void { + $file = $this->createTempFile($name, $content); + + $data = new DefaultHeaderParser(); + $this->assertEquals( $expected, $data->parse( $file ) ); + } + + public function provider(): iterable { + yield [ + 'first.php', +<<<PHP +<?php +/** + * Plugin Name: Example plugin + */ +PHP, + [ 'Name' => 'Example plugin' ], + ]; + + yield [ + 'second.php', +<<<PHP +<?php +/** + * Plugin Name: ShopMagic for WooCommerce + * Plugin URI: https://shopmagic.app/ + * Description: Marketing Automation and Custom Email Designer for WooCommerce + * Version: 3.0.9-beta.1 + * Author: WP Desk + * Author URI: https://shopmagic.app/ + * Text Domain: shopmagic-for-woocommerce + * Domain Path: /lang/ + * Requires at least: 5.0 + * Tested up to: 6.1 + * WC requires at least: 4.8 + * WC tested up to: 7.2 + * Requires PHP: 7.2 + */ +PHP, + [ + 'Name' => 'ShopMagic for WooCommerce', + 'PluginURI' => 'https://shopmagic.app/', + 'Version' => '3.0.9-beta.1', + 'Author' => 'WP Desk', + 'AuthorURI' => 'https://shopmagic.app/', + 'TextDomain' => 'shopmagic-for-woocommerce', + 'DomainPath' => '/lang/', + 'RequiresWP' => '5.0', + 'RequiresWC' => '4.8', + 'RequiresPHP' => '7.2', + 'TestedWP' => '6.1', + 'TestedWC' => '7.2', +], + ]; + } + +} diff --git a/tests/Dumper/PhpFileDumperTest.php b/tests/Dumper/PhpFileDumperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..84460038121f0a2b3a734cdb440529b2e54f7454 --- /dev/null +++ b/tests/Dumper/PhpFileDumperTest.php @@ -0,0 +1,20 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests\Dumper; + +use WPDesk\Init\Util\PhpFileDumper; + +class PhpFileDumperTest extends \WPDesk\Init\Tests\TestCase { + + public function test_dump_php_file() { + $dir = $this->initTempPlugin(); + $dumper = new PhpFileDumper(); + $dumper->dump( [ 'foo' => 'bar' ], $dir . '/dump.php' ); + $this->assertFileExists( $dir . '/dump.php' ); + $content = include $dir . '/dump.php'; + + $this->assertEquals( [ 'foo' => 'bar' ], $content ); + } + +} diff --git a/tests/Fixtures/advanced-plugin/advanced-plugin.php b/tests/Fixtures/advanced-plugin/advanced-plugin.php index fb4b39f5fbc2b92c17678c9e313fc52d0f561de5..15e95871193b73fb258f7cc2b43563a5b27c1ae8 100644 --- a/tests/Fixtures/advanced-plugin/advanced-plugin.php +++ b/tests/Fixtures/advanced-plugin/advanced-plugin.php @@ -1,21 +1,29 @@ <?php -$plugin = ( new \WPDesk\Init\PluginInit() ) - ->set_requirements( [ - 'wp' => '5.6', - 'php' => '7.2' - ] ) - ->add_container_definitions( [ - 'hello' => 'world', - 'hooks' => [ - 'anonymous' => static function () { - return new class implements \WPDesk\Init\HooksProvider { +/** + * Plugin Name: ShopMagic for WooCommerce + * Plugin URI: https://shopmagic.app/ + * Description: Marketing Automation and Custom Email Designer for WooCommerce + * Version: 3.0.9-beta.1 + * Author: WP Desk + * Author URI: https://shopmagic.app/ + * Text Domain: shopmagic-for-woocommerce + * Domain Path: /lang/ + * Requires at least: 5.0 + * Tested up to: 6.1 + * WC requires at least: 4.8 + * WC tested up to: 7.2 + * Requires PHP: 7.2 + */ - public function register_hooks(): void { - // TODO: Implement register_hooks() method. - } - }; - } - ], - ] ) - ->init(); +$plugin = ( new \WPDesk\Init\Kernel( [ + 'bundles' => [ + \WPDesk\Init\Bundle\ContainerBundle::class + ], + 'cache_path' => 'generated', + 'require' => [], + 'container_definitions' => [], + 'hook_subscribers' => [ + \WPDesk\Init\Bundle\ContainerBundle::class + ], +] ) )->boot(); diff --git a/tests/Fixtures/borked-bindings/index.php b/tests/Fixtures/borked-bindings/index.php new file mode 100644 index 0000000000000000000000000000000000000000..6f43e3641b1077874f255149c27237746f012755 --- /dev/null +++ b/tests/Fixtures/borked-bindings/index.php @@ -0,0 +1,5 @@ +<?php + +return [ + 'hook1' => 'binding', +]; diff --git a/tests/Fixtures/borked-bindings/plugins_loaded.php b/tests/Fixtures/borked-bindings/plugins_loaded.php new file mode 100644 index 0000000000000000000000000000000000000000..67fa7b2ff4c37c32beebefd8eb1a986a07e913fc --- /dev/null +++ b/tests/Fixtures/borked-bindings/plugins_loaded.php @@ -0,0 +1,6 @@ +<?php + +return [ + 'hook1' => 'binding1', + 'binding2', +]; diff --git a/tests/Fixtures/hook-bindings/index.php b/tests/Fixtures/hook-bindings/index.php new file mode 100644 index 0000000000000000000000000000000000000000..6f43e3641b1077874f255149c27237746f012755 --- /dev/null +++ b/tests/Fixtures/hook-bindings/index.php @@ -0,0 +1,5 @@ +<?php + +return [ + 'hook1' => 'binding', +]; diff --git a/tests/Fixtures/hook-bindings/plugins_loaded.php b/tests/Fixtures/hook-bindings/plugins_loaded.php new file mode 100644 index 0000000000000000000000000000000000000000..fc53751f7825ecd25afd538e9b11917db736f03c --- /dev/null +++ b/tests/Fixtures/hook-bindings/plugins_loaded.php @@ -0,0 +1,6 @@ +<?php + +return [ + 'binding1', + 'binding2', +]; diff --git a/tests/Fixtures/load.php b/tests/Fixtures/load.php new file mode 100644 index 0000000000000000000000000000000000000000..1733709e027a5f5be9a429672e3be627ee9938f8 --- /dev/null +++ b/tests/Fixtures/load.php @@ -0,0 +1,5 @@ +<?php + +return [ + 'foo' => 'bar' +]; diff --git a/tests/Fixtures/simple-plugin/config.php b/tests/Fixtures/simple-plugin/config.php new file mode 100644 index 0000000000000000000000000000000000000000..63da9d1507e1849521f2401cf3f9b745182096ea --- /dev/null +++ b/tests/Fixtures/simple-plugin/config.php @@ -0,0 +1,20 @@ +<?php + +return [ + 'bundles' => [ + new class { + public static function subscribers(): array { + return [ + 'WPDesk\Init\Tests\Fixtures\SimplePlugin\SimplePluginSubscriber', + ]; + } + } + ], + 'hookables' => [ + new class implements \WPDesk\Init\HookProvider\HookProvider { + public function hooks(): void { + // TODO: Implement hooks() method. + } + } + ] +]; \ No newline at end of file diff --git a/tests/Fixtures/simple-plugin/simple-plugin.php b/tests/Fixtures/simple-plugin/simple-plugin.php index 5a22cd05e86bfe3767943183cc20df1659b5bf74..7be21fd683e4597fa075ea42bfb980eafd4a59e5 100644 --- a/tests/Fixtures/simple-plugin/simple-plugin.php +++ b/tests/Fixtures/simple-plugin/simple-plugin.php @@ -1,3 +1,6 @@ <?php +/** + * Plugin Name: Example plugin + */ -$plugin = ( new \WPDesk\Init\PluginInit() )->init(); \ No newline at end of file +$plugin = ( new \WPDesk\Init\Kernel( 'config.php' ) )->boot(); diff --git a/tests/HookDriver/GenericDriverTest.php b/tests/HookDriver/GenericDriverTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ce92478304e5937f25ca752d80a7191be955d31d --- /dev/null +++ b/tests/HookDriver/GenericDriverTest.php @@ -0,0 +1,107 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests\HookDriver; + +use WPDesk\Init\Binding\Binder; +use WPDesk\Init\Binding\Binder\HookableBinder; +use WPDesk\Init\Binding\Binder\ObservableBinder; +use WPDesk\Init\Binding\Definition; +use WPDesk\Init\HookDriver\GenericDriver; +use WPDesk\Init\Configuration\Configuration; +use Psr\Container\ContainerInterface; +use WPDesk\Init\Binding\Loader\ArrayDefinitions; +use WPDesk\Init\Tests\TestCase; + +class GenericDriverTest extends TestCase { + + public function provider(): iterable { + yield [ + 'fake_binder' => new ObservableBinder(new class implements Binder { + + public function bind( Definition $def ): void { + } + }), + function ( $binder ) { + $this->assertTrue( $binder->is_bound() ); + } + ]; + +// yield 'interrupted with stoppable binder' => [ +// [ +// 'stoppable_binder' => new class implements StoppableBinder { +// +// public function should_stop(): bool { +// return true; +// } +// +// private $is_bound = false; +// public function bind(): void { +// $this->is_bound = true; +// } +// +// public function is_bound(): bool { +// return $this->is_bound; +// } +// }, +// 'fake_binder' => new ObservableBinder(new class implements Binder { +// +// public function bind(): void { +// } +// }), +// ], +// function ( $binder ) { +// $this->assertTrue( $binder->is_bound() ); +// } +// ]; + } + + public function test_register_no_hooks(): void { + $binder = new ObservableBinder($this->getBinder()); + $driver = new GenericDriver( + new ArrayDefinitions([]), + $binder + ); + + $driver->register_hooks(); + + $this->assertEquals(0, $binder->binds_count()); + } + + public function test_register_hooks(): void { + $binder = new ObservableBinder($this->getBinder()); + $driver = new GenericDriver( + new ArrayDefinitions(['' => ['hook1', 'hook2']]), + $binder + ); + + $driver->register_hooks(); + + $this->assertEquals(2, $binder->binds_count()); + } + + private function getContainer( array $services ): ContainerInterface { + return new class($services) implements ContainerInterface { + private $services; + public function __construct( $services ) { + $this->services = $services; + } + + public function get( $id ) { + return $this->services[$id]; + } + + public function has(string $id ): bool { + return isset( $this->services[$id] ); + } + }; + } + + private function getBinder(): Binder { + return new class implements Binder { + + public function bind( Definition $def ): void { + } + }; + } +} diff --git a/tests/HooksTraitTest.php b/tests/HooksTraitTest.php deleted file mode 100644 index b9ba4d053249a9aefb791a32cdd180baf8cd8573..0000000000000000000000000000000000000000 --- a/tests/HooksTraitTest.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init\Tests; - -class HooksTraitTest extends \PHPUnit\Framework\TestCase { - - /** - * How you can call a hook: - * 1. callable string: '__return_true' - * 2. closure: function () { echo "hello"; } - * 3. static call: [stdClass::class, 'hello'] - * 4. class instance call: [ $this, 'hello' ] - * 5. invokable object: $this - * ! Since PHP 8.1 - * 6. first-class callable: __return_true(...), $this->hello(...) - */ - -} \ No newline at end of file diff --git a/tests/Loader/PhpFileLoaderTest.php b/tests/Loader/PhpFileLoaderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b996b7e9d634d4e460b582ed44d3b72d17d2a2fc --- /dev/null +++ b/tests/Loader/PhpFileLoaderTest.php @@ -0,0 +1,17 @@ +<?php +declare( strict_types=1 ); + +namespace WPDesk\Init\Tests\Loader; + +use WPDesk\Init\Util\PhpFileLoader; + +class PhpFileLoaderTest extends \WPDesk\Init\Tests\TestCase { + + public function test_load_php_file() { + $loader = new PhpFileLoader(); + $resource = __DIR__ . '/../Fixtures/load.php'; + $data = $loader->load( $resource ); + $this->assertEquals( [ 'foo' => 'bar' ], $data ); + } + +} diff --git a/tests/PluginInitTest.php b/tests/PluginInitTest.php index fe5d97d7b98089a8eb25989767d4a202accf79d7..888748f9da6056d3f69d8548f5c88724ed58d554 100644 --- a/tests/PluginInitTest.php +++ b/tests/PluginInitTest.php @@ -3,8 +3,8 @@ declare( strict_types=1 ); namespace WPDesk\Init\Tests; -use WPDesk\Init\Plugin; -use WPDesk\Init\PluginInit; +use WPDesk\Init\Plugin\Plugin; +use WPDesk\Init\Kernel; use Brain\Monkey; class PluginInitTest extends TestCase { @@ -20,36 +20,10 @@ class PluginInitTest extends TestCase { parent::tearDown(); } - public function test_minimal_init(): void { - Monkey\Functions\stubs([ - 'plugin_basename', - 'plugin_dir_url' - ]); + public function xtest_initialization(): void { + $this->initTempPlugin('simple-plugin'); - $slug = 'simple-plugin'; - $dir = $this->initTempPlugin( $slug ); - $plugin = $this->load_plugin_file( $dir, $slug ); - - $this->assertFileDoesNotExist( $dir . '/cache' ); - $this->assertEquals( 'simple-plugin', $plugin->get_slug() ); - } - - public function test_advanced_init(): void { - Monkey\Functions\stubs([ - 'plugin_basename', - 'plugin_dir_url' - ]); - Monkey\Functions\expect('get_bloginfo') - ->with('version') - ->andReturn('5.6'); - - $slug = 'advanced-plugin'; - $dir = $this->initTempPlugin( $slug ); - - $plugin = $this->load_plugin_file( $dir, $slug ); - - $this->assertNotNull($plugin); - $this->assertFileExists( $dir . '/cache' ); + (new Kernel([]))->boot(); } private function load_plugin_file( $dir, $slug ): ?Plugin { @@ -61,4 +35,4 @@ class PluginInitTest extends TestCase { return $load(); } -} \ No newline at end of file +} diff --git a/tests/PluginTest.php b/tests/PluginTest.php deleted file mode 100644 index 54fc2a7a1a4da46ef2384980345d5e316fc1e3a8..0000000000000000000000000000000000000000 --- a/tests/PluginTest.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -declare( strict_types=1 ); - -namespace WPDesk\Init\Tests; - -use Psr\Container\ContainerInterface; -use WPDesk\Init\Conditional; -use WPDesk\Init\ContainerAwareInterface; -use WPDesk\Init\HooksProvider; -use WPDesk\Init\Plugin; -use WPDesk\Init\PluginAwareInterface; - -class PluginTest extends \PHPUnit\Framework\TestCase { - - public function test_should_register_hook_provider(): void { - $plugin = new Plugin(); - - $provider = new class implements HooksProvider { - public $called = false; - - public function register_hooks(): void { - $this->called = true; - } - }; - - $plugin->register_hooks( $provider ); - - $this->assertTrue( $provider->called ); - } - - public function test_should_not_register_failing_conditional_hook_provider(): void { - $plugin = new Plugin(); - - $provider = new class implements HooksProvider, Conditional { - public $called = false; - - public function is_needed(): bool { - return false; - } - - public function register_hooks(): void { - $this->called = true; - } - }; - - $plugin->register_hooks( $provider ); - - $this->assertFalse( $provider->called ); - } - - public function test_should_inject_plugin_on_provider(): void { - $plugin = new Plugin(); - - $provider = new class implements HooksProvider, PluginAwareInterface { - public $called = false; - public $plugin; - - public function set_plugin( Plugin $plugin ): void { - $this->plugin = $plugin; - } - - public function register_hooks(): void { - $this->called = true; - } - }; - - $plugin->register_hooks( $provider ); - - $this->assertSame( $plugin, $provider->plugin ); - } - - public function test_should_inject_container_on_provider(): void { - $plugin = new Plugin(); - $container = new class implements ContainerInterface { - - public function get( string $id ) { - // TODO: Implement get() method. - } - - public function has( string $id ): bool { - return true; - } - }; - $plugin->set_container( $container ); - - $provider = new class implements HooksProvider, ContainerAwareInterface { - public $called = false; - public $container; - - public function set_container( ContainerInterface $container ): void { - $this->container = $container; - } - - public function register_hooks(): void { - $this->called = true; - } - }; - - $plugin->register_hooks( $provider ); - - $this->assertSame( $container, $provider->container ); - } - -} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index d3462cbd98a0c58eaf0eb3fc93f430add44b17b1..ba3a16d9d8c85d6c8e3bc83cb71f05a9caf291df 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,11 +6,9 @@ namespace WPDesk\Init\Tests; use Symfony\Component\Filesystem\Filesystem; class TestCase extends \PHPUnit\Framework\TestCase { - /** @var string|null */ private ?string $prevCwd = null; - /** @var array */ - private array $tempPluginDirs = []; + protected array $tempPluginDirs = []; public static function getUniqueTmpDirectory( string $suffix = '' ): string { $attempts = 5; @@ -42,6 +40,13 @@ class TestCase extends \PHPUnit\Framework\TestCase { return $dir; } + public function createTempFile( string $name, string $content ): string { + $dir = self::getUniqueTmpDirectory(); + $fs = new Filesystem(); + $fs->dumpFile( $dir . '/' . $name, $content ); + return $dir . '/' . $name; + } + protected function tearDown(): void { parent::tearDown(); if ( null !== $this->prevCwd ) { @@ -73,4 +78,4 @@ class TestCase extends \PHPUnit\Framework\TestCase { } -} \ No newline at end of file +} diff --git a/tests/Util/PathTest.php b/tests/Util/PathTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e21fcd5b3e9db404c82f88986c568c020ef683f9 --- /dev/null +++ b/tests/Util/PathTest.php @@ -0,0 +1,48 @@ +<?php + +namespace WPDesk\Init\Tests\Util; + +use WPDesk\Init\Tests\TestCase; +use WPDesk\Init\Util\Path; + +class PathTest extends TestCase { + + public function test_canonical_path(): void + { + $path = new Path('src/unit/../etc'); + $this->assertEquals('src/etc', (string) $path->canonical()); + } + + public function test_absolute_path(): void + { + $path = new Path('src'); + $this->assertEquals(getcwd().'/src', (string) $path->absolute()); + } + + public function test_join(): void + { + $path = new Path('/var/www'); + $joinedPath = $path->join('public', 'html', 'index.php'); + $this->assertEquals('/var/www/public/html/index.php', (string) $joinedPath); + } + + public function test_get_basename(): void + { + $path = new Path('/var/www/public/html/index.php'); + $this->assertEquals('index.php', $path->get_basename()); + } + + public function test_get_filename_without_extension(): void + { + $path = new Path('/var/www/public/html/index.php'); + $this->assertEquals('index', $path->get_filename_without_extension()); + } + + public function test_read_directory(): void + { + $path = new Path(__DIR__ . '/../Fixtures/hook-bindings/'); + $dirContent = $path->read_directory(); + $this->assertEquals(getcwd() . '/tests/Fixtures/hook-bindings/index.php', (string) $dirContent[0]); + } + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 733441b514f558ab78146d4adf64669d1d0c6496..0ecc16b872be88c251106b193cafde9d1a96174b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,3 @@ <?php -require __DIR__ . '/../vendor/autoload.php'; - -WP_Mock::bootstrap(); \ No newline at end of file +require __DIR__ . '/../vendor/autoload.php'; \ No newline at end of file diff --git a/tests/generated/plugin.php b/tests/generated/plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..a0057af93b0e7bd2ded056e4f3a88bf3ae5703f0 --- /dev/null +++ b/tests/generated/plugin.php @@ -0,0 +1,6 @@ +<?php + +declare(strict_types=1); + +return array ( +);