Skip to content
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
60 changes: 56 additions & 4 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,62 @@ $app = new FrameworkX\App($container);
// …
```

If you do not want to log to the console, you can configure an absolute log file
path by passing an argument to the [`AccessLogHandler`](middleware.md#accessloghandler)
like this:

=== "Using DI container"

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

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

$container = new FrameworkX\Container([
'accesslog' => __DIR__ . '/../logs/access.log',
FrameworkX\AccessLogHandler::class => fn(string $accesslog) => new FrameworkX\AccessLogHandler($accesslog)
]);

$app = new FrameworkX\App($container);

// …
```

=== "Using middleware instances"

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

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

$app = new FrameworkX\App(
new FrameworkX\AccessLogHandler(__DIR__ . '/../logs/access.log'),
new FrameworkX\ErrorHandler()
);



// …
```

Likewise, you can disable writing an access log by passing an absolute path to
`/dev/null` (Unix) or `nul` (Windows) like this:

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

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

$container = new FrameworkX\Container([
'accesslog' => DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'
FrameworkX\AccessLogHandler::class => fn(string $accesslog) => new FrameworkX\AccessLogHandler($accesslog),
]);

$app = new FrameworkX\App($container);

// …
```

X supports running behind reverse proxies just fine. However, by default it will
see the IP address of the last proxy server as the client IP address (this will
often be `127.0.0.1`). You can get the original client IP address if you configure
Expand All @@ -385,8 +441,6 @@ it to the [`AccessLogHandler`](middleware.md#accessloghandler) like this:
new FrameworkX\ErrorHandler()
);

$app = new FrameworkX\App($container);

// …
```

Expand All @@ -405,8 +459,6 @@ it to the [`AccessLogHandler`](middleware.md#accessloghandler) like this:
FrameworkX\ErrorHandler::class
);

$app = new FrameworkX\App($container);

// …
```

Expand Down
15 changes: 11 additions & 4 deletions src/AccessLogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ class AccessLogHandler
/** @var bool */
private $hasHighResolution;

/** @throws void */
public function __construct()
/**
* @param ?string $path (optional) absolute log file path or will log to console output by default
* @throws \InvalidArgumentException if given `$path` is not an absolute file path
* @throws \RuntimeException if given `$path` can not be opened in append mode
*/
public function __construct(?string $path = null)
{
/** @throws void because `fopen()` is known to always return a `resource` for built-in wrappers */
$this->logger = new LogStreamHandler(\PHP_SAPI === 'cli' ? 'php://output' : 'php://stderr');
if ($path === null) {
$path = \PHP_SAPI === 'cli' ? 'php://output' : 'php://stderr';
}

$this->logger = new LogStreamHandler($path);
$this->hasHighResolution = \function_exists('hrtime'); // PHP 7.3+
}

Expand Down
17 changes: 16 additions & 1 deletion src/Io/LogStreamHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ class LogStreamHandler
/** @var resource */
private $stream;

