From 86631635f8119aa0c1095661ab607e44f02f7cc4 Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Thu, 29 Aug 2024 10:20:20 +0200 Subject: [PATCH 1/5] Improve Package status check from ouside the Package class - Introduced Package::hasContainer(), Package::hasFailed(), Package::hasReachedStatus() to ease status check from ouside the Package class - Use the above methods to improve PackageProxyContainer access to the proxied container Co-authored-by: Thorsten Frommen Signed-off-by: Giuseppe Mazzapica --- src/Container/PackageProxyContainer.php | 7 +--- src/Package.php | 41 ++++++++++++++++++- src/Properties/BaseProperties.php | 3 +- src/Properties/PluginProperties.php | 1 + .../Container/ContainerConfiguratorTest.php | 1 + .../unit/Container/ReadOnlyContainerTest.php | 1 + tests/unit/PackageTest.php | 26 ++++++++++++ 7 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/Container/PackageProxyContainer.php b/src/Container/PackageProxyContainer.php index 49398d0..4ddd049 100644 --- a/src/Container/PackageProxyContainer.php +++ b/src/Container/PackageProxyContainer.php @@ -53,12 +53,9 @@ private function tryContainer(): bool return true; } - /** TODO: We need a better way to deal with status checking besides equality */ if ( - $this->package->statusIs(Package::STATUS_INITIALIZED) - || $this->package->statusIs(Package::STATUS_MODULES_ADDED) - || $this->package->statusIs(Package::STATUS_READY) - || $this->package->statusIs(Package::STATUS_BOOTED) + $this->package->hasContainer() + || $this->package->hasReachedStatus(Package::STATUS_INITIALIZED) ) { $this->container = $this->package->container(); } diff --git a/src/Package.php b/src/Package.php index 91b7654..2c6fbfa 100644 --- a/src/Package.php +++ b/src/Package.php @@ -153,6 +153,14 @@ class Package public const STATUS_BOOTED = 8; public const STATUS_FAILED = -8; + private const SUCCESS_STATUSES = [ + self::STATUS_IDLE => self::STATUS_IDLE, + self::STATUS_INITIALIZED => self::STATUS_INITIALIZED, + self::STATUS_BOOTING => self::STATUS_BOOTING, + self::STATUS_READY => self::STATUS_READY, + self::STATUS_BOOTED => self::STATUS_BOOTED, + ]; + private const OPERATORS = [ '<' => '<', '<=' => '<=', @@ -274,8 +282,8 @@ public function connect(Package $package): bool } // Don't connect, if already booted or boot failed - $failed = $this->statusIs(self::STATUS_FAILED); - if ($failed || $this->checkStatus(self::STATUS_INITIALIZED, '>=')) { + $failed = $this->hasFailed(); + if ($failed || $this->hasReachedStatus(self::STATUS_INITIALIZED)) { $reason = $failed ? 'an errored package' : 'a package with a built container'; $status = $failed ? 'failed' : 'built_container'; $error = "{$errorMessage} to {$reason}."; @@ -606,6 +614,14 @@ public function container(): ContainerInterface return $this->containerConfigurator->createReadOnlyContainer(); } + /** + * @return bool + */ + public function hasContainer(): bool + { + return $this->hasContainer; + } + /** * @return string */ @@ -623,6 +639,27 @@ public function statusIs(int $status): bool return $this->checkStatus($status); } + /** + * @return bool + */ + public function hasFailed(): bool + { + return $this->status === self::STATUS_FAILED; + } + + /** + * @param int $status + * @return bool + */ + public function hasReachedStatus(int $status): bool + { + if ($this->hasFailed()) { + return false; + } + + return isset(self::SUCCESS_STATUSES[$status]) && $this->checkStatus($status, '>='); + } + /** * @param int $status * @param value-of $operator diff --git a/src/Properties/BaseProperties.php b/src/Properties/BaseProperties.php index 9921747..969c1aa 100644 --- a/src/Properties/BaseProperties.php +++ b/src/Properties/BaseProperties.php @@ -10,13 +10,14 @@ class BaseProperties implements Properties protected string $baseName; protected string $basePath; protected ?string $baseUrl; + /** @var array */ protected array $properties; /** * @param string $baseName * @param string $basePath * @param string|null $baseUrl - * @param array $properties + * @param array $properties */ protected function __construct( string $baseName, diff --git a/src/Properties/PluginProperties.php b/src/Properties/PluginProperties.php index 0aacd60..9fa8fd3 100644 --- a/src/Properties/PluginProperties.php +++ b/src/Properties/PluginProperties.php @@ -65,6 +65,7 @@ protected function __construct(string $pluginMainFile) $properties[$key] = $pluginData[$pluginDataKey] ?? ''; unset($pluginData[$pluginDataKey]); } + /** @var array $properties */ $properties = array_merge($properties, $pluginData); $this->pluginMainFile = wp_normalize_path($pluginMainFile); diff --git a/tests/unit/Container/ContainerConfiguratorTest.php b/tests/unit/Container/ContainerConfiguratorTest.php index 68a5122..d583ada 100644 --- a/tests/unit/Container/ContainerConfiguratorTest.php +++ b/tests/unit/Container/ContainerConfiguratorTest.php @@ -526,6 +526,7 @@ public function testCustomContainer(): void $childContainer = new class ($expectedId, $expectedValue) implements ContainerInterface { + /** @var array */ private array $values = []; public function __construct(string $expectedId, object $expectedValue) diff --git a/tests/unit/Container/ReadOnlyContainerTest.php b/tests/unit/Container/ReadOnlyContainerTest.php index f393cc7..fae2594 100644 --- a/tests/unit/Container/ReadOnlyContainerTest.php +++ b/tests/unit/Container/ReadOnlyContainerTest.php @@ -94,6 +94,7 @@ public function testHasGetServiceFromChildContainer(): void $childContainer = new class ($expectedKey, $expectedValue) implements ContainerInterface { + /** @var array */ private array $data = []; public function __construct(string $key, \stdClass $value) diff --git a/tests/unit/PackageTest.php b/tests/unit/PackageTest.php index 75a9811..ebe0035 100644 --- a/tests/unit/PackageTest.php +++ b/tests/unit/PackageTest.php @@ -27,8 +27,28 @@ public function testBasic(): void $package = Package::new($propertiesStub); static::assertTrue($package->statusIs(Package::STATUS_IDLE)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_IDLE)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_INITIALIZED)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTING)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTED)); + + $package->build(); + + static::assertFalse($package->statusIs(Package::STATUS_IDLE)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_IDLE)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_INITIALIZED)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTING)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTED)); + static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_BOOTED)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_IDLE)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_INITIALIZED)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_BOOTING)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_BOOTED)); + static::assertFalse($package->hasReachedStatus(3)); + static::assertSame($expectedName, $package->name()); static::assertInstanceOf(Properties::class, $package->properties()); static::assertInstanceOf(ContainerInterface::class, $package->container()); @@ -900,6 +920,8 @@ public function testFailureFlowWithFailureOnBootDebugModeOff(): void static::assertFalse($package->boot()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); + static::assertTrue($package->hasFailed()); + static::assertFalse($package->hasReachedStatus(Package::STATUS_IDLE)); } /** @@ -971,6 +993,7 @@ function (\Throwable $throwable) use ($exception, $package): void { ); static::assertFalse($package->addModule($module1)->addModule($module2)->build()->boot()); + static::assertTrue($package->hasFailed()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); } @@ -1025,6 +1048,7 @@ function (\Throwable $throwable) use ($exception, $package): void { static::assertFalse($package->connect($connected)); static::assertFalse($package->boot()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); + static::assertTrue($package->hasFailed()); } /** @@ -1066,6 +1090,7 @@ function (\Throwable $throwable) use ($exception, $package): void { static::assertFalse($package->build()->boot()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); + static::assertTrue($package->hasFailed()); } /** @@ -1089,6 +1114,7 @@ public function testFailureFlowWithFailureOnBuildDebugModeOn(): void static function (\Throwable $throwable) use ($exception, $package): void { static::assertSame($exception, $throwable); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); + static::assertTrue($package->hasFailed()); } ); From 2119d0e32706741a3c6dc0a85d908ec19ebf142e Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Tue, 3 Sep 2024 12:42:50 +0200 Subject: [PATCH 2/5] Introduce "generic" hook and refactor statuses and statuses actions hooks (#52) See #47 - Added the generic, statically-named `ACTION_MODULARITY_INIT` action hook - Refactor hooks triggering, using a map between statuses ans actions, introducing two new statuses: `STATUS_INIT` and `SATUS_DONE`(which replaces the now deprecated `STATUS_BOOTED`) - Deprecate `STATUS_MODULES_ADDED` (`STATUS_BOOTING` was already an alias) - Refactor hook triggering for failed connection, moving it to a separate method. Behavior change: connecting an alreayd connected package still fires a failed action hook but does not throw anymore. - Do not use `PackageProxyContainer` when the package to connect is initialized, considering its container is already available - Allow for multiple consecutive calls to `Package::boot()` and `Package::build()` avoiding throwing unnecessary exceptions - Add extensive inline documentation to explain the different parts of the bootstrapping flow - Add a large amount of tests to cover both existing but untested scenarios as well as the new/updated behaviors - Rework the "Package" and "Application flow" documentation chapters to make them more easily consumable, better account for latest additions (not limited to the changes in this commit). * Rename actions and statuses for consistency * Add consistency in failure actions hooks (with back-compat alias for Pacakage::ACTION_FAILED_CONNECTION) --- docs/Application-flow.md | 94 +-- docs/Package.md | 382 ++++++---- src/Container/PackageProxyContainer.php | 4 +- src/Package.php | 302 +++++--- tests/unit/PackageTest.php | 968 ++++++++++++++++++------ 5 files changed, 1184 insertions(+), 566 deletions(-) diff --git a/docs/Application-flow.md b/docs/Application-flow.md index 7f259aa..ad19b0e 100644 --- a/docs/Application-flow.md +++ b/docs/Application-flow.md @@ -1,55 +1,76 @@ # The application flow -Modularity implements its application flow in two stages: +Modularity implements its application flow in two phases: - First, the application's dependencies tree is "composed" by collecting services declared in modules, adding sub-containers, and connecting other applications. - After that, the application dependency tree is locked, and the services are "consumed" to execute their behavior. -The `Package` class implements the two stages above, respectively, in the two methods: +The `Package` class implements the two phases above, respectively, in the two methods: - **`Package::build()`** - **`Package::boot()`** -For convenience, `Package::boot()` is "smart enough" to call `build()` if it was not called before, so the following code (that makes the two stages evident): + + +### Single-phase VS two-phases bootstrapping + +It must be noted that **`Package::boot()`**, before proceeding with the "boot" phase, will execute the "build" phase if it hasn't been executed yet. In other words, it is not always necessary to explicitly call `Package::build()`, and many times calling `Package::boot()` will suffice. + +The following two code snippets are equivalent: ```php Package::new($properties)->build()->boot(); ``` -is entirely equivalent to the following: - ```php Package::new($properties)->boot(); ``` + + +### Use cases for two-phased bootstrapping + +There are at least two use cases for explicitly calling `Package::build()`: + +- When a plugin needs to "execute" pretty late during the WordPress loading, let's say, at `"template_redirect"`, we might to call `Package::boot()` at the latest possible time, but call `Package::build()` earlier to enable other packages to connect to it. +- In unit tests, it might be desirable to access services from the container without any need to add hook via `Package::boot()`. In this specific case, the production code might only call `Package::boot()` while test might just use `Package::build()`. + Both stages are implemented through a series of *steps*, and the application status progresses as the steps are complete. In the process, a few action hooks are fired to allow external code to interact with the flow. -At any point of the flow, by holding an instance of the `Package` is possible to inspect the current status via `Package::statusIs()`, passing as an argument one of the `Package::STATUS_*` constants. +At any point of the flow, by holding an instance of the `Package`, it is possible to inspect the current status via `Package::statusIs()`, passing as an argument one of the `Package::STATUS_*` constants. -## Building stage + +## The "build" phase 1. Upon instantiation, the `Package` status is at **`Package::STATUS_IDLE`** -2. Default modules can be added by calling **`Package::addModule()`** on the instance. -3. The **`Package::ACTION_INIT`** action hook is fired, passing the package instance as an argument. That allows external code to add modules. -4. The `Package` status moves to **`Package::STATUS_INITIALIZED`**. The "building" stage is completed, and no more modules can be added. +2. Modules can be added by directly calling **`Package::addModule()`** on the instance, and other packages can be added by calling **`Package::connect()`**. +3. **`Package::build()`** is called. +4. The `Package` status moves to **`Package::STATUS_INITIALIZING`**. +5. The **`Package::ACTION_INIT`** action hook is fired, passing the package instance as an argument. That allows external code to add modules and connect other packages. +6. The `Package` status moves to **`Package::STATUS_INITIALIZED`**. No more modules can be added. +7. The **`Package::ACTION_INITIALIZED`** action hook is fired, passing the package instance as an argument. That allows external code to get services from the container. + -## Booting stage +## The "boot" phase + +1. **`Package::boot()`** is called. +2. `Package` status moves to **`Package::STATUS_BOOTING`**. +3. **All executables modules run**. That is when all the application behavior happens. +4. The `Package` status moves to **`Package::STATUS_BOOTED`**. +5. The **`Package::ACTION_BOOTED`** action hook is fired, passing the package instance as an argument. +6. The `Package` status moves to **`Package::STATUS_DONE`**. The booting stage is completed. `Package::boot()` returns true. -1. When the booting stage begins, the `Package` status moves to **`Package::STATUS_MODULES_ADDED`**. -2. A read-only PSR-11 container is created. It can lazily resolve the dependency tree defined in the previous stage. -3. **All executables modules run**. That is when all the application behavior happens. Note: Because the container is "lazy", only the consumed services are resolved. The `Package` never executes factory callbacks for services "registered" in the previous stage but not used in this stage. -4. The `Package` status moves to **`Package::STATUS_READY`**. -5. The **`Package::ACTION_READY`** action hook is fired, passing the package instance as an argument. External code hooking that action can access the read-only container instance, resolve services, and perform additional actions but not register modules. -6. The `Package` status moves to **`Package::STATUS_BOOTED`**. The booting stage is completed. `Package::boot()` returns true. ## The "failure flow" The steps listed above for the two stages represent the "happy paths". If any exception is thrown at any of the steps above, the flows are halted and the "failure flow" starts. -### When the failure starts during the "building" stage + + +### When the failure starts during the "build" phase 1. The `Package` status moves to **`Package::STATUS_FAILED`**. 2. The **`Package::ACTION_FAILED_BUILD`** action hook is fired, passing the raised `Throwable` as an argument. @@ -57,7 +78,9 @@ The steps listed above for the two stages represent the "happy paths". If any ex 4. If the `Properties` instance is _not_ in "debug mode", the **`Package::ACTION_FAILED_BOOT`** action hook is fired, passing a `Throwable` whose `previous` property is the `Throwable` thrown during the building stage. The "previous hierarchy" could be several levels if during the building stage many failures happened. 5. `Package::boot()` returns false. -### When the failure starts during the "booting" stage + + +### When the failure starts during the "boot" phase 1. The `Package` status moves to **`Package::STATUS_FAILED`**. 2. The **`Package::ACTION_FAILED_BOOT`** action hook is fired, passing the raised `Throwable` as an argument. @@ -65,36 +88,15 @@ The steps listed above for the two stages represent the "happy paths". If any ex 4. `Package::boot()` returns false. -## A note about default modules passed to boot() - -The `Package::boot()` method accepts a list of modules. That has been deprecated since Modularity v1.7. - -Considering that `Package::boot()` represents the "booting" stage that is supposed to happen *after* the "building" stage, it might be hard to figure out where the addition of those modules fits in the flows described above. - -When `Package::boot()` is called without calling `Package::build()` first, as in: - -```php -Package::new($properties)->boot(new ModuleOne(), new ModuleTwo()); -``` -The code is equivalent to the following: +## About modules passed to `Package::boot()` -```php -Package::new($properties)->addModule(new ModuleOne())->addModule(new ModuleTwo())->boot(); -``` - -So the "building" flow is respected. - -However, when `Package::boot()` is called after `Package::build()`, as in: - -```php -Package::new($properties)->build()->boot(new ModuleOne(), new ModuleTwo()); -``` +Passing modules to add to `Package::boot()` has been deprecated since Modularity `v1.7.0`. -The `Package` is at the end of the "building" flow after `Package::build()` is called, but it must "jump" back in the middle of "building" flow to add the modules. +For backward compatibility, when that happens, a deprecation notice is triggered (similarly to WordPress' `_deprecated_argument`) but modules are still added. -In fact, after `Package::build()` is called the application status is at `Package::STATUS_INITIALIZED`, and no more modules can be added. +It must be noted, that when first calling `Package::build()` and after that `Package::boot()` passing modules as argument, we will add those modules _after_ the status is already at `Package::STATUS_INITIALIZED` (because of the `Package::build()` call) and, as mentioned above, that should not be possible. -However, for backward compatibility reasons, in that case, the `Package` temporarily "hacks" the status back to `Package::STATUS_IDLE` so modules can be added, and then resets it to `Package::STATUS_INITIALIZED` so that the "booting" stage can start as usual. +The `Package` class still deals with this scenario aiming for 100% backward compatibility, but there's an edge case. If anything that listens to the `Package::ACTION_INITIALIZED` hook accesses the container (which is an accepted and documented possibility) the compiled container will be created, which means we can't add modules to it anymore. In this specific case, calling something like `$package->build()->boot($someModule)` will end-up in an exception. -This "hack" is why passing modules to `Package::boot()` has been deprecated and will be removed in the next major version when backward compatibility breaks are allowed. +While this is a breakage of the backward compatibility promise, it is also true that `Package::build()` was introduced in `v1.7.0` when passing modules to `Package::boot()` was deprecated. Developers who have introduced `Package::build()` should also have removed any module passed to `Package::boot()`. diff --git a/docs/Package.md b/docs/Package.md index 9fc7bd1..15e6e4b 100644 --- a/docs/Package.md +++ b/docs/Package.md @@ -1,234 +1,170 @@ # Package -This is the central class, which will allow you to add multiple Containers, register Modules and use Properties to get more information about your Application. -Aside from that, the `Package`-class will boot your Application on a specific point (like plugins_loaded) and grants access for other Applications via hook to register and extend Services via Modules. +`Package` is the library's main class that manages different modules, containers, and embeds a "properties" object that provides information about the application. -```php -boot(); -``` -The `Package`-class contains the following public API: -**Package::moduleStatus(): array** +## "Build" and "Boot" procedures -Returns an array of all Modules and the current status. +The `Package` class is responsible for "bootstrapping" the application and, by emitting hooks, enable external code to register and extend services, as well as "connecting" other `Package` instances sharing the containers. -**Package::moduleIs(string $moduleId, string $status): bool** +That happens in two separate phases, the "build" and "boot" phase. -Allows to check the status for a given `Module::id()`. +In the **"build" phase**, initialized by calling **`Package::build()`**, the class emits an hook that allow external code to add modules or connect other packages. After that, the package container is "locked" and no more services can be added. -Following `Module` statuses are available: +In the **"boot" phase**, initialized by calling **`Package::boot()`**, any "executable" module that was added in the "build" phase is now executed. -| Status | Description | -| -------------------------------------- | ------------------------------------------------------------ | -| `Package::MODULE_REGISTERED` | A `ServiceModule` was added and returned a non-zero number of services. | -| `Package::MODULE_REGISTERED_FACTORIES` | A `FactoryModule` was added and returned a non-zero number of factories. | -| `Package::MODULE_EXTENDED` | An `ExtendingModule` was added and returned a non-zero number of extension. | -| `Package::MODULE_ADDED` | _Any_ of the three statuses above applied, or a module implements `ExecutableModule` | -| `Package::MODULE_NOT_ADDED` | _None_ of the first three statuses applied for a modules that is non-executable. That might happen in two scenarios: a module only implemented base `Module` interface, or did not return any service/factory/extension. | -| `Package::MODULE_EXECUTED` | An `ExecutableModule::run()` method was called and returned `true`. | -| `Package::MODULE_EXECUTION_FAILED` | An `ExecutableModule::run()` method was called and returned `false`. | +More info about the two phases can be found in the ["Application flow" chapter](./Application-flow.md) -**Package::hookName(string $suffix = ''): string** -Allows to generate the hookName for Package-class (see below) -**Package::properties(): PropertiesInterface** +## Action hooks -Access to Properties. +It has been mentioned how during both the "build" and "boot" phases the `Package` instance emits hooks that allow external code to interact with it, e. g. by extending or connecting it. -**Package::container(): ContainerInterface** +There are three package-specific hooks: -Access to the compiled Container after the booting process is finished. +- `Package::ACTION_INITIALIZING`, fired at the beginning of the "build" phase, enables adding modules or connecting packages to the passed `Package` instance. +- `Package::ACTION_INITIALIZED`, fired at the end of the "build" phase, enables external code to access `Package`'s container, resolving services. No modification to the `Package`'s services are possible at this time or later. +- `Package::ACTION_BOOTED`, fired at the end of the "boot" phase, enables external code to access `Package`'s instance at a stage where it did all its job by registering services and adding hook to WordPress. -**Package::name():string** +All the hooks above enable access to `Package` properties and to retrieve information about specific modules. -A shortcut to `Properties::baseName()` which contains the name of your Application -**Package::addModule(Module $module): self** -Allows adding Modules from outside via custom Hooks triggered. +### Hooking package-specific hooks -**Package::statusIs(int $status): bool** +The three package-specific hooks are so called because their name is dynamic, and can be obtained via a `Package` instance, by calling `Package::hookName()` passing any of the hook name constant mentioned above. For example: -Retrieve the current status of the Application. Following are available: +```php +add_action( + $package->hookName(Package::ACTION_INIT), + fn (Package $package) => $package->addModule(new SomeModule()) +); +``` -- `Package::STATUS_IDLE` - before Application is booted. -- `Package::STATUS_INITIALIZED` - after first init action is triggered. -- `Package::STATUS_MODULES_ADDED` - after all modules have been added. -- `Package::STATUS_READY` - after the "ready" action has been fired. -- `Package::STATUS_BOOTED` - Application has successfully booted. -- `Package::STATUS_FAILED_BOOT` - when Application did not boot properly. +### Generic "init" hook -## Access from external +Besides the three package-specific hooks, the `Package` instance emits a single hook whose name is not dynamic, but is fired for every `Package` instance. -The recommended way to set up your Application is to provide a function in your Application namespace which returns an instance of Package. Here’s a short example of an “Acme”-Plugin: +The hook name is stored in the `Package::ACTION_MODULARITY_INIT` constant, it is executed right after the package-specific `Package::ACTION_INIT` hook, and unlike the three package-specific hooks, it passes the package name as first argument and the `Package` instance as second. ```php -connect(\Acme\someGlobalLibrary()) + } + } +); +``` + +Among other things, this enables to easily apply the same operations to multiple packages without calling `function_exists()` and even without knowing in advance what packages will be there. + + -declare(strict_types=1); +## Usage example -/* - * Plugin Name: Acme - * Author: Inpsyde GmbH - * Author URI: https://inpsyde.com/ - * Version: 1.0.0 - * Text Domain: acme - */ +The following code shows how to use this class for a plugin. A theme or library usage would not differ much. + +```php +/* Plugin Name: Acme */ namespace Acme; -use Inpsyde\Modularity; +use Inpsyde\Modularity\{Package, Properties}; -function plugin(): Modularity\Package { +function plugin(): Package { static $package; if (!$package) { - $properties = Modularity\Properties\PluginProperties::new(__FILE__); - $package = Modularity\Package::new($properties); + $properties = Properties\PluginProperties::new(__FILE__); + $package = Package::new($properties) + ->addModule(new ModuleOne()) + ->addModule(new ModuleTwo()); } - return $package; } -add_action( - 'plugins_loaded', - static function(): void { - plugin()->boot(); - } -); +// An early hook. Not _too_ early to allow external code to extend the instance before +// the call to `plugin()->build()` "locks" it. A late priority is used so that hooking +// 'plugins_loaded' is still ok to call `plugin()` and extend the obtained `Package`. +add_action('plugins_loaded', fn () => plugin()->build(), PHP_INT_MAX); + +// The latest hook the plugin can use to do its job. +add_action('template_redirect', fn () => plugin()->boot()); ``` -By providing the `Acme\plugin()` function, you’ll enable external code to hook into your application: +The `Acme\plugin()` function above enables external code to use an action hook to extend the package, for example adding more modules: ```php -hookName(Package::ACTION_INIT), + fn (Package $plugin) => $plugin->addModule(new MyModule()) + ); } - -add_action( - Acme\plugin()->hookName(Package::ACTION_INIT), - static function (Package $plugin): void { - $plugin->addModule(new MyModule()); - } -); ``` -## Building the package -Sometimes, especially in unit tests, it might be desirable to obtain services as defined for the -production code, but without calling any `ExecutableModule::run()`, which usually contains -WP-dependant code, and therefore requires heavy mocking. -For example, assuming a common `plugin()` function like the following: +### Alternative usage using a plugin-specific hook + +An alternative to the previous example makes use of a plugin-specific hook to allow for extension. This hook is fired inside the `plugin()` function, right before calling `build()`: ```php -function plugin(): Modularity\Package { +use Inpsyde\Modularity\{Package, Properties}; + +function plugin(): Package { static $package; if (!$package) { - $properties = Modularity\Properties\PluginProperties::new(__FILE__); - $package = Modularity\Package::new($properties) - ->addModule(new ModuleOne()) - ->addModule(new ModuleTwo()) + $properties = Properties\PluginProperties::new(__FILE__); + $package = Package::new($properties); + // Add default modules here... + do_action('acme-plugin.extend', $package); + $package->build(); } return $package; } -``` -In unit test it will be possible (as of v1.7+) to do something like the following: - -```php -$myService = plugin()->build()->container()->get(MyService::class); -static::assertTrue($myService->isValid()); +// The latest hook the plugin can use to do its job. +add_action('template_redirect', fn () => plugin()->boot()); ``` -### Booting a built container - -The `Package::boot()` method can be called on already built package. - -For example, the following is a valid unit test code: +Thanks to that, any code that needs to extend this plugin, does not need to call `function_exists()`, and the bootstrap process is easier without a separate `build()`, still keeping `boot()` as late as possible. Extending code can look like the following: ```php -$plugin = plugin()->build(); -$myService = $plugin->container()->get(MyService::class); - -static::assertTrue($myService->isValid()); -static::assertFalse($myService->isBooted()); - -$plugin->boot(); - -static::assertTrue($myService->isBooted()); -``` - -### Deprecated boot parameters - -Before Modularity v1.7.0, it was an accepted practice to pass default modules to `Package::boot()`, -as in: +use Inpsyde\Modularity\Package; -```php add_action( - 'plugins_loaded', - static function(): void { - plugin()->boot(new ModuleOne(), new ModuleTwo()); + 'acme-plugin.extend', + function (Package $plugin): void { + $plugin->addModule(new MyModule()); } ); ``` -This is now deprecated to allow a better separation of the "building" and "booting" steps. - -While it still works (and it will work up to version 2.0), it will emit a deprecation notice. - -The replacement is using `Package::addModule()`: +This approach makes sense when we expect multiple external plugins/libraries/themes to extend our plugin, e. g. when we are writing a plugin we design to be extended via extensions. -```php -plugin()->addModule(new ModuleOne())->addModule(new ModuleTwo())->boot(); -``` - -There's only one case in which calling `Package::boot()` with default modules will throw an -exception (besides triggering a deprecated notice), that is when a passed modules was not added -before `Package::addModule()` and an instance of the container was already obtained from the package. - -For example, this will throw an exception: - -```php -$plugin = plugin()->build(); - -// Now that container is built, passing modules to `boot()` will raise an exception, because we -// can't add new modules to an already "compiled" container being that read-only. -$container = $plugin->container(); - -$plugin->boot(new ModuleOne()); -``` - -To prevent the exception it would be necessary to add the module before calling `build()`, or alternatively, to call `$plugin->boot(new ModuleOne())` _before_ calling `$plugin->container()`. -In this latter case the exception is not thrown, but the deprecation will still be emitted. ## Connecting packages -Every `Package` has a separate container, however sometimes it might be desirable access another package's services. For example from a plugin access one library services, or from a theme access a plugin's services. - -That can be done using the `Package::connect()` method. +Every `Package` has a separate container, however it might be desirable access another package's services. For example, from a plugin access a library's services, or from a theme access a plugin's services. -For example: +That can be done using the `Package::connect()` method. Here's an example: ```php -// a theme functions.php - -$properties = Properties\ThemeProperties::new('/path/to/theme/dir/'); -$theme = Inpsyde\Modularity\Package::new($properties); +// Theme functions.php +use Inpsyde\Modularity\{Package, Properties}; +$theme = Package::new(Properties\ThemeProperties::new(__DIR__)); $theme->connect(\Acme\plugin()); $theme->boot(); ``` @@ -238,37 +174,165 @@ To note: - `Package::connect()` must be called **before** the package enters the "initialized" status, that is, before calling `Package::boot()` or `Package::build()`. If called later, no connections happen and it returns `false` - The package to be connected might be already booted or not. In the second case the connection will happen, but before accessing its services it has to be at least built, or an exception will happen. -Package connection is a great way to create reusable libraries and services that can be used by many plugins. For example, it might be possible to have a *library* that has something like this: +Package connection enables the creation of reusable libraries to be consumed by multiple plugins. For example, it might be possible to have a *library* that has something like this: ```php namespace Acme; +use Inpsyde\Modularity\{Package, Properties}; + function myLibrary(): Package { static $lib; if (!$lib) { $properties = Properties\LibraryProperties::new('path/to/composer.json'); - $lib = Inpsyde\Modularity\Package::new($properties); - $lib->addModule(new ModuleOne()); - $lib->addModule(new ModuleTwo()); - $lib->boot(); + Package::new($properties) + ->addModule(new ModuleOne()) + ->addModule(new ModuleTwo()) + ->boot(); } return $lib; } ``` -This would be autoloaded by Composer, but not being a plugin will not be called by WordPress. +This function might be autoloaded via Composer, autoload, but not being a plugin, it will not be executed by WordPress. -However, *many* plugins in the same installation could do: +However, multiple plugins in the same installation could do: ```php -/** @var Package $plugin */ $plugin->connect(\Acme\myLibrary()); ``` Thanks to that, all plugins will be able to access the library's services in the same way they access own modules' services. +Please note that by calling `Package::boot()` in the `myLibrary()` function immediately after having instantiated the `Package` instance will prevent any external code to extend the library, adding more modules or connecting other packages. -### Accessing connected packages' properties + +### Accessing connected packages' properties In modules, we can access package properties calling `$container->get(Package::PROPERTIES)`. If we'd like to access any connected package properties, we could do that using a key whose format is: `sprintf('%s.%s', $connectedPackage->name(), Package::PROPERTIES)`. + + + +## `Package` public API + + + + +### `Package::boot(): bool` + +Executes the "boot" phase, and the "build" phase, if it has not be executed separately via `Package::build()`. + + + +### `Package::build(): static` + +Executes the "build" phase. The inner container is safely accessible after that, and no more services can be added to it. + + + +### `Package::connect(Package $package): bool` + +Connect the given package sharing their services with the calling `Package` instance. + + + + +### `Package::connectedPackages(): array` + +Returns an array of names of packages connected via `Package::connect()`. + + + + +### `Package::container(): ContainerInterface` + +Access to the compiled PSR-11 container. Throws an exception if called before the "build" phase is completed. + + + +### `Package::hasContainer(): bool` + +Returns true if a container has already be generated for the Package, regardless current status. Note: this might be true even in case of failures. + + + +### `Package::hasFailed(): bool` + +Returns true if the current status is failed. + + + +### `Package::hasReachedStatus(int $status): bool` + +Returns true if the current given status is either the current Package status, or a status the package has previously been. Please note that it will always return false when in a "failed" status (`Package::hasFailed()` returns true). + +For the list of available statuses see `Package::statusIs()` below. + + + + +### `Package::hookName(string $suffix = ''): string` + +Generates the hook name for package-specific hooks. + + + + +### `Package::isPackageConnected(string $packageName): bool` + +Returns `true` when give a name of a package previously connected via `Package::connect()`. + + + + +### `Package::moduleIs(string $moduleId, string $status): bool` + +Used to check the status for a given `Module::id()`. The following statuses are available: + +| Status | Description | +|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Package::MODULE_REGISTERED` | A `ServiceModule` was added and returned a non-zero number of services. | +| `Package::MODULE_REGISTERED_FACTORIES` | A `FactoryModule` was added and returned a non-zero number of factories. | +| `Package::MODULE_EXTENDED` | An `ExtendingModule` was added and returned a non-zero number of extension. | +| `Package::MODULE_ADDED` | _Any_ of the three statuses above applied, or a module implements `ExecutableModule` | +| `Package::MODULE_NOT_ADDED` | _None_ of the first three statuses applied for a modules that is non-executable. That might happen in two scenarios: a module only implemented base `Module` interface, or did not return any service/factory/extension. | +| `Package::MODULE_EXECUTED` | An `ExecutableModule::run()` method was called and returned `true`. | +| `Package::MODULE_EXECUTION_FAILED` | An `ExecutableModule::run()` method was called and returned `false`. | + + + +### `Package::moduleStatus(): array` + +Returns an associative array that maps module names to their current status. + + + + +### `Package::name(): string` + +A shortcut to `Properties::baseName()`. + + + + +### `Package::properties(): PropertiesInterface` + +Access to the wrapped [properties instance](./Properties.md). + + + + +### `Package::statusIs(int $status): bool` + +Retrieve the current status of the application. The following statuses are available: + +| Status | Description | +|--------------------------------|-----------------------------------------------------------------------------------| +| `Package::STATUS_IDLE` | Before application is built or booted (`Package` instance just instantiated). | +| `Package::STATUS_INITIALIZING` | Before `Package::build()` started processing modules. | +| `Package::STATUS_INITIALIZED` | After `Package::build()` end processing modules. | +| `Package::STATUS_BOOTING` | Before `Package::boot()` started processing executable modules' "run procedures". | +| `Package::STATUS_BOOTED` | After `Package::boot()` ended processing executable modules' "run procedures". | +| `Package::STATUS_DONE` | The application has successfully completed both processes. | +| `Package::STATUS_FAILED` | The application did not build/boot properly. | \ No newline at end of file diff --git a/src/Container/PackageProxyContainer.php b/src/Container/PackageProxyContainer.php index 4ddd049..8b655f9 100644 --- a/src/Container/PackageProxyContainer.php +++ b/src/Container/PackageProxyContainer.php @@ -76,9 +76,7 @@ private function assertPackageBooted(string $id): void } $name = $this->package->name(); - $status = $this->package->statusIs(Package::STATUS_FAILED) - ? 'is errored' - : 'is not ready yet'; + $status = $this->package->hasFailed() ? 'is errored' : 'is not ready yet'; $error = "Error retrieving service {$id} because package {$name} {$status}."; throw new class (esc_html($error)) extends \Exception implements ContainerExceptionInterface diff --git a/src/Package.php b/src/Package.php index 2c6fbfa..737a999 100644 --- a/src/Package.php +++ b/src/Package.php @@ -41,8 +41,9 @@ class Package public const PROPERTIES = 'properties'; /** - * Custom action to be used to add Modules to the package. + * Custom action to be used to add modules and connect other packages. * It might also be used to access package properties. + * Access container is not possible at this stage. * * @example * @@ -50,67 +51,64 @@ class Package * * add_action( * $package->hookName(Package::ACTION_INIT), - * $callback + * fn (Package $package) => // do something, * ); * */ public const ACTION_INIT = 'init'; /** - * Custom action which is triggered after the application - * is booted to access container and properties. + * Very similar to `ACTION_INIT`, but it is static, so not dependent on package name. + * It passes package name as first argument. * * @example - * - * $package = Package::new(); - * - * add_action( - * $package->hookName(Package::ACTION_READY), - * $callback - * ); - * + * + * add_action( + * Package::ACTION_MODULARITY_INIT, + * fn (string $packageName, Package $package) => // do something, + * 10, + * 2 + * ); + * */ - public const ACTION_READY = 'ready'; + public const ACTION_MODULARITY_INIT = self::HOOK_PREFIX . self::ACTION_INIT; /** - * Custom action which is triggered when a failure happens during the building stage. - * - * @example - * - * $package = Package::new(); - * - * add_action( - * $package->hookName(Package::ACTION_FAILED_BUILD), - * $callback - * ); - * + * Action fired when it is safe to access container. + * Add more modules is not anymore possible at this stage. + */ + public const ACTION_INITIALIZED = 'initialized'; + + /** + * Action fired when plugin finished its bootstrapping process, all its hooks are added. + * Add more modules is not anymore possible at this stage. + */ + public const ACTION_BOOTED = 'ready'; + + /** + * Action fired when anything went wrong during the "build" procedure. */ public const ACTION_FAILED_BUILD = 'failed-build'; /** - * Custom action which is triggered when a failure happens during the booting stage. - * - * @example - * - * $package = Package::new(); - * - * add_action( - * $package->hookName(Package::ACTION_FAILED_BOOT), - * $callback - * ); - * + * Action fired when anything went wrong during the "boot" procedure. */ public const ACTION_FAILED_BOOT = 'failed-boot'; /** - * Custom action which is triggered when a package is connected. + * Action fired when adding a module failed. */ - public const ACTION_PACKAGE_CONNECTED = 'package-connected'; + public const ACTION_FAILED_ADD_MODULE = 'failed-add-module'; /** - * Custom action which is triggered when a package cannot be connected. + * Action fired when a package connection failed. */ - public const ACTION_FAILED_CONNECTION = 'failed-connection'; + public const ACTION_FAILED_CONNECT = 'failed-connection'; + + /** + * Action fired when a package is connected successfully. + */ + public const ACTION_PACKAGE_CONNECTED = 'package-connected'; /** * Module states can be used to get information about your module. @@ -138,27 +136,43 @@ class Package * @example * * $package = Package::new(); - * $package->statusIs(Package::IDLE); // true + * $package->statusIs(Package::STATUS_IDLE); // true * $package->build(); - * $package->statusIs(Package::INITIALIZED); // true + * $package->statusIs(Package::STATUS_INITIALIZED); // true * $package->boot(); - * $package->statusIs(Package::BOOTED); // true + * $package->statusIs(Package::STATUS_DONE); // true * */ public const STATUS_IDLE = 2; + public const STATUS_INITIALIZING = 3; public const STATUS_INITIALIZED = 4; - public const STATUS_MODULES_ADDED = 5; - public const STATUS_BOOTING = self::STATUS_MODULES_ADDED; - public const STATUS_READY = 7; - public const STATUS_BOOTED = 8; + public const STATUS_BOOTING = 5; + public const STATUS_BOOTED = 7; + public const STATUS_DONE = 8; public const STATUS_FAILED = -8; + // Deprecated flags + /** @deprecated */ + public const STATUS_MODULES_ADDED = self::STATUS_BOOTING; + /** @deprecated */ + public const ACTION_READY = self::ACTION_BOOTED; + /** @deprecated */ + public const ACTION_FAILED_CONNECTION = self::ACTION_FAILED_CONNECT; + + // Map of status to package-specific and global hook, both optional (i..e, null). + private const STATUSES_ACTIONS_MAP = [ + self::STATUS_INITIALIZING => [self::ACTION_INIT, self::ACTION_MODULARITY_INIT], + self::STATUS_INITIALIZED => [self::ACTION_INITIALIZED, null], + self::STATUS_BOOTED => [self::ACTION_BOOTED, null], + ]; + private const SUCCESS_STATUSES = [ self::STATUS_IDLE => self::STATUS_IDLE, + self::STATUS_INITIALIZING => self::STATUS_INITIALIZING, self::STATUS_INITIALIZED => self::STATUS_INITIALIZED, self::STATUS_BOOTING => self::STATUS_BOOTING, - self::STATUS_READY => self::STATUS_READY, self::STATUS_BOOTED => self::STATUS_BOOTED, + self::STATUS_DONE => self::STATUS_DONE, ]; private const OPERATORS = [ @@ -218,7 +232,9 @@ static function () use ($properties): Properties { public function addModule(Module $module): Package { try { - $this->assertStatus(self::STATUS_IDLE, sprintf('add module %s', $module->id())); + $reason = sprintf('add module %s', $module->id()); + $this->assertStatus(self::STATUS_FAILED, $reason, '!='); + $this->assertStatus(self::STATUS_INITIALIZING, $reason, '<='); $registeredServices = $this->addModuleServices( $module, @@ -245,7 +261,7 @@ public function addModule(Module $module): Package $status = $added ? self::MODULE_ADDED : self::MODULE_NOT_ADDED; $this->moduleProgress($module->id(), $status); } catch (\Throwable $throwable) { - $this->handleFailure($throwable, self::ACTION_FAILED_BUILD); + $this->handleFailure($throwable, self::ACTION_FAILED_ADD_MODULE); } return $this; @@ -266,34 +282,17 @@ public function connect(Package $package): bool } $packageName = $package->name(); - $errorData = ['package' => $packageName, 'status' => $this->status]; - $errorMessage = "Failed connecting package {$packageName}"; // Don't connect, if already connected if (array_key_exists($packageName, $this->connectedPackages)) { - $error = "{$errorMessage} because it was already connected."; - do_action( - $this->hookName(self::ACTION_FAILED_CONNECTION), - $packageName, - new \WP_Error('already_connected', $error, $errorData) - ); - - throw new \Exception($error, 0, $this->lastError); + return $this->handleConnectionFailure($packageName, 'already connected', false); } // Don't connect, if already booted or boot failed $failed = $this->hasFailed(); if ($failed || $this->hasReachedStatus(self::STATUS_INITIALIZED)) { - $reason = $failed ? 'an errored package' : 'a package with a built container'; - $status = $failed ? 'failed' : 'built_container'; - $error = "{$errorMessage} to {$reason}."; - do_action( - $this->hookName(self::ACTION_FAILED_CONNECTION), - $packageName, - new \WP_Error("no_connect_on_{$status}", $error, $errorData) - ); - - throw new \Exception($error, 0, $this->lastError); + $reason = $failed ? 'is errored' : 'has a built container already'; + $this->handleConnectionFailure($packageName, "current package {$reason}", true); } $this->connectedPackages[$packageName] = true; @@ -307,9 +306,10 @@ static function () use ($package): Properties { } ); - // If the other package is booted, we can obtain a container, otherwise - // we build a proxy container - $container = $package->statusIs(self::STATUS_BOOTED) + // If we can obtain a container we do, otherwise we build a proxy container + $packageHasContainer = $package->hasReachedStatus(self::STATUS_INITIALIZED) + || $package->hasContainer(); + $container = $packageHasContainer ? $package->container() : new PackageProxyContainer($package); @@ -342,18 +342,26 @@ static function () use ($package): Properties { public function build(): Package { try { - // Don't allow building the application multiple times. + // Be tolerant about things like `$package->build()->build()`. + // Sometimes, from the extern, we might want to call `build()` to ensure the container + // is ready before accessing a service. And in that case we don't want to throw an + // exception if the container is already built. + if ($this->built && $this->statusIs(self::STATUS_INITIALIZED)) { + return $this; + } + + // We expect `build` to be called only after `addModule()` or `connect()` which do + // not change the status, so we expect status to be still "IDLE". + // This will prevent invalid things like calling `build()` from inside something + // hooking ACTION_INIT OR ACTION_INITIALIZED. $this->assertStatus(self::STATUS_IDLE, 'build package'); - do_action( - $this->hookName(self::ACTION_INIT), - $this - ); - // Changing the status here ensures we can not call this method again, and also we can - // not add new modules, because both here and in `addModule()` we check for idle status. - // For backward compatibility, adding new modules via `boot()` will still be possible, - // even if deprecated, at the condition that the container was not yet accessed at that - // point. + // This will change the status to "INITIALIZING" then fire the action that allow other + // packages to add modules or connect packages. + $this->progress(self::STATUS_INITIALIZING); + + // This will change the status to "INITIALIZED" then fire an action when it is safe to + // access the container, because from this moment on, container is locked from change. $this->progress(self::STATUS_INITIALIZED); } catch (\Throwable $throwable) { $this->handleFailure($throwable, self::ACTION_FAILED_BUILD); @@ -371,31 +379,43 @@ public function build(): Package public function boot(Module ...$defaultModules): bool { try { + // When package is done, nothing should happen to it calling boot again, but we call + // false to signal something is off. + if ($this->statusIs(self::STATUS_DONE)) { + return false; + } + // Call build() if not called yet, and ensure any new module passed here is added // as well, throwing if the container was already built. $this->doBuild(...$defaultModules); - // Don't allow booting the application multiple times. - $this->assertStatus(self::STATUS_BOOTING, 'boot application', '<'); - $this->assertStatus(self::STATUS_FAILED, 'boot application', '!='); + // Make sure we call boot() on a non-failed instance, and also make a sanity check + // on the status flow, e.g. prevent calling boot() from an action hook. + $this->assertStatus(self::STATUS_INITIALIZED, 'boot application'); + // This will change status to STATUS_BOOTING "locking" subsequent call to `boot()`, but + // no hook is fired here, because at this point we can not do anything more or less than + // what can be done on the ACTION_INITIALIZED hook, so that hook is sufficient. $this->progress(self::STATUS_BOOTING); $this->doExecute(); - $this->progress(self::STATUS_READY); - - do_action( - $this->hookName(self::ACTION_READY), - $this - ); + // This will change status to STATUS_BOOTED and then fire an action that make it + // possible to hook on a package that has finished its bootstrapping process, so all its + // "executable" modules have been executed. + $this->progress(self::STATUS_BOOTED); } catch (\Throwable $throwable) { $this->handleFailure($throwable, self::ACTION_FAILED_BOOT); return false; } - $this->progress(self::STATUS_BOOTED); + // This will change the status to DONE and will not fire any action. + // This is a status that proves that everything went well, not only the Package itself, + // but also anything hooking Package's hooks. + // The only way to move out of this status is a failure that might only happen directly + // calling `addModule()`, `connect()` or `build()`. + $this->progress(self::STATUS_DONE); return true; } @@ -418,39 +438,67 @@ private function doBuild(Module ...$defaultModules): void ); } + // We expect `boot()` to be called either: + // 1. Directly after `addModule()`/`connect()`, without any `build()` call in between, so + // status is IDLE and `$this->built` is `false`. + // 2. After `build()` is called, so status is INITIALIZED and `$this->built` is `true`. + // Any other usage is not allowed (e.g. calling `boot()` from an hook callback) and in that + // case we return here, giving back control to `boot()` which will throw. + $validFlows = (!$this->built && $this->statusIs(self::STATUS_IDLE)) + || ($this->built && $this->statusIs(self::STATUS_INITIALIZED)); + + if (!$validFlows) { + // If none of the two supported flows happened, we just return handling control back + // to `boot()`, that will throw. + return; + } + if (!$this->built) { - $defaultModules and array_map([$this, 'addModule'], $defaultModules); + // First valid flow: `boot()` was called directly after `addModule()`/`connect()` + // without any call to `build()`. We can call `build()` and return, handing control + // back to `boot()`. Before returning, if we had default modules passed to `boot()` we + // already have fired a deprecation, so here we just add them dealing with back-compat. + foreach ($defaultModules as $defaultModule) { + $this->addModule($defaultModule); + } $this->build(); return; } - if ( - !$defaultModules - || ($this->checkStatus(self::STATUS_INITIALIZED, '>')) - || ($this->statusIs(self::STATUS_FAILED)) - ) { - // If we don't have default modules, there's nothing to do, and if the status is beyond - // initialized or is failed, we do nothing as well and let `boot()` throw. + // Second valid flow: we have called `boot()` after `build()`. If we did it correctly, + // without default modules passed to `boot()`, we can just return handing control back + // to `boot()`. + if (!$defaultModules) { return; } + // If here, we have done something like: `$package->build()->boot($module1, $module2)`. + // Passing modules to `boot()` was deprecated when `build()` was introduced, so whoever + // added `build()` should have removed modules passed to `boot()`. + // But we want to keep 100% backward compatibility so we still support this behavior + // until the next major is released. To do that, we simulate IDLE status to prevent + // `addModule()` from throwing when adding default modules. + // But we can do that only if we don't have a compiled container yet. + // If anything hooking ACTION_INIT called `container()` we have a compiled container + // already, and we can't add modules, so we not going to simulate INIT status, which mean + // the `$this->addModule()` call below will throw. $backup = $this->status; - try { - // simulate idle status to prevent `addModule()` from throwing - // only if we don't have a container yet - $this->hasContainer or $this->status = self::STATUS_IDLE; - + if (!$this->hasContainer()) { + $this->status = self::STATUS_IDLE; + } foreach ($defaultModules as $defaultModule) { - // If a module was added by `build()` or `addModule()` we can skip it, a - // deprecation was trigger to make it noticeable without breakage + // If a module was already added via `addModule()` we can skip it, reducing the + // chances of throwing an exception if not needed. if (!$this->moduleIs($defaultModule->id(), self::MODULE_ADDED)) { $this->addModule($defaultModule); } } } finally { - $this->status = $backup; + if (!$this->hasFailed()) { + $this->status = $backup; + } } } @@ -678,6 +726,42 @@ private function checkStatus(int $status, string $operator = '=='): bool private function progress(int $status): void { $this->status = $status; + + [$packageHookSuffix, $globalHook] = self::STATUSES_ACTIONS_MAP[$status] ?? [null, null]; + if ($packageHookSuffix !== null) { + do_action($this->hookName($packageHookSuffix), $this); + } + if ($globalHook !== null) { + do_action($globalHook, $this->name(), $this); + } + } + + /** + * @param string $packageName + * @param string $reason + * @param bool $throw + * @return ($throw is true ? never: false) + */ + private function handleConnectionFailure(string $packageName, string $reason, bool $throw): bool + { + $errorData = ['package' => $packageName, 'status' => $this->status]; + $message = "Failed connecting package {$packageName} because {$reason}."; + + do_action( + $this->hookName(self::ACTION_FAILED_CONNECT), + $packageName, + new \WP_Error('failed_connection', $message, $errorData) + ); + + if ($throw) { + throw new \Exception( + esc_html($message), + 0, + $this->lastError // phpcs:ignore WordPress.Security.EscapeOutput + ); + } + + return false; } /** @@ -709,7 +793,7 @@ private function assertStatus(int $status, string $action, string $operator = '= throw new \Exception( sprintf("Can't %s at this point of application.", esc_html($action)), 0, - $this->lastError // phpcs:ignore + $this->lastError // phpcs:ignore WordPress.Security.EscapeOutput ); } } diff --git a/tests/unit/PackageTest.php b/tests/unit/PackageTest.php index ebe0035..55785fa 100644 --- a/tests/unit/PackageTest.php +++ b/tests/unit/PackageTest.php @@ -31,23 +31,33 @@ public function testBasic(): void static::assertFalse($package->hasReachedStatus(Package::STATUS_INITIALIZED)); static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTING)); static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTED)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_DONE)); $package->build(); - static::assertFalse($package->statusIs(Package::STATUS_IDLE)); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); static::assertTrue($package->hasReachedStatus(Package::STATUS_IDLE)); static::assertTrue($package->hasReachedStatus(Package::STATUS_INITIALIZED)); static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTING)); static::assertFalse($package->hasReachedStatus(Package::STATUS_BOOTED)); + static::assertFalse($package->hasReachedStatus(Package::STATUS_DONE)); - static::assertTrue($package->boot()); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED)) + ->once() + ->whenHappen(static function (Package $package): void { + static::assertTrue($package->statusIs(Package::STATUS_BOOTED)); + }); - static::assertTrue($package->statusIs(Package::STATUS_BOOTED)); + static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); static::assertTrue($package->hasReachedStatus(Package::STATUS_IDLE)); static::assertTrue($package->hasReachedStatus(Package::STATUS_INITIALIZED)); static::assertTrue($package->hasReachedStatus(Package::STATUS_BOOTING)); static::assertTrue($package->hasReachedStatus(Package::STATUS_BOOTED)); - static::assertFalse($package->hasReachedStatus(3)); + static::assertTrue($package->hasReachedStatus(Package::STATUS_DONE)); + static::assertFalse($package->hasReachedStatus(6)); + // check back compat + static::assertTrue($package->hasReachedStatus(Package::STATUS_MODULES_ADDED)); static::assertSame($expectedName, $package->name()); static::assertInstanceOf(Properties::class, $package->properties()); @@ -95,10 +105,10 @@ public static function provideHookNameSuffix(): \Generator $baseHookName . '.' . Package::ACTION_INIT, ]; - yield 'ready' => [ - Package::ACTION_READY, + yield 'booted' => [ + Package::ACTION_BOOTED, $expectedName, - $baseHookName . '.' . Package::ACTION_READY, + $baseHookName . '.' . Package::ACTION_BOOTED, ]; } @@ -110,35 +120,80 @@ public function testBootWithEmptyModule(): void $expectedId = 'my-module'; $moduleStub = $this->stubModule($expectedId); - $propertiesStub = $this->stubProperties('name', false); + $propertiesStub = $this->stubProperties('name', true); $package = Package::new($propertiesStub)->addModule($moduleStub); static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); static::assertTrue($package->moduleIs($expectedId, Package::MODULE_NOT_ADDED)); static::assertFalse($package->moduleIs($expectedId, Package::MODULE_REGISTERED)); static::assertFalse($package->moduleIs($expectedId, Package::MODULE_REGISTERED_FACTORIES)); static::assertFalse($package->moduleIs($expectedId, Package::MODULE_EXTENDED)); static::assertFalse($package->moduleIs($expectedId, Package::MODULE_ADDED)); - // booting again fails, but does not throw because debug is false + // booting again return false, but we expect no breakage static::assertFalse($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); } /** * @test */ - public function testBootWithServiceModule(): void + public function testBuildWithEmptyModule(): void { - $moduleId = 'my-service-module'; - $serviceId = 'service-id'; + $expectedId = 'my-module'; - $module = $this->stubModule($moduleId, ServiceModule::class); - $module->expects('services')->andReturn($this->stubServices($serviceId)); + $moduleStub = $this->stubModule($expectedId); + $propertiesStub = $this->stubProperties('name', true); - $package = Package::new($this->stubProperties())->addModule($module); + $package = Package::new($propertiesStub)->addModule($moduleStub); + + $package->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + static::assertTrue($package->moduleIs($expectedId, Package::MODULE_NOT_ADDED)); + static::assertFalse($package->moduleIs($expectedId, Package::MODULE_REGISTERED)); + static::assertFalse($package->moduleIs($expectedId, Package::MODULE_REGISTERED_FACTORIES)); + static::assertFalse($package->moduleIs($expectedId, Package::MODULE_EXTENDED)); + static::assertFalse($package->moduleIs($expectedId, Package::MODULE_ADDED)); + + // building again we expect no breakage + $package->build()->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + } + + /** + * @test + */ + public function testBootWithServiceModule(): void + { + $moduleId = 'module_test'; + $serviceId = 'service_test'; + + $package = $this->stubSimplePackage('test'); static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXTENDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertTrue($package->container()->has($serviceId)); + } + + /** + * @test + */ + public function testBuildWithServiceModule(): void + { + $moduleId = 'module_test'; + $serviceId = 'service_test'; + + $package = $this->stubSimplePackage('test'); + + $package->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); @@ -161,6 +216,30 @@ public function testBootWithFactoryModule(): void $package = Package::new($this->stubProperties())->addModule($module); static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXTENDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertTrue($package->container()->has($factoryId)); + } + + /** + * @test + */ + public function testBuildWithFactoryModule(): void + { + $moduleId = 'my-factory-module'; + $factoryId = 'factory-id'; + + $module = $this->stubModule($moduleId, FactoryModule::class); + $module->expects('factories')->andReturn($this->stubServices($factoryId)); + + $package = Package::new($this->stubProperties())->addModule($module); + + $package->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); @@ -183,6 +262,31 @@ public function testBootWithExtendingModuleWithNonExistingService(): void $package = Package::new($this->stubProperties())->addModule($module); static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXTENDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + // false because extending a service not in container + static::assertFalse($package->container()->has($extensionId)); + } + + /** + * @test + */ + public function testBuildWithExtendingModuleWithNonExistingService(): void + { + $moduleId = 'my-extension-module'; + $extensionId = 'extension-id'; + + $module = $this->stubModule($moduleId, ExtendingModule::class); + $module->expects('extensions')->andReturn($this->stubServices($extensionId)); + + $package = Package::new($this->stubProperties())->addModule($module); + + $package->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); @@ -195,95 +299,615 @@ public function testBootWithExtendingModuleWithNonExistingService(): void /** * @test */ - public function testBootWithExtendingModuleWithExistingService(): void + public function testBootWithExtendingModuleWithExistingService(): void + { + $moduleId = 'my-extension-module'; + $serviceId = 'service-id'; + + $module = $this->stubModule($moduleId, ServiceModule::class, ExtendingModule::class); + $module->expects('services')->andReturn($this->stubServices($serviceId)); + $module->expects('extensions')->andReturn($this->stubServices($serviceId)); + + $package = Package::new($this->stubProperties())->addModule($module); + + static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXTENDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertTrue($package->container()->has($serviceId)); + } + + /** + * @test + */ + public function testBuildWithExtendingModuleWithExistingService(): void + { + $moduleId = 'my-extension-module'; + $serviceId = 'service-id'; + + $module = $this->stubModule($moduleId, ServiceModule::class, ExtendingModule::class); + $module->expects('services')->andReturn($this->stubServices($serviceId)); + $module->expects('extensions')->andReturn($this->stubServices($serviceId)); + + $package = Package::new($this->stubProperties())->addModule($module); + + $package->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXTENDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertTrue($package->container()->has($serviceId)); + } + + /** + * @test + */ + public function testBootWithExecutableModule(): void + { + $moduleId = 'executable-module'; + $module = $this->stubModule($moduleId, ExecutableModule::class); + $module->expects('run')->andReturn(true); + + $package = Package::new($this->stubProperties())->addModule($module); + + static::assertTrue($package->boot()); + static::assertTrue($package->statusIs(Package::STATUS_DONE)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXECUTED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXECUTION_FAILED)); + } + + /** + * @test + */ + public function testBuildWithExecutableModule(): void + { + $moduleId = 'executable-module'; + $module = $this->stubModule($moduleId, ExecutableModule::class); + $module->expects('run')->never(); + + $package = Package::new($this->stubProperties())->addModule($module); + + $package->build(); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXECUTED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXECUTION_FAILED)); + } + + /** + * Test, when the ExecutableModule::run() return false, that the state is correctly set. + * + * @test + */ + public function testBootWithExecutableModuleFailed(): void + { + $moduleId = 'executable-module'; + $module = $this->stubModule($moduleId, ExecutableModule::class); + $module->expects('run')->andReturn(false); + + $package = Package::new($this->stubProperties())->addModule($module); + + static::assertTrue($package->boot()); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); + static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXECUTED)); + static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXECUTION_FAILED)); + } + + /** + * @test + * @runInSeparateProcess + */ + public function testBootPassingModulesEmitDeprecation(): void + { + $module1 = $this->stubModule('module_1', ServiceModule::class); + $module1->allows('services')->andReturn($this->stubServices('service_1')); + + $package = Package::new($this->stubProperties('test', true)); + + $this->convertDeprecationsToExceptions(); + try { + $count = 0; + $package->boot($module1); + } catch (\Throwable $throwable) { + $count++; + $this->assertThrowableMessageMatches($throwable, 'boot().+?deprecated.+?1\.7'); + } finally { + static::assertSame(1, $count); + } + } + + /** + * @test + * @runInSeparateProcess + */ + public function testBootPassingModulesAddModules(): void + { + $module1 = $this->stubModule('module_1', ServiceModule::class); + $module1->allows('services')->andReturn($this->stubServices('service_1')); + + $package = Package::new($this->stubProperties('test', true)); + + $this->ignoreDeprecations(); + $package->boot($module1); + + static::assertSame('service_1', $package->container()->get('service_1')['id']); + } + + /** + * @test + */ + public function testAddModuleFailsAfterBuild(): void + { + $package = Package::new($this->stubProperties('test', true))->build(); + + $this->expectExceptionMessageMatches("/add module/i"); + + $package->addModule($this->stubModule()); + } + + /** + * @test + * + * phpcs:disable Inpsyde.CodeQuality.NestingLevel + */ + public function testBuildResolveServices(): void + { + // phpcs:enable phpcs:disable Inpsyde.CodeQuality.NestingLevel + $module = new class () implements ServiceModule, ExtendingModule, ExecutableModule + { + public function id(): string + { + return 'test-module'; + } + + public function services(): array + { + return [ + 'dependency' => static function (): object { + return (object) ['x' => 'Works!']; + }, + 'service' => static function (ContainerInterface $container): object { + $works = $container->get('dependency')->x; + + return new class (['works?' => $works]) extends \ArrayObject + { + }; + }, + ]; + } + + public function extensions(): array + { + return [ + 'service' => function (\ArrayObject $current): object { + return new class ($current) + { + public \ArrayObject $object; // phpcs:ignore + + public function __construct(\ArrayObject $object) + { + $this->object = $object; + } + + public function works(): string + { + return $this->object->offsetGet('works?'); + } + }; + }, + ]; + } + + public function run(ContainerInterface $container): bool + { + throw new \Error('This should not run!'); + } + }; + + $actual = Package::new($this->stubProperties()) + ->addModule($module) + ->build() + ->container() + ->get('service') + ->works(); + + static::assertSame('Works!', $actual); + } + + /** + * @test + */ + public function testBuildPassingModulesToBoot(): void + { + $module1 = $this->stubModule('module_1', ServiceModule::class); + $module1->expects('services')->andReturn($this->stubServices('service_1')); + + $module2 = $this->stubModule('module_2', ServiceModule::class); + $module2->expects('services')->andReturn($this->stubServices('service_2')); + + $module3 = $this->stubModule('module_3', ServiceModule::class); + $module3->expects('services')->andReturn($this->stubServices('service_3')); + + $package = Package::new($this->stubProperties('test', true)) + ->addModule($module1) + ->addModule($module2) + ->build(); + + $this->ignoreDeprecations(); + $package->boot($module2, $module3); + + $container = $package->container(); + + static::assertSame('service_1', $container->get('service_1')['id']); + static::assertSame('service_2', $container->get('service_2')['id']); + static::assertSame('service_3', $container->get('service_3')['id']); + } + + /** + * @test + */ + public function testBootFailsIfPassingNotAddedModulesAfterContainer(): void + { + $module1 = $this->stubModule('module_1', ServiceModule::class); + $module1->expects('services')->andReturn($this->stubServices('service_1')); + + $module2 = $this->stubModule('module_2', ServiceModule::class); + $module2->expects('services')->andReturn($this->stubServices('service_2')); + + $module3 = $this->stubModule('module_3', ServiceModule::class); + $module3->allows('services')->andReturn($this->stubServices('service_3')); + + $package = Package::new($this->stubProperties('test', true)) + ->addModule($module1) + ->addModule($module2) + ->build(); + + $container = $package->container(); + + static::assertSame('service_1', $container->get('service_1')['id']); + static::assertSame('service_2', $container->get('service_2')['id']); + + $this->expectExceptionMessageMatches("/can't add module module_3/i"); + $this->ignoreDeprecations(); + $package->boot($module2, $module3); + } + + /** + * @test + */ + public function testBootFireHooks(): void + { + $package = $this->stubSimplePackage('1'); + + $log = []; + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_PACKAGE_CONNECTED)) + ->once() + ->whenHappen( + static function (string $packageName, int $status) use (&$log): void { + static::assertSame($status, Package::STATUS_IDLE); + $log[] = 0; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZING)); + $log[] = 1; + } + ); + + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT) + ->once() + ->whenHappen( + static function (string $packageName, Package $package) use (&$log): void { + static::assertSame('package_1', $packageName); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZING)); + $log[] = 2; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + $log[] = 3; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_BOOTED)); + $log[] = 4; + } + ); + + $package->connect(Package::new($this->stubProperties('connected', true))); + $package->boot(); + + static::assertSame(range(0, 4), $log); + static::assertCount(1, $package->connectedPackages()); + } + + /** + * This is identical to the above where we do only `boot()`, we do here `build()->boot()` but + * we expect identical result. + * + * @test + */ + public function testBuildAndBootFireHooks(): void + { + $package = $this->stubSimplePackage('1'); + + $log = []; + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_PACKAGE_CONNECTED)) + ->once() + ->whenHappen( + static function (string $packageName, int $status) use (&$log): void { + static::assertSame($status, Package::STATUS_IDLE); + $log[] = 0; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZING)); + $log[] = 1; + } + ); + + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT) + ->once() + ->whenHappen( + static function (string $packageName, Package $package) use (&$log): void { + static::assertSame('package_1', $packageName); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZING)); + $log[] = 2; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + $log[] = 3; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_BOOTED)); + $log[] = 4; + } + ); + + $package->connect(Package::new($this->stubProperties('connected', true))); + $package->build()->boot(); + + static::assertSame(range(0, 4), $log); + static::assertCount(1, $package->connectedPackages()); + } + + /** + * This is mostly identical to the above where we do `build()->boot()` but here we do + * we do just `build()` and we expect very similar result, but ACTION_READY never fired. + * + * @test + */ + public function testBuildFireHooks(): void + { + $package = $this->stubSimplePackage('1'); + + $log = []; + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_PACKAGE_CONNECTED)) + ->once() + ->whenHappen( + static function (string $packageName, int $status) use (&$log): void { + static::assertSame($status, Package::STATUS_IDLE); + $log[] = 0; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZING)); + $log[] = 1; + } + ); + + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT) + ->once() + ->whenHappen( + static function (string $packageName, Package $package) use (&$log): void { + static::assertSame('package_1', $packageName); + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZING)); + $log[] = 2; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED)) + ->once() + ->whenHappen( + static function (Package $package) use (&$log): void { + static::assertTrue($package->statusIs(Package::STATUS_INITIALIZED)); + $log[] = 3; + } + ); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED)) + ->never(); + + $package->connect(Package::new($this->stubProperties('connected', true))); + $package->build(); + + static::assertSame(range(0, 3), $log); + static::assertCount(1, $package->connectedPackages()); + } + + /** + * @test + */ + public function testItFailsWhenCallingBootFromInitHookDebugOff(): void + { + $package = Package::new($this->stubProperties('test', false)); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT)) + ->once() + ->whenHappen([$package, 'boot']); + + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->never(); + + $package->build(); + } + + /** + * @test + */ + public function testItFailsWhenCallingBootFromInitHookDebugOn(): void + { + $package = Package::new($this->stubProperties('test', true)); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT)) + ->once() + ->whenHappen([$package, 'boot']); + + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->never(); + + $this->expectExceptionMessageMatches('/boot/i'); + $package->build(); + } + + /** + * @test + */ + public function testItFailsWhenCallingBootFromInitializedHook(): void { - $moduleId = 'my-extension-module'; - $serviceId = 'service-id'; + $package = Package::new($this->stubProperties('test', true)); - $module = $this->stubModule($moduleId, ServiceModule::class, ExtendingModule::class); - $module->expects('services')->andReturn($this->stubServices($serviceId)); - $module->expects('extensions')->andReturn($this->stubServices($serviceId)); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED)) + ->once() + ->whenHappen([$package, 'boot']); - $package = Package::new($this->stubProperties())->addModule($module); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT))->once(); + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->never(); - static::assertTrue($package->boot()); - static::assertFalse($package->moduleIs($moduleId, Package::MODULE_NOT_ADDED)); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_REGISTERED)); - static::assertFalse($package->moduleIs($moduleId, Package::MODULE_REGISTERED_FACTORIES)); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXTENDED)); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); - static::assertTrue($package->container()->has($serviceId)); + $this->expectExceptionMessageMatches('/boot/i'); + $package->build(); } /** * @test */ - public function testBootWithExecutableModule(): void + public function testItFailsWhenCallingBootFromReadyHook(): void { - $moduleId = 'executable-module'; - $module = $this->stubModule($moduleId, ExecutableModule::class); - $module->expects('run')->andReturn(true); + $package = Package::new($this->stubProperties('test', true)); - $package = Package::new($this->stubProperties())->addModule($module); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED)) + ->once() + ->whenHappen([$package, 'boot']); - static::assertTrue($package->boot()); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXECUTED)); - static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXECUTION_FAILED)); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT))->once(); + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->once(); + + $this->expectExceptionMessageMatches('/boot/i'); + $package->boot(); } /** - * Test, when the ExecutableModule::run() return false, that the state is correctly set. - * * @test */ - public function testBootWithExecutableModuleFailed(): void + public function testItFailsWhenCallingBuildFromInitHook(): void { - $moduleId = 'executable-module'; - $module = $this->stubModule($moduleId, ExecutableModule::class); - $module->expects('run')->andReturn(false); + $package = Package::new($this->stubProperties('test', true)); - $package = Package::new($this->stubProperties())->addModule($module); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT)) + ->once() + ->whenHappen([$package, 'build']); - static::assertTrue($package->boot()); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_ADDED)); - static::assertFalse($package->moduleIs($moduleId, Package::MODULE_EXECUTED)); - static::assertTrue($package->moduleIs($moduleId, Package::MODULE_EXECUTION_FAILED)); + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->never(); + + $this->expectExceptionMessageMatches('/build/i'); + $package->build(); } /** * @test - * @runInSeparateProcess */ - public function testBootPassingModulesEmitDeprecation(): void + public function testItFailsWhenCallingBuildFromInitializedHook(): void { - $module1 = $this->stubModule('module_1', ServiceModule::class); - $module1->allows('services')->andReturn($this->stubServices('service_1')); - $package = Package::new($this->stubProperties('test', true)); - $this->convertDeprecationsToExceptions(); - try { - $count = 0; - $package->boot($module1); - } catch (\Throwable $throwable) { - $count++; - $this->assertThrowableMessageMatches($throwable, 'boot().+?deprecated.+?1\.7'); - } finally { - static::assertSame(1, $count); - } + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED)) + ->once() + ->whenHappen([$package, 'build']); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT))->once(); + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED))->never(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->never(); + + $this->expectExceptionMessageMatches('/build/i'); + $package->build(); } /** * @test */ - public function testAddModuleFailsAfterBuild(): void + public function testItFailsWhenCallingBuildFromReadyHook(): void { - $package = Package::new($this->stubProperties('test', true))->build(); + $package = Package::new($this->stubProperties('test', true)); + + Monkey\Actions\expectDone($package->hookName(Package::ACTION_BOOTED)) + ->once() + ->whenHappen([$package, 'build']); - $this->expectExceptionMessageMatches("/can't add module/i"); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INIT))->once(); + Monkey\Actions\expectDone(Package::ACTION_MODULARITY_INIT)->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_INITIALIZED))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD))->once(); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT))->once(); - $package->addModule($this->stubModule()); + $this->expectExceptionMessageMatches('/build/i'); + $package->boot(); } /** @@ -523,7 +1147,7 @@ public function testConnectBuiltPackageFromIdlePackage(): void Monkey\Actions\expectDone($package2->hookName(Package::ACTION_PACKAGE_CONNECTED)) ->once() - ->with($package1->name(), Package::STATUS_IDLE, true); + ->with($package1->name(), Package::STATUS_IDLE, false); $package1->build(); @@ -571,7 +1195,7 @@ public function testConnectBuiltPackageFromBuildPackageFailsDebugOff(): void $package1 = $this->stubSimplePackage('1'); $package2 = $this->stubSimplePackage('2'); - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECTION)) + Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECT)) ->once() ->with($package1->name(), \Mockery::type(\WP_Error::class)); @@ -589,7 +1213,7 @@ public function testConnectBuiltPackageFromBuildPackageFailsDebugOn(): void $package1 = $this->stubSimplePackage('1'); $package2 = $this->stubSimplePackage('2', true); - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECTION)) + Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECT)) ->once() ->with($package1->name(), \Mockery::type(\WP_Error::class)); @@ -608,7 +1232,7 @@ public function testConnectBuiltPackageFromBootedPackageFailsDebugOff(): void $package1 = $this->stubSimplePackage('1'); $package2 = $this->stubSimplePackage('2'); - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECTION)) + Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECT)) ->once() ->with($package1->name(), \Mockery::type(\WP_Error::class)); @@ -626,7 +1250,7 @@ public function testConnectBuiltPackageFromBootedPackageFailsDebugOn(): void $package1 = $this->stubSimplePackage('1'); $package2 = $this->stubSimplePackage('2', true); - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECTION)) + Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECT)) ->once() ->with($package1->name(), \Mockery::type(\WP_Error::class)); @@ -661,25 +1285,20 @@ public function testAccessingServicesFromIdleConnectedPackageFails(): void /** * @test */ - public function testPackageCanOnlyBeConnectedOnceDebugOff(): void + public function testPackageCanOnlyBeConnectedOnce(): void { - $package1 = $this->stubSimplePackage('1', true); - $package2 = $this->stubSimplePackage('2', false); + $package1 = $this->stubSimplePackage('1', false); + $package2 = $this->stubSimplePackage('2', true); Monkey\Actions\expectDone($package2->hookName(Package::ACTION_PACKAGE_CONNECTED)) ->once(); - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECTION)) + Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECT)) ->twice() ->with($package1->name(), \Mockery::type(\WP_Error::class)); Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_BUILD)) - ->once() - ->whenHappen( - function (\Throwable $throwable): void { - $this->assertThrowableMessageMatches($throwable, 'failed connect.+?already'); - } - ); + ->never(); static::assertTrue($package2->connect($package1)); static::assertTrue($package2->isPackageConnected($package1->name())); @@ -689,36 +1308,10 @@ function (\Throwable $throwable): void { static::assertFalse($package2->connect($package1)); static::assertTrue($package2->isPackageConnected($package1->name())); - } - - /** - * @test - */ - public function testPackageCanOnlyBeConnectedOnceDebugOn(): void - { - $package1 = $this->stubSimplePackage('1', false); - $package2 = $this->stubSimplePackage('2', true); - - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_PACKAGE_CONNECTED)) - ->once(); - - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_CONNECTION)) - ->once() - ->with($package1->name(), \Mockery::type(\WP_Error::class)); - - Monkey\Actions\expectDone($package2->hookName(Package::ACTION_FAILED_BUILD)) - ->once() - ->whenHappen( - function (\Throwable $throwable): void { - $this->assertThrowableMessageMatches($throwable, 'failed connect.+?already'); - } - ); - - static::assertTrue($package2->connect($package1)); - static::assertTrue($package2->isPackageConnected($package1->name())); - $this->expectExceptionMessageMatches('/already connected/i'); - $package2->connect($package1); + $package1->build(); + $package2->build(); + static::assertSame('service_1', $package2->container()->get('service_1')['id']); } /** @@ -728,7 +1321,7 @@ public function testPackageCanNotBeConnectedWithThemselves(): void { $package1 = $this->stubSimplePackage('1'); - $action = $package1->hookName(Package::ACTION_FAILED_CONNECTION); + $action = $package1->hookName(Package::ACTION_FAILED_CONNECT); Monkey\Actions\expectDone($action)->never(); static::assertFalse($package1->connect($package1)); @@ -772,133 +1365,6 @@ public function testGettingServicesFromBuiltConnectedPackage(): void static::assertSame('service_3', $package1->container()->get('service_3')['id']); } - /** - * @test - * - * phpcs:disable Inpsyde.CodeQuality.NestingLevel - */ - public function testBuildResolveServices(): void - { - // phpcs:enable phpcs:disable Inpsyde.CodeQuality.NestingLevel - $module = new class () implements ServiceModule, ExtendingModule, ExecutableModule - { - public function id(): string - { - return 'test-module'; - } - - public function services(): array - { - return [ - 'dependency' => static function (): object { - return (object) ['x' => 'Works!']; - }, - 'service' => static function (ContainerInterface $container): object { - $works = $container->get('dependency')->x; - - return new class (['works?' => $works]) extends \ArrayObject - { - }; - }, - ]; - } - - public function extensions(): array - { - return [ - 'service' => function (\ArrayObject $current): object { - return new class ($current) - { - public \ArrayObject $object; // phpcs:ignore - - public function __construct(\ArrayObject $object) - { - $this->object = $object; - } - - public function works(): string - { - return $this->object->offsetGet('works?'); - } - }; - }, - ]; - } - - public function run(ContainerInterface $container): bool - { - throw new \Error('This should not run!'); - } - }; - - $actual = Package::new($this->stubProperties()) - ->addModule($module) - ->build() - ->container() - ->get('service') - ->works(); - - static::assertSame('Works!', $actual); - } - - /** - * @test - */ - public function testBuildPassingModulesToBoot(): void - { - $module1 = $this->stubModule('module_1', ServiceModule::class); - $module1->expects('services')->andReturn($this->stubServices('service_1')); - - $module2 = $this->stubModule('module_2', ServiceModule::class); - $module2->expects('services')->andReturn($this->stubServices('service_2')); - - $module3 = $this->stubModule('module_3', ServiceModule::class); - $module3->expects('services')->andReturn($this->stubServices('service_3')); - - $package = Package::new($this->stubProperties('test', true)) - ->addModule($module1) - ->addModule($module2) - ->build(); - - $this->ignoreDeprecations(); - $package->boot($module2, $module3); - - $container = $package->container(); - - static::assertSame('service_1', $container->get('service_1')['id']); - static::assertSame('service_2', $container->get('service_2')['id']); - static::assertSame('service_3', $container->get('service_3')['id']); - } - - /** - * @test - */ - public function testBootFailsIfPassingNotAddedModulesAfterContainer(): void - { - $module1 = $this->stubModule('module_1', ServiceModule::class); - $module1->expects('services')->andReturn($this->stubServices('service_1')); - - $module2 = $this->stubModule('module_2', ServiceModule::class); - $module2->expects('services')->andReturn($this->stubServices('service_2')); - - $module3 = $this->stubModule('module_3', ServiceModule::class); - $module3->allows('services')->andReturn($this->stubServices('service_3')); - - $package = Package::new($this->stubProperties('test', true)) - ->addModule($module1) - ->addModule($module2) - ->build(); - - $container = $package->container(); - - static::assertSame('service_1', $container->get('service_1')['id']); - static::assertSame('service_2', $container->get('service_2')['id']); - - $this->expectExceptionMessageMatches("/can't add module module_3/i"); - $this->ignoreDeprecations(); - $package->boot($module2, $module3); - } - /** * When an exception happen inside `Package::boot()` and debug is off, we expect the exception * to be caught, an "boot failed" action to be failed, and the Package to be in errored status. @@ -969,7 +1435,7 @@ public function testFailureFlowWithFailureOnAddModuleDebugModeOff(): void $package = Package::new($this->stubProperties()); - Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD)) + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_ADD_MODULE)) ->once() ->whenHappen( static function (\Throwable $throwable) use ($exception, $package): void { @@ -978,6 +1444,15 @@ static function (\Throwable $throwable) use ($exception, $package): void { } ); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD)) + ->once() + ->whenHappen( + function (\Throwable $throwable) use ($package): void { + $this->assertThrowableMessageMatches($throwable, 'build package'); + static::assertTrue($package->statusIs(Package::STATUS_FAILED)); + } + ); + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BOOT)) ->once() ->whenHappen( @@ -986,7 +1461,7 @@ function (\Throwable $throwable) use ($exception, $package): void { $previous = $throwable->getPrevious(); $this->assertThrowableMessageMatches($previous, 'build package'); $previous = $previous->getPrevious(); - $this->assertThrowableMessageMatches($previous, 'add module two'); + $this->assertThrowableMessageMatches($previous, 'add module'); static::assertSame($exception, $previous->getPrevious()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); } @@ -1018,7 +1493,7 @@ public function testFailureFlowWithFailureOnAddModuleWithoutBuildDebugModeOff(): $connected = Package::new($this->stubProperties()); $connected->boot(); - Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_BUILD)) + Monkey\Actions\expectDone($package->hookName(Package::ACTION_FAILED_ADD_MODULE)) ->once() ->whenHappen( static function (\Throwable $throwable) use ($exception, $package): void { @@ -1033,11 +1508,7 @@ static function (\Throwable $throwable) use ($exception, $package): void { function (\Throwable $throwable) use ($exception, $package): void { $this->assertThrowableMessageMatches($throwable, 'boot application'); $previous = $throwable->getPrevious(); - $this->assertThrowableMessageMatches($previous, 'build package'); - $previous = $previous->getPrevious(); - $this->assertThrowableMessageMatches($previous, 'failed connect.+?errored'); - $previous = $previous->getPrevious(); - $this->assertThrowableMessageMatches($previous, 'add module two'); + $this->assertThrowableMessageMatches($previous, 'two'); static::assertSame($exception, $previous->getPrevious()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); } @@ -1045,7 +1516,6 @@ function (\Throwable $throwable) use ($exception, $package): void { $package = $package->addModule($module1)->addModule($module2); - static::assertFalse($package->connect($connected)); static::assertFalse($package->boot()); static::assertTrue($package->statusIs(Package::STATUS_FAILED)); static::assertTrue($package->hasFailed()); From 171dd96f8b58c2826cef5f3a539d7050cf3cc355 Mon Sep 17 00:00:00 2001 From: Niklas Gutberlet <70433191+InpsydeNiklas@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:53:45 +0200 Subject: [PATCH 3/5] Update minimum PHP version to 7.4 in README.md and /docs/README.md (#54) * Update minimum PHP version to 7.4 in README.md Signed-off-by: Niklas Gutberlet <70433191+InpsydeNiklas@users.noreply.github.com> * Update minimum PHP version to 7.4 in /docs/README.md Signed-off-by: Niklas Gutberlet <70433191+InpsydeNiklas@users.noreply.github.com> --------- Signed-off-by: Niklas Gutberlet <70433191+InpsydeNiklas@users.noreply.github.com> --- README.md | 2 +- docs/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d357e1f..88c4724 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ composer require inpsyde/modularity ## Minimum Requirements and Dependencies -* PHP 7.2+ +* PHP 7.4+ When installed for development via Composer, the package also requires: diff --git a/docs/README.md b/docs/README.md index a5b2bf9..9f88d73 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ composer require inpsyde/modularity ## Minimum Requirements and Dependencies -* PHP 7.2+ +* PHP 7.4+ When installed for development, via Composer, the package also requires: From 372a915198dac7e5a5edc5c2337c1aa3da698626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Mei=C3=9Felbach?= Date: Thu, 28 Nov 2024 10:32:19 +0100 Subject: [PATCH 4/5] Avoid loading translations too early (#56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Avoid loading translations too early Signed-off-by: Moritz Meißelbach * Update Properties.md Signed-off-by: Moritz Meißelbach * Fix typo Signed-off-by: Moritz Meißelbach * Only skip translations if loaded early. Add tests and improve docs * Revert "Only skip translations if loaded early." This reverts commit 2108ee299413385a5459c2cc5394f011d54571de. --------- Signed-off-by: Moritz Meißelbach --- docs/Properties.md | 3 ++- src/Properties/PluginProperties.php | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/Properties.md b/docs/Properties.md index aca1ad5..f4f98b7 100644 --- a/docs/Properties.md +++ b/docs/Properties.md @@ -77,7 +77,8 @@ Additionally, PluginProperties will have the following public API: - `PluginProperties::isNetworkActive(): bool` - returns if the current Plugin is network-wide active. - `PluginProperties::isMuPlugin(): bool` - returns if the current Plugin is a must-use Plugin. - +Please note that our usage of `get_plugin_data` opts out of translations and HTML-safe text processing (via `wptexturize`) offered by default. +These functions should not be used before the 'init' hook which may be too late for some applications. ## ThemeProperties diff --git a/src/Properties/PluginProperties.php b/src/Properties/PluginProperties.php index 9fa8fd3..5d72d8d 100644 --- a/src/Properties/PluginProperties.php +++ b/src/Properties/PluginProperties.php @@ -55,9 +55,10 @@ protected function __construct(string $pluginMainFile) } // $markup = false, to avoid an incorrect early wptexturize call. - // We also probably don't want HTML here anyway + // $translate = false, to avoid loading translations too early // @see https://core.trac.wordpress.org/ticket/49965 - $pluginData = (array) get_plugin_data($pluginMainFile, false); + // @see https://core.trac.wordpress.org/ticket/34114 + $pluginData = (array) get_plugin_data($pluginMainFile, false, false); $properties = Properties::DEFAULT_PROPERTIES; // Map pluginData to internal structure. From c79bb3682f55e1a2ece67f36e70d04fa2ab8c65d Mon Sep 17 00:00:00 2001 From: Alex Pantechovskis Date: Thu, 28 Nov 2024 11:34:00 +0200 Subject: [PATCH 5/5] Rename to Syde (#55) Signed-off-by: Alex Pantechovskis --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5957dd4..11dff38 100644 --- a/composer.json +++ b/composer.json @@ -5,9 +5,9 @@ "license": "GPL-2.0-or-later", "authors": [ { - "name": "Inpsyde GmbH", - "email": "hello@inpsyde.com", - "homepage": "https://inpsyde.com/", + "name": "Syde GmbH", + "email": "hello@syde.com", + "homepage": "https://syde.com/", "role": "Company" } ],