Skip to content

Commit 0bde4d1

Browse files
gmazzapChricoesurov
authored
Introduce Package::build() (#33)
* Introduce Package::boot() The new method allows building a container out of the package without running any `ExecutableModule::run()`, simplifying unit tests. Passing default modules to `Package::boot()` is now deprecated, for a better separation of the "building" and "booting" steps, but it continues to work while emitting a deprecation notice. There's an edge case in which passing modules to `Package::boot()` causes an exception, but one of the conditions is that `Package::container()` was called before `Package::boot()` which caused an exception before anyway, so the change is 100% backward compatible. Two new package statuses have been added: - `Package::STATUS_MODULES_ADDED` - `Package::STATUS_READY` The first is necessary to distinguish the status after `build()` was called but `boot()` was not. The second was a missing status between initialized and ready. Documentation and tests were added. * Modernize tests code * Fix tests * Commit @Biont suggestions * Move Package's ready status before the ready action * Do not accept default modules as argument to Package::boot() Props @Chrico Also, make sure failures inside `build()` are caught the same way they are in `boot()`, and introduce a "failed build" action hook as the counterpart of the "failed boot" action hook. * Catch failures in `addModule()` and `connect()` Implement a uniform "failure flow", catching all the errors when debug is off, and collecting them in a Throwable's "previous" hierarchy. All the application flows are documented in a separate document. Fix and add new tests for all the flows. * Applicaton-flow.md // fix wrong formatting Signed-off-by: Christian Leucht <3417446+Chrico@users.noreply.github.com> * Use Package::statusIs() instead of === comparison * Update for phpunit (#31) * Update for phpunit This allows to update phpunit/phpunit with it's dependencies to 8 to 9 and correctly work with PHP8.* * phpunit 8 & 9 To satisfy PHP7.2 requirement we need to have dual version of PHP. phpunit-update * Remove unnecessary asset * Increase test coverage, adapt to PHPUnit 9 --------- Signed-off-by: Christian Leucht <3417446+Chrico@users.noreply.github.com> Co-authored-by: Christian Leucht <3417446+Chrico@users.noreply.github.com> Co-authored-by: Gene Surov <esurov@gmail.com>
1 parent cc91569 commit 0bde4d1

12 files changed

Lines changed: 1131 additions & 256 deletions

File tree

.github/workflows/php-unit-tests.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
uses: actions/checkout@v3
2626

2727
- name: Use coverage?
28-
if: ${{ (matrix.php-versions == '7.4') && (matrix.dependency-versions == 'highest') && (matrix.container-versions == '^2') }}
28+
if: ${{ (matrix.php-versions == '8.0') && (matrix.dependency-versions == 'highest') && (matrix.container-versions == '^2') }}
2929
run: echo "USE_COVERAGE=yes" >> $GITHUB_ENV
3030

3131
- name: Setup PHP
@@ -46,7 +46,9 @@ jobs:
4646
dependency-versions: ${{ matrix.dependency-versions }}
4747

4848
- name: Run unit tests
49-
run: composer tests:${{ ((env.USE_COVERAGE == 'yes') && 'codecov') || 'no-cov' }}
49+
run:
50+
./vendor/bin/phpunit --atleast-version 9 && ./vendor/bin/phpunit --migrate-configuration || echo 'Config does not need updates.'
51+
./vendor/bin/phpunit ${{ ((env.USE_COVERAGE == 'yes') && '--coverage-clover coverage.xml') || '--no-coverage' }}
5052

5153
- name: Update coverage
5254
if: ${{ env.USE_COVERAGE == 'yes' }}

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
"@psalm"
6363
],
6464
"tests": "@php ./vendor/phpunit/phpunit/phpunit",
65-
"tests:codecov": "@php ./vendor/phpunit/phpunit/phpunit --coverage-clover coverage.xml",
6665
"tests:no-cov": "@php ./vendor/phpunit/phpunit/phpunit --no-coverage"
6766
},
6867
"config": {

docs/Applicaton-flow.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# The application flow
2+
3+
Modularity implements its application flow in two stages:
4+
5+
- First, the application's dependencies tree is "composed" by collecting services declared in modules, adding sub-containers, and connecting other applications.
6+
- After that, the application dependency tree is locked, and the services are "consumed" to execute their behavior.
7+
8+
The `Package` class implements the two stages above, respectively, in the two methods:
9+
10+
- **`Package::build()`**
11+
- **`Package::boot()`**
12+
13+
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):
14+
15+
```php
16+
Package::new($properties)->build()->boot();
17+
```
18+
19+
is entirely equivalent to the following:
20+
21+
```php
22+
Package::new($properties)->boot();
23+
```
24+
25+
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.
26+
27+
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.
28+
29+
30+
## Building stage
31+
32+
1. Upon instantiation, the `Package` status is at **`Package::STATUS_IDLE`**
33+
2. Default modules can be added by calling **`Package::addModule()`** on the instance.
34+
3. The **`Package::ACTION_INIT`** action hook is fired, passing the package instance as an argument. That allows external code to add modules.
35+
4. The `Package` status moves to **`Package::STATUS_INITIALIZED`**. The "building" stage is completed, and no more modules can be added.
36+
37+
38+
## Booting stage
39+
40+
1. When the booting stage begins, the `Package` status moves to **`Package::STATUS_MODULES_ADDED`**.
41+
2. A read-only PSR-11 container is created. It can lazily resolve the dependency tree defined in the previous stage.
42+
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.
43+
4. The `Package` status moves to **`Package::STATUS_READY`**.
44+
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.
45+
6. The `Package` status moves to **`Package::STATUS_BOOTED`**. The booting stage is completed. `Package::boot()` returns true.
46+
47+
48+
## The "failure flow"
49+
50+
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.
51+
52+
### When the failure starts during the "building" stage
53+
54+
1. The `Package` status moves to **`Package::STATUS_FAILED`**.
55+
2. The **`Package::ACTION_FAILED_BUILD`** action hook is fired, passing the raised `Throwable` as an argument.
56+
3. If the `Package`'s `Properties` instance is in "debug mode" (`Properties::isDebug()` returns `true`), the exception bubbles up, and the flow stops here.
57+
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.
58+
5. `Package::boot()` returns false.
59+
60+
### When the failure starts during the "booting" stage
61+
62+
1. The `Package` status moves to **`Package::STATUS_FAILED`**.
63+
2. The **`Package::ACTION_FAILED_BOOT`** action hook is fired, passing the raised `Throwable` as an argument.
64+
3. If the `Package`'s `Properties` instance is in "debug mode" (`Properties::isDebug()` returns `true`), the exception bubbles up, and the flow stops here.
65+
4. `Package::boot()` returns false.
66+
67+
68+
## A note about default modules passed to boot()
69+
70+
The `Package::boot()` method accepts a list of modules. That has been deprecated since Modularity v1.7.
71+
72+
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.
73+
74+
When `Package::boot()` is called without calling `Package::build()` first, as in:
75+
76+
```php
77+
Package::new($properties)->boot(new ModuleOne(), new ModuleTwo());
78+
```
79+
80+
The code is equivalent to the following:
81+
82+
```php
83+
Package::new($properties)->addModule(new ModuleOne())->addModule(new ModuleTwo())->boot();
84+
```
85+
86+
So the "building" flow is respected.
87+
88+
However, when `Package::boot()` is called after `Package::build()`, as in:
89+
90+
```php
91+
Package::new($properties)->build()->boot(new ModuleOne(), new ModuleTwo());
92+
```
93+
94+
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.
95+
96+
In fact, after `Package::build()` is called the application status is at `Package::STATUS_INITIALIZED`, and no more modules can be added.
97+
98+
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.
99+
100+
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.

