Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

Foundation is a StellarWP Composer monorepo for reusable PHP packages intended for libraries and WordPress plugin ecosystems.

Initial packages:
Split packages:

- `stellarwp/foundation-container`
- `stellarwp/foundation-log`
- `stellarwp/foundation-pipeline`
- `stellarwp/foundation-shutdown`
- `stellarwp/foundation-wpcli`
- `stellarwp/foundation-cli`

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Foundation is a StellarWP Composer monorepo for reusable PHP packages intended f
- [stellarwp/foundation-container](https://github.com/stellarwp/foundation-container)
- [stellarwp/foundation-pipeline](https://github.com/stellarwp/foundation-pipeline)
- [stellarwp/foundation-log](https://github.com/stellarwp/foundation-log)
- [stellarwp/foundation-shutdown](https://github.com/stellarwp/foundation-shutdown)
- [stellarwp/foundation-wpcli](https://github.com/stellarwp/foundation-wpcli)
- [stellarwp/foundation-cli](https://github.com/stellarwp/foundation-cli)

Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"stellarwp/foundation-container": "self.version",
"stellarwp/foundation-log": "self.version",
"stellarwp/foundation-pipeline": "self.version",
"stellarwp/foundation-shutdown": "self.version",
"stellarwp/foundation-wpcli": "self.version"
},
"minimum-stability": "dev",
Expand All @@ -47,6 +48,7 @@
"StellarWP\\Foundation\\Container\\": "src/Container/",
"StellarWP\\Foundation\\Log\\": "src/Log/",
"StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/",
"StellarWP\\Foundation\\Shutdown\\": "src/Shutdown/",
"StellarWP\\Foundation\\WPCli\\": "src/WPCli/"
},
"exclude-from-classmap": [
Expand Down
7 changes: 7 additions & 0 deletions src/Shutdown/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Path-based git attributes
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html

# Ignore paths when git creates an archive of this package
.gitattributes export-ignore
.gitignore export-ignore
.github export-ignore
13 changes: 13 additions & 0 deletions src/Shutdown/.github/workflows/close-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Close Pull Request

on:
pull_request_target:
types: [opened]

jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: "This is a read-only repository. Please submit your PR on the https://github.com/stellarwp/foundation repository.<br><br>Thanks!"
Comment thread
defunctl marked this conversation as resolved.
2 changes: 2 additions & 0 deletions src/Shutdown/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vendor/
composer.lock
10 changes: 10 additions & 0 deletions src/Shutdown/Contracts/ShutdownRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Shutdown\Contracts;

/**
* Runs application shutdown work at a lifecycle boundary.
*/
interface ShutdownRunner extends Terminable
{
}
11 changes: 11 additions & 0 deletions src/Shutdown/Contracts/Terminable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Shutdown\Contracts;

/**
* Work that should run when an application reaches its termination boundary.
*/
interface Terminable
{
public function terminate(): void;
}
136 changes: 136 additions & 0 deletions src/Shutdown/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Foundation Shutdown

> [!WARNING]
> **This is a read-only repository!** For pull requests or issues, see [stellarwp/foundation](https://github.com/stellarwp/foundation).

Run application termination work once, in a predictable order, without allowing
one failed task to prevent the remaining tasks from running.

## Installation

```shell
composer require stellarwp/foundation-shutdown
```

## Register the provider

Register `ShutdownProvider` through your application's normal Foundation provider
list:

```php
use StellarWP\Foundation\Shutdown\ShutdownProvider;

private array $providers = [
ShutdownProvider::class,
];
```

The provider has no custom constructor and uses the application's existing
Foundation container and configuration. Package installation alone has no side
effects; consumers may omit this provider and construct the public runner directly
or supply their own provider.

## Create and contribute tasks

Termination work implements the small `Terminable` contract:

```php
use StellarWP\Foundation\Shutdown\Contracts\Terminable;

final class FlushTelemetry implements Terminable
{
public function terminate(): void {
// Flush bounded application telemetry.
}
}

final class CloseRequestLog implements Terminable
{
public function terminate(): void {
// Close the request log after the response is sent.
}
}
```

Contribute an application's termination work from one provider. Resolve the
concrete tasks lazily so all providers can finish registering before termination
services are constructed. Contributions must be registered before the runner is
resolved:

```php
use lucatume\DI52\Container;
use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Shutdown\ShutdownProvider as FoundationShutdownProvider;
use StellarWP\Foundation\Shutdown\ShutdownTask;

final class ApplicationShutdownProvider extends Provider
{
public function register(): void {
$this->container->singleton(CloseRequestLog::class);
$this->container->singleton(FlushTelemetry::class);

$this->container->mergeArrayVar(
FoundationShutdownProvider::TASKS,
static fn (Container $container): array => [
new ShutdownTask($container->get(CloseRequestLog::class), 10),
new ShutdownTask($container->get(FlushTelemetry::class), 100),
]
);
}
}
```

Register both providers through the application's provider list:

```php
private array $providers = [
FoundationShutdownProvider::class,
ApplicationShutdownProvider::class,
];
```

Lower priority values run first. Tasks with the same priority retain their
registration order.

## WordPress shutdown

`ShutdownProvider` binds the `ShutdownRunner` contract to the ordered task runner,
decorated by `ResponseFinishingRunner`, and attaches it to WordPress's `shutdown`
action at the latest priority. When supported, it finishes the response with
`fastcgi_finish_request()` or `litespeed_finish_request()` before running the
contributed tasks. The PHP worker remains occupied until those tasks finish, so
long-running work still belongs in a proper background queue.

The runner is resolved lazily when the action fires, so features may contribute
tasks after the provider is registered.

With the default provider registered, applications may also invoke the configured
runner chain directly:

```php
use StellarWP\Foundation\Shutdown\Contracts\ShutdownRunner;

$container->get(ShutdownRunner::class)->terminate();
```

Applications that omit the default provider must bind the `ShutdownRunner` contract
in their own provider or construct the concrete
`StellarWP\Foundation\Shutdown\ShutdownRunner` with their desired tasks.

Each runner instance executes only once, including when termination is invoked
recursively. A `Throwable` from one task is isolated so later tasks still run.

## Logging

`ShutdownRunner` accepts an optional PSR-3 logger. When the application binds a
`Psr\Log\LoggerInterface`—including through `foundation-log`—the container injects
it automatically. Applications without a logger require no additional setup.

The runner logs the task count and each task at `debug` level. Task failures are
logged at `error` level with the task class, priority, and actual exception so
compatible loggers retain its message and stack trace. Logger failures are isolated
so diagnostics cannot interrupt termination work.

Output-buffer management, hard task timeouts, and asynchronous execution beyond
the default WordPress shutdown action belong to the consuming application or a
dedicated framework integration.
43 changes: 43 additions & 0 deletions src/Shutdown/ResponseFinishingRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Shutdown;

use StellarWP\Foundation\Shutdown\Contracts\ShutdownRunner;
use Throwable;

/**
* Finishes the current HTTP response before running termination work.
*/
final class ResponseFinishingRunner implements ShutdownRunner
{
private bool $terminated = false;

public function __construct(
private readonly ShutdownRunner $runner
) {
}

public function terminate(): void {
if ($this->terminated) {
return;
}

$this->terminated = true;

foreach (['fastcgi_finish_request', 'litespeed_finish_request'] as $finishRequest) {
if (! function_exists($finishRequest)) {
continue;
}

try {
if ($finishRequest()) {
break;
}
} catch (Throwable) {
// Response finishing is best-effort and must not block termination work.
}
}

$this->runner->terminate();
}
}
42 changes: 42 additions & 0 deletions src/Shutdown/ShutdownProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Shutdown;

use lucatume\DI52\Container;
use StellarWP\Foundation\Container\ContainerAdapter;
use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Shutdown\Contracts\ShutdownRunner as ShutdownRunnerContract;

/**
* Registers the default shutdown runner and task contribution point.
*
* @property-read ContainerAdapter $container
*/
final class ShutdownProvider extends Provider
{
public const string TASKS = self::class . '.tasks';
private const string REGISTERED = self::class . '.registered';

public function register(): void {
if ($this->container->has(self::REGISTERED)) {
return;
}

$this->container->singleton(self::REGISTERED, true);

$this->container->when(ShutdownRunner::class)
->needs('$tasks')
->give(static fn (Container $container): array => $container->getVar(self::TASKS, []));

$this->container->singletonDecorators(ShutdownRunnerContract::class, [
ResponseFinishingRunner::class,
ShutdownRunner::class,
]);

add_action(
'shutdown',
$this->container->callback(ShutdownRunnerContract::class, 'terminate'),
PHP_INT_MAX
);
}
}
Loading