Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make AccessLogHandler part of public API #174

Merged
merged 1 commit into from
Jul 22, 2022
Merged
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
52 changes: 52 additions & 0 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,55 @@ leaking too much internal information.
If you want to implement custom error handling, you're recommended to either
catch any exceptions your own or use a custom [middleware handler](middleware.md)
to catch any exceptions in your application.

## Access log

If you're using X with its [built-in web server](../best-practices/deployment.md#built-in-web-server),
it will log all requests and responses to console output (`STDOUT`) by default.

```bash
$ php public/index.php
2023-07-21 17:30:03.617 Listening on http://0.0.0.0:8080
2023-07-21 17:30:03.725 127.0.0.1 "GET / HTTP/1.1" 200 13 0.000
2023-07-21 17:30:03.742 127.0.0.1 "GET /unknown HTTP/1.1" 404 956 0.000
```

> ℹ️ **Framework X runs anywhere**
>
> This example uses the efficient built-in web server written in pure PHP.
> We also support running behind traditional web server setups like Apache,
> nginx, and more. If you're using X behind a traditional web server, X will not
> write an access log itself, but your web server of choice can be configured to
> write an access log instead.
> See [production deployment](../best-practices/deployment.md) for more details.

Internally, the `App` will automatically add a default access log handler by
adding the [`AccessLogHandler`](middleware.md#accessloghandler) to the list of
middleware used. You may also explicitly pass an [`AccessLogHandler`](middleware.md#accessloghandler)
middleware to the `App` like this:

```php title="public/index.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

$app = new FrameworkX\App(
new FrameworkX\AccessLogHandler(),
new FrameworkX\ErrorHandler()
);

// Register routes here, see routing…

$app->run();
```

> ⚠️ **Feature preview**
>
> Note that the [`AccessLogHandler`](middleware.md#accessloghandler) may
> currently only be passed as a global middleware instance and not as a global
> middleware name to the `App` and may not be used for individual routes.

If you pass an [`AccessLogHandler`](middleware.md#accessloghandler) to the `App`,
it must be followed by an [`ErrorHandler`](middleware.md#errorhandler) like in
the previous example. See also [error handling](#error-handling) for more
details.
13 changes: 13 additions & 0 deletions docs/api/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,19 @@ Global middleware handlers will always be called before route middleware handler

## Built-in middleware

### AccessLogHandler

> ⚠️ **Feature preview**
>
> This is a feature preview, i.e. it might not have made it into the current beta.
> Give feedback to help us prioritize.
> We also welcome [contributors](../getting-started/community.md) to help out!

X ships with a built-in `AccessLogHandler` middleware that is responsible for
logging any requests and responses from following middleware and controllers.
This default access log handling can be configured through the [`App`](app.md).
See [access logging](app.md#access-logging) for more details.

### ErrorHandler

> ⚠️ **Feature preview**
Expand Down
2 changes: 1 addition & 1 deletion src/AccessLogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use React\Stream\ReadableStreamInterface;

/**
* @internal
* @final
*/
class AccessLogHandler
{
Expand Down
24 changes: 20 additions & 4 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,44 @@ public function __construct(...$middleware)
// new MiddlewareHandler([$fiberHandler, $accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
$handlers = [];

// only log for built-in webserver and PHP development webserver by default, others have their own access log
$needsAccessLog = (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server');

$container = new Container();
if ($middleware) {
$needsErrorHandlerNext = false;
foreach ($middleware as $handler) {
if ($needsErrorHandlerNext && !$handler instanceof ErrorHandler) {
break;
}
$needsErrorHandlerNext = false;

if ($handler instanceof Container) {
$container = $handler;
} elseif ($handler === ErrorHandler::class) {
throw new \TypeError('ErrorHandler may currently only be passed as instance');
} elseif ($handler === ErrorHandler::class || $handler === AccessLogHandler::class) {
throw new \TypeError($handler . ' may currently only be passed as a middleware instance');
} elseif (!\is_callable($handler)) {
$handlers[] = $container->callable($handler);
} else {
$handlers[] = $handler;
if ($handler instanceof AccessLogHandler) {
$needsAccessLog = false;
$needsErrorHandlerNext = true;
}
}
}
if ($needsErrorHandlerNext) {
throw new \TypeError('AccessLogHandler must be followed by ErrorHandler');
}
}

// add default ErrorHandler as first handler unless it is already added explicitly
if (!($handlers[0] ?? null) instanceof ErrorHandler) {
if (!($handlers[0] ?? null) instanceof ErrorHandler && !($handlers[0] ?? null) instanceof AccessLogHandler) {
\array_unshift($handlers, new ErrorHandler());
}

// only log for built-in webserver and PHP development webserver by default, others have their own access log
if (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') {
if ($needsAccessLog) {
\array_unshift($handlers, new AccessLogHandler());
}

Expand Down
2 changes: 2 additions & 0 deletions src/RouteHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public function map(array $methods, string $route, $handler, ...$handlers): void
if ($handler instanceof Container && $i !== $last) {
$container = $handler;
unset($handlers[$i]);
} elseif ($handler instanceof AccessLogHandler || $handler === AccessLogHandler::class) {
throw new \TypeError('AccessLogHandler may currently only be passed as a global middleware instance');
} elseif (!\is_callable($handler)) {
$handlers[$i] = $container->callable($handler);
}
Expand Down
100 changes: 99 additions & 1 deletion tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public function testConstructWithErrorHandlerOnlyAssignsErrorHandlerAfterDefault
public function testConstructWithErrorHandlerClassThrows()
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('ErrorHandler may currently only be passed as instance');
$this->expectExceptionMessage('ErrorHandler may currently only be passed as a middleware instance');
new App(ErrorHandler::class);
}

Expand Down Expand Up @@ -241,6 +241,88 @@ public function testConstructWithMiddlewareAndErrorHandlerAssignsGivenErrorHandl
$this->assertInstanceOf(RouteHandler::class, $handlers[4]);
}

public function testConstructWithAccessLogHandlerAndErrorHandlerAssignsHandlersAsGiven()
{
$accessLogHandler = new AccessLogHandler();
$errorHandler = new ErrorHandler();

$app = new App($accessLogHandler, $errorHandler);

$ref = new ReflectionProperty($app, 'handler');
$ref->setAccessible(true);
$handler = $ref->getValue($app);

$this->assertInstanceOf(MiddlewareHandler::class, $handler);
$ref = new ReflectionProperty($handler, 'handlers');
$ref->setAccessible(true);
$handlers = $ref->getValue($handler);

if (PHP_VERSION_ID >= 80100) {
$first = array_shift($handlers);
$this->assertInstanceOf(FiberHandler::class, $first);
}

$this->assertCount(3, $handlers);
$this->assertSame($accessLogHandler, $handlers[0]);
$this->assertSame($errorHandler, $handlers[1]);
$this->assertInstanceOf(RouteHandler::class, $handlers[2]);
}

public function testConstructWithMiddlewareBeforeAccessLogHandlerAndErrorHandlerAssignsDefaultErrorHandlerAsFirstHandlerFollowedByGivenHandlers()
{
$middleware = static function (ServerRequestInterface $request, callable $next) { };
$accessLog = new AccessLogHandler();
$errorHandler = new ErrorHandler();

$app = new App($middleware, $accessLog, $errorHandler);

$ref = new ReflectionProperty($app, 'handler');
$ref->setAccessible(true);
$handler = $ref->getValue($app);

$this->assertInstanceOf(MiddlewareHandler::class, $handler);
$ref = new ReflectionProperty($handler, 'handlers');
$ref->setAccessible(true);
$handlers = $ref->getValue($handler);

if (PHP_VERSION_ID >= 80100) {
$first = array_shift($handlers);
$this->assertInstanceOf(FiberHandler::class, $first);
}

$this->assertCount(5, $handlers);
$this->assertInstanceOf(ErrorHandler::class, $handlers[0]);
$this->assertNotSame($errorHandler, $handlers[0]);
$this->assertSame($middleware, $handlers[1]);
$this->assertSame($accessLog, $handlers[2]);
$this->assertSame($errorHandler, $handlers[3]);
$this->assertInstanceOf(RouteHandler::class, $handlers[4]);
}

public function testConstructWithAccessLogHandlerOnlyThrows()
{
$accessLogHandler = new AccessLogHandler();

$this->expectException(\TypeError::class);
new App($accessLogHandler);
}

public function testConstructWithAccessLogHandlerClassThrows()
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('AccessLogHandler may currently only be passed as a middleware instance');
new App(AccessLogHandler::class);
}

public function testConstructWithAccessLogHandlerFollowedByMiddlewareThrows()
{
$accessLogHandler = new AccessLogHandler();
$middleware = function (ServerRequestInterface $request, callable $next) { };

$this->expectException(\TypeError::class);
new App($accessLogHandler, $middleware);
}

public function testRunWillReportListeningAddressAndRunLoopWithSocketServer()
{
$socket = @stream_socket_server('127.0.0.1:8080');
Expand Down Expand Up @@ -572,6 +654,22 @@ public function testMapMethodAddsRouteOnRouter()
$app->map(['GET', 'POST'], '/', function () { });
}

public function testGetWithAccessLogHandlerAsMiddlewareThrows()
{
$app = new App();

$this->expectException(\TypeError::class);
$app->get('/', new AccessLogHandler(), function () { });
}

public function testGetWithAccessLogHandlerClassAsMiddlewareThrows()
{
$app = new App();

$this->expectException(\TypeError::class);
$app->get('/', AccessLogHandler::class, function () { });
}

public function testRedirectMethodAddsAnyRouteOnRouterWhichWhenInvokedReturnsRedirectResponseWithTargetLocation()
{
$app = new App();
Expand Down