Skip to content

Commit

Permalink
Merge branch 'master' into feature/remove-deprecated-default-modules
Browse files Browse the repository at this point in the history
  • Loading branch information
tfrommen committed Jan 8, 2025
2 parents 2b1e7b4 + c79bb36 commit 9da3a20
Show file tree
Hide file tree
Showing 13 changed files with 1,201 additions and 565 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
],
Expand Down
96 changes: 42 additions & 54 deletions docs/Application-flow.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,88 @@
# 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.



## The "boot" phase

## Booting stage
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.
3. If the `Package`'s `Properties` instance is in "debug mode" (`Properties::isDebug()` returns `true`), the exception bubbles up, and the flow stops here.
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.
3. If the `Package`'s `Properties` instance is in "debug mode" (`Properties::isDebug()` returns `true`), the exception bubbles up, and the flow stops here.
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:

```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());
```

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.

In fact, after `Package::build()` is called the application status is at `Package::STATUS_INITIALIZED`, and no more modules can be added.

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.

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.
Loading

0 comments on commit 9da3a20

Please sign in to comment.