docs/Package.md

Lines changed: 91 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Retrieve the current status of the Application. Following are available:
5656

5757
- `Package::STATUS_IDLE` - before Application is booted.
5858
- `Package::STATUS_INITIALIZED` - after first init action is triggered.
59+
- `Package::STATUS_MODULES_ADDED` - after all modules have been added.
60+
- `Package::STATUS_READY` - after the "ready" action has been fired.
5961
- `Package::STATUS_BOOTED` - Application has successfully booted.
6062
- `Package::STATUS_FAILED_BOOT` - when Application did not boot properly.
6163

@@ -100,7 +102,7 @@ add_action(
100102
);
101103
```
102104

103-
By providing the `Acme\plugin()`-function, you’ll enable externals to hook into your Application:
105+
By providing the `Acme\plugin()` function, you’ll enable external code to hook into your application:
104106

105107
```php
106108
<?php
@@ -123,6 +125,94 @@ add_action(
123125
);
124126
```
125127

128+
## Building the package
129+
130+
Sometimes, especially in unit tests, it might be desirable to obtain services as defined for the
131+
production code, but without calling any `ExecutableModule::run()`, which usually contains
132+
WP-dependant code, and therefore requires heavy mocking.
133+
134+
For example, assuming a common `plugin()` function like the following:
135+
136+
```php
137+
function plugin(): Modularity\Package {
138+
static $package;
139+
if (!$package) {
140+
$properties = Modularity\Properties\PluginProperties::new(__FILE__);
141+
$package = Modularity\Package::new($properties)
142+
->addModule(new ModuleOne())
143+
->addModule(new ModuleTwo())
144+
}
145+
return $package;
146+
}
147+
```
148+
149+
In unit test it will be possible (as of v1.7+) to do something like the following:
150+
151+
```php
152+
$myService = plugin()->build()->container()->get(MyService::class);
153+
static::assertTrue($myService->isValid());
154+
```
155+
156+
### Booting a built container
157+
158+
The `Package::boot()` method can be called on already built package.
159+
160+
For example, the following is a valid unit test code:
161+
162+
```php
163+
$plugin = plugin()->build();
164+
$myService = $plugin->container()->get(MyService::class);
165+
166+
static::assertTrue($myService->isValid());
167+
static::assertFalse($myService->isBooted());
168+
169+
$plugin->boot();
170+
171+
static::assertTrue($myService->isBooted());
172+
```
173+
174+
### Deprecated boot parameters
175+
176+
Before Modularity v1.7.0, it was an accepted practice to pass default modules to `Package::boot()`,
177+
as in:
178+
179+
```php
180+
add_action(
181+
'plugins_loaded',
182+
static function(): void {
183+
plugin()->boot(new ModuleOne(), new ModuleTwo());
184+
}
185+
);
186+
```
187+
188+
This is now deprecated to allow a better separation of the "building" and "booting" steps.
189+
190+
While it still works (and it will work up to version 2.0), it will emit a deprecation notice.
191+
192+
The replacement is using `Package::addModule()`:
193+
194+
```php
195+
plugin()->addModule(new ModuleOne())->addModule(new ModuleTwo())->boot();
196+
```
197+
198+
There's only one case in which calling `Package::boot()` with default modules will throw an
199+
exception (besides triggering a deprecated notice), that is when a passed modules was not added
200+
before `Package::addModule()` and an instance of the container was already obtained from the package.
201+
202+
For example, this will throw an exception:
203+
204+
```php
205+
$plugin = plugin()->build();
206+
207+
// Now that container is built, passing modules to `boot()` will raise an exception, because we
208+
// can't add new modules to an already "compiled" container being that read-only.
209+
$container = $plugin->container();
210+
211+
$plugin->boot(new ModuleOne());
212+
```
213+
214+
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()`.
215+
In this latter case the exception is not thrown, but the deprecation will still be emitted.
126216

