mirror of
https://github.com/notrab/dumbo.git
synced 2025-01-16 21:58:25 +01:00
feat: add logger helper (#32)
refact: remove monolog/monolog and use psr/log
This commit is contained in:
parent
a24f90eabb
commit
643aa4f90d
@ -15,6 +15,7 @@
|
||||
"psr/http-message": "^2.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-server-handler": "^1.0",
|
||||
"psr/log": "^3.0",
|
||||
"guzzlehttp/psr7": "^2.0",
|
||||
"nikic/fast-route": "^1.3"
|
||||
},
|
||||
|
52
composer.lock
generated
52
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "bfa3f4a827ed1cc39aa7fc27ac40d13a",
|
||||
"content-hash": "0c0bda01f8c04d2e20e328eeb6eff878",
|
||||
"packages": [
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
@ -336,6 +336,56 @@
|
||||
},
|
||||
"time": "2023-04-10T20:06:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for logging libraries",
|
||||
"homepage": "https://github.com/php-fig/log",
|
||||
"keywords": [
|
||||
"log",
|
||||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.1"
|
||||
},
|
||||
"time": "2024-08-21T13:31:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
"version": "3.0.3",
|
||||
|
23
examples/logger/README.md
Normal file
23
examples/logger/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Logger Example
|
||||
|
||||
This example demonstrates how to use a logger in Dumbo.
|
||||
|
||||
## Running the Example
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
2. Start the server:
|
||||
|
||||
```bash
|
||||
composer start
|
||||
```
|
||||
|
||||
3. Access the route:
|
||||
|
||||
```bash
|
||||
curl -v http://localhost:8000
|
||||
```
|
15
examples/logger/composer.json
Normal file
15
examples/logger/composer.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"require": {
|
||||
"notrab/dumbo": "@dev",
|
||||
"monolog/monolog": "^3.7"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../../"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"start": "php -S localhost:8000 index.php"
|
||||
}
|
||||
}
|
58
examples/logger/index.php
Normal file
58
examples/logger/index.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
require "vendor/autoload.php";
|
||||
|
||||
use Dumbo\Dumbo;
|
||||
use Dumbo\Helpers\Logger;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Logger as MonologLogger;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
$app = new Dumbo();
|
||||
|
||||
$logger = new MonologLogger("example");
|
||||
$handler = new StreamHandler("php://stdout");
|
||||
$formatter = new LineFormatter(
|
||||
"[%datetime%] %channel%.%level_name%: %message%\n",
|
||||
"Y-m-d H:i:s.u"
|
||||
);
|
||||
$handler->setFormatter($formatter);
|
||||
$logger->pushHandler($handler);
|
||||
|
||||
$app->use(Logger::logger($logger));
|
||||
|
||||
$app->get("/", function ($context) {
|
||||
return $context->html("<h1>We've just logged something on the console!</h1>");
|
||||
});
|
||||
|
||||
$userData = [
|
||||
[
|
||||
"id" => 1,
|
||||
"name" => "Jamie Barton",
|
||||
"email" => "jamie@notrab.dev",
|
||||
],
|
||||
];
|
||||
|
||||
$user = new Dumbo();
|
||||
|
||||
$user->get("/", function ($c) use ($userData) {
|
||||
return $c->json($userData);
|
||||
});
|
||||
|
||||
$user->get("/:id", function ($c) use ($userData) {
|
||||
$id = (int) $c->req->param("id");
|
||||
|
||||
$user =
|
||||
array_values(array_filter($userData, fn($u) => $u["id"] === $id))[0] ??
|
||||
null;
|
||||
|
||||
if (!$user) {
|
||||
return $c->json(["error" => "User not found"], 404);
|
||||
}
|
||||
|
||||
return $c->json($user);
|
||||
});
|
||||
|
||||
$app->route("/users", $user);
|
||||
|
||||
$app->run();
|
@ -130,30 +130,36 @@ class Dumbo
|
||||
try {
|
||||
$route = $this->router->findRoute($request);
|
||||
|
||||
$context = new Context(
|
||||
$request,
|
||||
$route ? $route["params"] : [],
|
||||
$route ? $route["routePath"] : ""
|
||||
);
|
||||
|
||||
$fullMiddlewareStack = $this->getFullMiddlewareStack();
|
||||
|
||||
if ($route) {
|
||||
$context = new Context(
|
||||
$request,
|
||||
$route["params"],
|
||||
$route["routePath"]
|
||||
);
|
||||
|
||||
$fullMiddlewareStack = array_merge(
|
||||
$this->getFullMiddlewareStack(),
|
||||
$fullMiddlewareStack = array_unique(array_merge(
|
||||
$fullMiddlewareStack,
|
||||
$route["middleware"] ?? []
|
||||
);
|
||||
), SORT_REGULAR);
|
||||
|
||||
$response = $this->runMiddleware(
|
||||
$context,
|
||||
$route["handler"],
|
||||
$fullMiddlewareStack
|
||||
);
|
||||
|
||||
return $response instanceof ResponseInterface
|
||||
? $response
|
||||
: $context->getResponse();
|
||||
$handler = $route["handler"];
|
||||
} else {
|
||||
$handler = function () {
|
||||
return new Response(404, [], "404 Not Found");
|
||||
};
|
||||
}
|
||||
|
||||
return new Response(404, [], "404 Not Found");
|
||||
$response = $this->runMiddleware(
|
||||
$context,
|
||||
$handler,
|
||||
$fullMiddlewareStack
|
||||
);
|
||||
|
||||
return $response instanceof ResponseInterface
|
||||
? $response
|
||||
: $context->getResponse();
|
||||
} catch (HTTPException $e) {
|
||||
return $this->handleHTTPException($e, $request);
|
||||
} catch (\Exception $e) {
|
||||
@ -306,7 +312,6 @@ class Dumbo
|
||||
private function getFullMiddlewareStack(): array
|
||||
{
|
||||
$stack = $this->middleware;
|
||||
|
||||
$current = $this;
|
||||
|
||||
while ($current->parent !== null) {
|
||||
|
113
src/Helpers/Logger.php
Normal file
113
src/Helpers/Logger.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Dumbo\Helpers;
|
||||
|
||||
use Dumbo\Context;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Logger
|
||||
{
|
||||
public const LOG_PREFIX_INCOMING = '-->';
|
||||
public const LOG_PREFIX_OUTGOING = '<--';
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* Constructor to initialize the logger with a PSR-3 compliant logger.
|
||||
*
|
||||
* @param LoggerInterface $logger The PSR-3 compliant logger.
|
||||
*/
|
||||
private function __construct(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a middleware that logs incoming requests.
|
||||
*
|
||||
* @param LoggerInterface $logger The PSR-3 compliant logger.
|
||||
* @return callable The middleware.
|
||||
*/
|
||||
public static function logger(LoggerInterface $logger): callable
|
||||
{
|
||||
$middleware = new self($logger);
|
||||
return $middleware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the logger middleware.
|
||||
*
|
||||
* @param Context $ctx The context object containing the request.
|
||||
* @param callable $next The next middleware or handler.
|
||||
* @return ResponseInterface The HTTP response.
|
||||
*/
|
||||
public function __invoke(Context $ctx, callable $next): ResponseInterface
|
||||
{
|
||||
$method = $ctx->req->method();
|
||||
$path = $ctx->req->path();
|
||||
|
||||
$this->log(self::LOG_PREFIX_INCOMING, $method, $path);
|
||||
|
||||
$start = microtime(true);
|
||||
$response = $next($ctx);
|
||||
$elapsed = $this->getElapsedTime($start);
|
||||
|
||||
$this->log(self::LOG_PREFIX_OUTGOING, $method, $path, $response->getStatusCode(), $elapsed);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message with the specified prefix, method, path, status, and elapsed time.
|
||||
*
|
||||
* @param string $prefix The log prefix indicating incoming or outgoing.
|
||||
* @param string $method The HTTP method of the request.
|
||||
* @param string $path The request path.
|
||||
* @param int $status The HTTP status code (default is 0).
|
||||
* @param string $elapsed The elapsed time for processing the request (default is an empty string).
|
||||
* @return void
|
||||
*/
|
||||
private function log(string $prefix, string $method, string $path, int $status = 0, string $elapsed = ''): void
|
||||
{
|
||||
$message = $prefix === self::LOG_PREFIX_INCOMING
|
||||
? sprintf("%s %s %s", $prefix, $method, $path)
|
||||
: sprintf("%s %s %s %s %s", $prefix, $method, $path, $this->colorStatus($status), $elapsed);
|
||||
|
||||
$this->logger->info($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the elapsed time since the given start time.
|
||||
*
|
||||
* @param float $start The start time in microseconds.
|
||||
* @return string The elapsed time in milliseconds or seconds.
|
||||
*/
|
||||
private function getElapsedTime(float $start): string
|
||||
{
|
||||
$delta = (microtime(true) - $start) * 1000;
|
||||
return $delta < 1000 ? $delta . 'ms' : round($delta / 1000, 3) . 's';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a colored string representation of the HTTP status code.
|
||||
*
|
||||
* @param int $status The HTTP status code.
|
||||
* @return string The colored status code.
|
||||
*/
|
||||
private function colorStatus(int $status): string
|
||||
{
|
||||
$colors = [
|
||||
7 => "\033[35m%d\033[0m",
|
||||
5 => "\033[31m%d\033[0m",
|
||||
4 => "\033[33m%d\033[0m",
|
||||
3 => "\033[36m%d\033[0m",
|
||||
2 => "\033[32m%d\033[0m",
|
||||
1 => "\033[32m%d\033[0m",
|
||||
0 => "\033[33m%d\033[0m",
|
||||
];
|
||||
|
||||
$colorCode = $colors[intdiv($status, 100)] ?? "%d";
|
||||
return sprintf($colorCode, $status);
|
||||
}
|
||||
}
|
66
tests/Helpers/LoggerTest.php
Normal file
66
tests/Helpers/LoggerTest.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Dumbo\Tests\Helpers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Dumbo\Helpers\Logger;
|
||||
use Dumbo\Context;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
class LoggerTest extends TestCase
|
||||
{
|
||||
private function createMockContext(string $method, string $path): Context
|
||||
{
|
||||
$uri = $this->createMock(UriInterface::class);
|
||||
$uri->method('getPath')->willReturn($path);
|
||||
|
||||
$request = $this->createMock(ServerRequestInterface::class);
|
||||
$request->method('getUri')->willReturn($uri);
|
||||
$request->method('getMethod')->willReturn($method);
|
||||
|
||||
return new Context($request, [], "");
|
||||
}
|
||||
|
||||
public function testLoggerInstance()
|
||||
{
|
||||
$loggerMock = $this->createMock(LoggerInterface::class);
|
||||
$middleware = Logger::logger($loggerMock);
|
||||
|
||||
$this->assertInstanceOf(Logger::class, $middleware);
|
||||
}
|
||||
|
||||
public function testInvokeLogsIncomingAndOutgoingRequest()
|
||||
{
|
||||
$loggerMock = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$messages = [];
|
||||
$loggerMock->expects($this->exactly(2))
|
||||
->method('info')
|
||||
->willReturnCallback(function ($message) use (&$messages) {
|
||||
$messages[] = $message;
|
||||
});
|
||||
|
||||
$context = $this->createMockContext('GET', '/test-path');
|
||||
|
||||
$responseMock = $this->createMock(ResponseInterface::class);
|
||||
$responseMock->method('getStatusCode')->willReturn(200);
|
||||
|
||||
$middleware = Logger::logger($loggerMock);
|
||||
|
||||
$next = fn () => $responseMock;
|
||||
|
||||
$middleware($context, $next);
|
||||
|
||||
$this->assertCount(2, $messages);
|
||||
|
||||
$this->assertStringContainsString(Logger::LOG_PREFIX_INCOMING, $messages[0]);
|
||||
$this->assertStringContainsString('GET /test-path', $messages[0]);
|
||||
|
||||
$this->assertStringContainsString(Logger::LOG_PREFIX_OUTGOING, $messages[1]);
|
||||
$this->assertStringContainsString('GET /test-path', $messages[1]);
|
||||
$this->assertStringContainsString('200', $messages[1]);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user