Shutdown
Some work must happen before a PHP request ends, but does not need to delay the response sent to the client. For example, an application may need to flush buffered logs or telemetry to a third-party service, close request-scoped resources, or publish diagnostics collected during the request. Running that work before returning the response makes the website or API feel slower even though the result does not affect what the client receives.
Foundation Shutdown lets features contribute these end-of-request tasks from their providers. In WordPress, it attempts to finish the HTTP response before running the tasks from the shutdown action. Each task runs once in priority order, and one failed task does not prevent the remaining tasks from running.
Use shutdown tasks for bounded, best-effort work that can finish within the current PHP process. They are not asynchronous jobs: the PHP worker remains occupied until every task finishes.
Installation
Section titled “Installation”Install the runtime package:
Prepare the application
Section titled “Prepare the application”Shutdown tasks use the application’s existing container and ordered provider graph:
Configuration
Section titled “Configuration”Configure the cache write
Section titled “Configure the cache write”In the root config.php, choose the WordPress transient key and lifetime used for the cached snapshot:
Create a cache-write task
Section titled “Create a cache-write task”Create src/Product_Cache/Product_Cache_Writer.php. This example assumes Product_Cache_Buffer collects an in-memory snapshot while the application handles the request. The task writes that prepared snapshot to a WordPress transient only when it changed:
The buffer is responsible for collecting cacheable state during the request. The shutdown task only performs the final bounded write. The PHP worker remains occupied until terminate() returns, even when the response has already been sent to the client.
Contribute the task from its feature provider
Section titled “Contribute the task from its feature provider”In src/Product_Cache/Provider.php, supply the task’s scalar configuration before adding it lazily to ShutdownProvider::TASKS:
The registration guard prevents duplicate task contributions. Register the buffer as a singleton so application services and the shutdown writer receive the same instance. Data collected during the request is then available when the writer is resolved at shutdown. The contextual bindings supply the writer’s $cache_key and $ttl constructor arguments.
Task is an immutable value object pairing one Terminable service with its priority. Lower values run first. Tasks with the same priority retain provider contribution order.
Register the providers
Section titled “Register the providers”In src/App.php, register ShutdownProvider before the feature provider that contributes the cache task:
The shutdown provider registers the shared task collection, the decorated runner, and one callback on WordPress’s shutdown action at PHP_INT_MAX. Register it after WordPress’s hook API is available, such as during plugin or MU-plugin loading. Registration alone does not construct or run contributed tasks.
The callback checks WP_UNINSTALL_PLUGIN and wp_installing() when shutdown fires. If either excludes the request, it returns before resolving the runner, constructing tasks, or finishing the response. This also covers uninstall or installation state that begins after provider registration. If installation mode ends before shutdown, automatic termination can run.
If the application uses foundation-log, register LogProvider with the other infrastructure providers before the feature providers. The shutdown runner receives the configured LoggerInterface automatically.
Register every contributing provider before resolving the shutdown runner. The normal application bootstrap does this automatically because the runner is resolved only when the WordPress action fires.
Run tasks at WordPress shutdown
Section titled “Run tasks at WordPress shutdown”No application hook is required after ShutdownProvider is registered. At WordPress shutdown, Foundation first skips requests where WP_UNINSTALL_PLUGIN is defined or wp_installing() is true. For eligible requests, it:
- Resolves the complete contributed task collection.
- Attempts
fastcgi_finish_request()and thenlitespeed_finish_request()when available, stopping after one succeeds. - Runs tasks from the lowest priority to the highest.
- Runs equal-priority tasks in contribution order.
- Ignores repeated or recursive calls to the same runner instance.
Response finishing is best effort. A missing function, a false result, or a thrown exception does not prevent termination tasks from running.
Invoke the runner from another lifecycle
Section titled “Invoke the runner from another lifecycle”An application with another explicit termination boundary can resolve the configured contract directly:
Each runner instance executes only once. Calling it before WordPress shutdown means the later shutdown callback cannot repeat its work. Direct terminate() calls bypass the automatic callback’s installation and uninstall checks; the caller owns eligibility for its explicit lifecycle.
Handle task failures
Section titled “Handle task failures”The runner catches every Throwable from a task and continues with the remaining tasks. When a PSR-3 logger is available, it records task execution at debug and task failures at error, including the task class, priority, and exception. Logger failures are also isolated.
Testing
Section titled “Testing”Test each Terminable service directly through its observable behavior. Add one provider integration test when the contribution itself matters: register ShutdownProvider and the feature provider, resolve the ShutdownRunner contract, call terminate(), and assert the task’s effect.
To test automatic WordPress execution, fire the shutdown action and restore the test’s WordPress hooks afterward. Verify that excluded requests never resolve the runner, including when installation or uninstall state changes after registration.
The package already tests priority ordering, equal-priority stability, once-only execution, recursive invocation, response-finishing fallbacks, failure isolation, and optional logging. Application tests do not need to duplicate those generic guarantees.