127217

128218
## Connecting packages
@@ -180,39 +270,3 @@ Thanks to that, all plugins will be able to access the library's services in the
180270
### Accessing connected packages' properties
181271

182272
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)`.
183-
184-
185-
186-
## What happens on Package::boot()?
187-
188-
When booting your Application, following will happen inside:
189-
190-
**0. Package::statusIs(Package::STATUS_IDLE);**
191-
192-
Application is idle and ready to start.
193-
194-
**1. Register default Modules**
195-
196-
Default Modules which are injected before `Package::boot()` will be registered first by iterating over all Modules and calling `Package::addModule()`.
197-
198-
**2. Package::ACTION_INIT**
199-
200-
A custom WordPress action will be triggered first to allow registration of additional Modules via `Package::addModule()` by accessing the `Package`-class. Application will change into `Package::STATUS_INITIALIZED` afterwards.
201-
202-
Newly registered Modules via that hook will be executed after the default Modules which are injected before the `Package::boot()`-method.
203-
204-
**3. Compile read-only Container**
205-
206-
The default primary PSR-Container is generated by the ContainerConfigurator by injecting all Factories, Extension and child PSR-Containers into it.
207-
208-
**4. Execute all ExecutableModules**
209-
210-
After collecting all ExecutableModules, the Package-class will now iterate over all ExecutableModules and execute them by injecting the default primary PSR-Container.
211-
212-
**5. Package::ACTION_READY**
213-
214-
Last but not least, `Package::boot()` will trigger custom WordPress Action which allows you to access the Package-class again for the purpose of debugging all Modules.
215-
216-
**6. Done**
217-
218-
The package was either successfully booted and state changed to `Package::STATUS_BOOTED` or failed booting due some exceptions and state was changed to `Package::STATUS_FAILED_BOOT`.

phpunit.xml.dist

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.0/phpunit.xsd"
2+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
33
bootstrap="./tests/boot.php"
44
colors="true"
55
convertErrorsToExceptions="true"
66
convertNoticesToExceptions="true"
77
convertWarningsToExceptions="true"
8+
convertDeprecationsToExceptions="false"
89
backupGlobals="false"
910
stopOnFailure="false">
1011
<filter>

src/Container/PackageProxyContainer.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function __construct(Package $package)
3636
*/
3737
public function get(string $id)
3838
{
39-
assert(is_string($id));
4039
$this->assertPackageBooted($id);
4140

4241
return $this->container->get($id);
@@ -50,8 +49,6 @@ public function get(string $id)
5049
*/
5150
public function has(string $id): bool
5251
{
53-
assert(is_string($id));
54-
5552
return $this->tryContainer() && $this->container->has($id);
5653
}
5754

@@ -67,7 +64,11 @@ private function tryContainer(): bool
6764
return true;
6865
}
6966

70-
if ($this->package->statusIs(Package::STATUS_BOOTED)) {
67+
/** TODO: We need a better way to deal with status checking besides equality */
68+
if (
69+
$this->package->statusIs(Package::STATUS_READY)
70+
|| $this->package->statusIs(Package::STATUS_BOOTED)
71+
) {
7172
$this->container = $this->package->container();
7273
}
7374

@@ -90,8 +91,8 @@ private function assertPackageBooted(string $id): void
9091

9192
$name = $this->package->name();
9293
$status = $this->package->statusIs(Package::STATUS_FAILED)
93-
? 'failed booting'
94-
: 'is not booted yet';
94+
? 'is errored'
95+
: 'is not ready yet';
9596

9697
throw new class ("Error retrieving service {$id} because package {$name} {$status}.")
9798
extends \Exception

0 commit comments

Comments
 (0)