Framework Documentation - Application Package
Lifecycle and events
The lifecycle
AbstractApplication::execute() is the base sequence:
dispatch application.before_execute
doExecute() ← your code
dispatch application.after_execute
and if anything above throws a Throwable:
dispatch application.error
AbstractWebApplication::execute() extends it:
try {
dispatch application.before_execute
doExecute() ← your code
dispatch application.after_execute
compress() ← only when the `gzip` config key is truthy
} catch (Throwable) {
dispatch application.error
}
dispatch application.before_respond
respond() ← status line, headers, body
dispatch application.after_respond
Two things follow from that shape:
- The response is always sent.
before_respond,respond()andafter_respondare outside the
try, so they run even after an error. A listener onapplication.errorcan therefore still set
a status code and a body, and the client will receive it. - A throw inside
respond()is not caught. OnlydoExecute()andcompress()are protected.
The events
All five names are constants on Joomla\Application\ApplicationEvents:
| Constant | Name | Payload | Dispatched |
|---|---|---|---|
BEFORE_EXECUTE |
application.before_execute |
ApplicationEvent |
Before doExecute() |
AFTER_EXECUTE |
application.after_execute |
ApplicationEvent |
After doExecute() returns normally |
ERROR |
application.error |
ApplicationErrorEvent |
When doExecute() or compress() throws |
BEFORE_RESPOND |
application.before_respond |
ApplicationEvent |
Web applications only, before the response is sent |
AFTER_RESPOND |
application.after_respond |
ApplicationEvent |
Web applications only, after the response is sent |
ApplicationEvent::getApplication() returns the AbstractApplication.
ApplicationErrorEvent adds getError(): Throwable and setError(Throwable): void.
Attaching listeners
The application uses Joomla\Event\DispatcherAwareTrait, so a dispatcher is optional:
use Joomla\Application\ApplicationEvents;
use Joomla\Application\Event\ApplicationEvent;
use Joomla\Event\Dispatcher;
$dispatcher = new Dispatcher();
$dispatcher->addListener(
ApplicationEvents::BEFORE_RESPOND,
static function (ApplicationEvent $event): void {
$event->getApplication()->setHeader('X-Frame-Options', 'DENY');
}
);
$app->setDispatcher($dispatcher);When no dispatcher has been set, AbstractApplication::dispatchEvent() catches the
UnexpectedValueException from getDispatcher() and returns null. Events are then simply not
dispatched — the application still runs.
Subscribers
For anything beyond a one-liner, use a subscriber:
use Joomla\Application\ApplicationEvents;
use Joomla\Application\Event\ApplicationEvent;
use Joomla\Event\Priority;
use Joomla\Event\SubscriberInterface;
final class SecurityHeadersSubscriber implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ApplicationEvents::BEFORE_RESPOND => ['onBeforeRespond', Priority::HIGH],
];
}
public function onBeforeRespond(ApplicationEvent $event): void
{
$app = $event->getApplication();
$app->setHeader('X-Content-Type-Options', 'nosniff');
$app->setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
$app->setHeader('X-Frame-Options', 'DENY');
}
}
$dispatcher->addSubscriber(new SecurityHeadersSubscriber());Listeners run in descending priority order. A listener that calls $event->stopPropagation()
prevents the remaining listeners for that event from running.
A listener that throws inside
before_respondorafter_respondis not caught by
execute()— those dispatches sit outside thetry. Guard listeners that can fail.
Error handling
The package dispatches application.error and does nothing else. There is no default error page,
no logging of the throwable, and no status code is set. Without a listener the client receives
whatever was in the response body at the time — usually nothing, with status 200.
A minimal but complete error listener:
use Joomla\Application\ApplicationEvents;
use Joomla\Application\Event\ApplicationErrorEvent;
use Joomla\Router\Exception\MethodNotAllowedException;
use Joomla\Router\Exception\RouteNotFoundException;
$dispatcher->addListener(
ApplicationEvents::ERROR,
static function (ApplicationErrorEvent $event) use ($debug): void {
$app = $event->getApplication();
$error = $event->getError();
$app->getLogger()->error(
$error->getMessage(),
['exception' => $error]
);
$status = match (true) {
$error instanceof RouteNotFoundException => 404,
$error instanceof MethodNotAllowedException => 405,
default => 500,
};
$app->setHeader('Status', (string) $status, true);
$app->setBody(
$debug
? '<pre>' . htmlspecialchars((string) $error, ENT_QUOTES, 'UTF-8') . '</pre>'
: '<h1>Something went wrong</h1>'
);
}
);Note the htmlspecialchars() in the debug branch. Exception messages routinely contain request
data; writing them into the response unescaped is a reflected XSS vector. Never send the message
at all outside of debug mode — it also leaks paths, SQL fragments and class names.
Logging
AbstractApplication implements Psr\Log\LoggerAwareInterface. getLogger() lazily installs a
NullLogger if none was set, so it is always safe to call:
$app->setLogger(new Monolog\Logger('app'));
$app->getLogger()->info('Request handled', ['route' => $app->get('uri.route')]);Closing the application
close($code = 0) calls exit($code). It is called by redirect() after the response has been
sent. Override it in tests so a test run does not terminate:
final class TestApplication extends WebApplication
{
public array $closed = [];
public function close($code = 0): void
{
$this->closed[] = $code;
}
}