feat: add logger helper (#32)

refact: remove monolog/monolog and use psr/log
This commit is contained in:
Lucas Coutinho 2024-09-02 07:29:17 -03:00 committed by GitHub
parent a24f90eabb
commit 643aa4f90d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 352 additions and 21 deletions

View File

@ -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
View File

@ -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
View 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
```

View 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
View 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();

View File

@ -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
View 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);
}
}

View 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]);
}
}