/** @throws \RuntimeException if given `$path` can not be opened in append mode */
/**
* @param string $path absolute log file path
* @throws \InvalidArgumentException if given `$path` is not an absolute file path
* @throws \RuntimeException if given `$path` can not be opened in append mode
*/
public function __construct(string $path)
{
if (\strpos($path, "\0") !== false || (\stripos($path, 'php://') !== 0 && !$this->isAbsolutePath($path))) {
throw new \InvalidArgumentException(
'Unable to open log file "' . \addslashes($path) . '": Invalid path given'
);
}

$errstr = '';
\set_error_handler(function (int $_, string $error) use (&$errstr): bool {
// Match errstr from PHP's warning message.
Expand Down Expand Up @@ -42,4 +52,9 @@ public function log(string $message): void
$ret = \fwrite($this->stream, $prefix . $message . \PHP_EOL);
assert(\is_int($ret));
}

private function isAbsolutePath(string $path): bool
{
return \DIRECTORY_SEPARATOR !== '\\' ? \substr($path, 0, 1) === '/' : (bool) \preg_match('#^[A-Z]:[/\\\\]#i', $path);
}
}
99 changes: 99 additions & 0 deletions tests/AccessLogHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,105 @@

class AccessLogHandlerTest extends TestCase
{
public function testCtorWithRelativePathThrows(): void
{
$this->expectException(\InvalidArgumentException::class);
new AccessLogHandler('../access.log');
}

public function testCtorWithPathToDirectoryThrows(): void
{
$this->expectException(\RuntimeException::class);
new AccessLogHandler(__DIR__);
}

public function testCtorWithPathToNewFileWillCreateNewFile(): void
{
$path = tempnam(sys_get_temp_dir(), 'log');
assert(is_string($path));
unlink($path);

new AccessLogHandler($path);

$this->assertFileExists($path);
unlink($path);
}

public function testInvokeWithDefaultPathWillLogMessageToConsole(): void
{
$handler = new AccessLogHandler();

$request = new ServerRequest('GET', 'http://localhost:8080/users', [], '', '1.1', ['REMOTE_ADDR' => '127.0.0.1']);
$response = new Response(200, [], "Hello\n");

$this->expectOutputRegex('#^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d 127\.0\.0\.1 "GET /users HTTP/1\.1" 200 6 0\.0\d\d' . PHP_EOL . '$#');
$handler($request, function () use ($response) { return $response; });
}

public function testInvokeWithPathToNewFileWillCreateNewFileWithLogMessage(): void
{
$path = tempnam(sys_get_temp_dir(), 'log');
assert(is_string($path));
unlink($path);

$handler = new AccessLogHandler($path);

$request = new ServerRequest('GET', 'http://localhost:8080/users', [], '', '1.1', ['REMOTE_ADDR' => '127.0.0.1']);
$response = new Response(200, [], "Hello\n");
$handler($request, function () use ($response) { return $response; });

$log = file_get_contents($path);
assert(is_string($log));

if (method_exists($this, 'assertMatchesRegularExpression')) {
$this->assertMatchesRegularExpression('#^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d 127\.0\.0\.1 "GET /users HTTP/1\.1" 200 6 0\.0\d\d' . PHP_EOL . '$#', $log);
} else {
// legacy PHPUnit < 9.1
$this->assertRegExp('#^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d 127\.0\.0\.1 "GET /users HTTP/1\.1" 200 6 0\.0\d\d' . PHP_EOL . '$#', $log);
}

unset($handler);
unlink($path);
}

public function testInvokeWithPathToExistingFileWillAppendLogMessage(): void
{
$path = tempnam(sys_get_temp_dir(), 'log');
assert(is_string($path));
file_put_contents($path, 'first' . PHP_EOL);

$handler = new AccessLogHandler($path);

$request = new ServerRequest('GET', 'http://localhost:8080/users', [], '', '1.1', ['REMOTE_ADDR' => '127.0.0.1']);
$response = new Response(200, [], "Hello\n");
$handler($request, function () use ($response) { return $response; });

$log = file_get_contents($path);
assert(is_string($log));

if (method_exists($this, 'assertMatchesRegularExpression')) {
$this->assertMatchesRegularExpression('#^first' . PHP_EOL . '\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d 127\.0\.0\.1 "GET /users HTTP/1\.1" 200 6 0\.0\d\d' . PHP_EOL . '$#', $log);
} else {
// legacy PHPUnit < 9.1
$this->assertRegExp('#^first' . PHP_EOL . '\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d 127\.0\.0\.1 "GET /users HTTP/1\.1" 200 6 0\.0\d\d' . PHP_EOL . '$#', $log);
}

unset($handler);
unlink($path);
}

/**
* @doesNotPerformAssertions
*/
public function testInvokeWithDevNullWritesNothing(): void
{
$handler = new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul');

$request = new ServerRequest('GET', 'http://localhost:8080/users', [], '', '1.1', ['REMOTE_ADDR' => '127.0.0.1']);
$response = new Response(200, [], "Hello\n");
$handler($request, function () use ($response) { return $response; });
}

public function testInvokeLogsRequest(): void
{
$handler = new AccessLogHandler();
Expand Down
25 changes: 6 additions & 19 deletions tests/AppMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use FrameworkX\AccessLogHandler;
use FrameworkX\App;
use FrameworkX\Io\MiddlewareHandler;
use FrameworkX\ErrorHandler;
use FrameworkX\Io\RouteHandler;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
Expand Down Expand Up @@ -674,23 +674,10 @@ public function testInvokeWithGlobalMiddlewareReturnsResponseWhenGlobalMiddlewar
/** @param callable|class-string ...$middleware */
private function createAppWithoutLogger(...$middleware): App
{
$app = new App(...$middleware);

$ref = new \ReflectionProperty($app, 'handler');
$ref->setAccessible(true);
$middleware = $ref->getValue($app);
assert($middleware instanceof MiddlewareHandler);

$ref = new \ReflectionProperty($middleware, 'handlers');
$ref->setAccessible(true);
$handlers = $ref->getValue($middleware);
assert(is_array($handlers));

$first = array_shift($handlers);
$this->assertInstanceOf(AccessLogHandler::class, $first);

$ref->setValue($middleware, $handlers);

return $app;
return new App(
new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'),
new ErrorHandler(),
...$middleware
);
}
}
23 changes: 5 additions & 18 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1651,23 +1651,10 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe

private function createAppWithoutLogger(callable ...$middleware): App
{
$app = new App(...$middleware);

$ref = new \ReflectionProperty($app, 'handler');
$ref->setAccessible(true);
$middleware = $ref->getValue($app);
assert($middleware instanceof MiddlewareHandler);

$ref = new \ReflectionProperty($middleware, 'handlers');
$ref->setAccessible(true);
$handlers = $ref->getValue($middleware);
assert(is_array($handlers));

$first = array_shift($handlers);
$this->assertInstanceOf(AccessLogHandler::class, $first);

$ref->setValue($middleware, $handlers);

return $app;
return new App(
new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'),
new ErrorHandler(),
...$middleware
);
}
}
Loading