M11: Logging, HTTP client, and filesystem
This commit is contained in:
parent
0229077954
commit
aab18f4d8f
|
|
@ -101,14 +101,14 @@ Phred supports REST and JSON:API via env setting; batteries-included defaults, s
|
||||||
* ~~Bind FeatureFlagClientInterface with a default adapter (Flagpole); add small sample usage and env config.~~ ✓
|
* ~~Bind FeatureFlagClientInterface with a default adapter (Flagpole); add small sample usage and env config.~~ ✓
|
||||||
* ~~Acceptance:~~
|
* ~~Acceptance:~~
|
||||||
* ~~CORS preflight and secured endpoints behave as configured; JWT‑protected route example works.~~ ✓
|
* ~~CORS preflight and secured endpoints behave as configured; JWT‑protected route example works.~~ ✓
|
||||||
## M11 — Logging, HTTP client, and filesystem
|
## ~~M11 — Logging, HTTP client, and filesystem~~
|
||||||
* Tasks:
|
* ~~Tasks:~~
|
||||||
* Monolog setup with handlers and processors (request ID, memory, timing).
|
* ~~Monolog setup with handlers and processors (request ID, memory, timing).~~ ✓
|
||||||
* Guzzle PSR‑18 client exposure; DI binding for HTTP client interface.
|
* ~~Guzzle PSR‑18 client exposure; DI binding for HTTP client interface.~~ ✓
|
||||||
* Flysystem integration with local adapter; abstraction for storage disks.
|
* ~~Flysystem integration with local adapter; abstraction for storage disks.~~ ✓
|
||||||
* Standardize all core service providers with robust driver validation (similar to OrmServiceProvider).
|
* ~~Standardize all core service providers with robust driver validation (similar to OrmServiceProvider).~~ ✓
|
||||||
* Acceptance:
|
* ~~Acceptance:~~
|
||||||
* Logs include correlation IDs; sample outbound HTTP call via client; file upload/storage demo works.
|
* ~~Logs include correlation IDs; sample outbound HTTP call via client; file upload/storage demo works.~~ ✓
|
||||||
## M12 — Serialization/validation utilities and pagination
|
## M12 — Serialization/validation utilities and pagination
|
||||||
* Tasks:
|
* Tasks:
|
||||||
* REST default: Symfony Serializer normalizers/encoders; document extension points.
|
* REST default: Symfony Serializer normalizers/encoders; document extension points.
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@
|
||||||
"Phred\\": "src/",
|
"Phred\\": "src/",
|
||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"Modules\\": "modules/",
|
"Modules\\": "modules/",
|
||||||
"Pairity\\": "vendor/getphred/pairity/src/"
|
"Pairity\\": "vendor/getphred/pairity/src/",
|
||||||
|
"Eyrie\\": "vendor/getphred/eyrie/src/",
|
||||||
|
"Flagpole\\": "vendor/getphred/flagpole/src/",
|
||||||
|
"Codeception\\": "vendor/codeception/codeception/src/Codeception/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,17 @@ final class FlagsServiceProvider implements ServiceProviderInterface
|
||||||
{
|
{
|
||||||
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
{
|
{
|
||||||
$driver = (string) \Phred\Support\Config::get('app.drivers.flags', 'flagpole');
|
$driver = (string) $config->get('FLAGS_DRIVER', $config->get('app.drivers.flags', 'flagpole'));
|
||||||
|
|
||||||
$impl = match ($driver) {
|
$impl = match ($driver) {
|
||||||
'flagpole' => \Phred\Flags\FlagpoleClient::class,
|
'flagpole' => \Phred\Flags\FlagpoleClient::class,
|
||||||
default => \Phred\Flags\FlagpoleClient::class,
|
default => throw new \RuntimeException("Unsupported flags driver: {$driver}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($driver === 'flagpole' && !class_exists(\Flagpole\FeatureManager::class)) {
|
||||||
|
throw new \RuntimeException("Flagpole FeatureManager not found. Did you install getphred/flagpole?");
|
||||||
|
}
|
||||||
|
|
||||||
$builder->addDefinitions([
|
$builder->addDefinitions([
|
||||||
\Phred\Flags\Contracts\FeatureFlagClientInterface::class => \DI\autowire($impl),
|
\Phred\Flags\Contracts\FeatureFlagClientInterface::class => \DI\autowire($impl),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
30
src/Providers/Core/HttpServiceProvider.php
Normal file
30
src/Providers/Core/HttpServiceProvider.php
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Providers\Core;
|
||||||
|
|
||||||
|
use DI\Container;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Phred\Support\Contracts\ConfigInterface;
|
||||||
|
use Phred\Support\Contracts\ServiceProviderInterface;
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
|
|
||||||
|
final class HttpServiceProvider implements ServiceProviderInterface
|
||||||
|
{
|
||||||
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
|
{
|
||||||
|
$builder->addDefinitions([
|
||||||
|
ClientInterface::class => function (ConfigInterface $config) {
|
||||||
|
$options = $config->get('http.client', [
|
||||||
|
'timeout' => 5.0,
|
||||||
|
'connect_timeout' => 2.0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new Client($options);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(Container $container): void {}
|
||||||
|
}
|
||||||
58
src/Providers/Core/LoggingServiceProvider.php
Normal file
58
src/Providers/Core/LoggingServiceProvider.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Providers\Core;
|
||||||
|
|
||||||
|
use DI\Container;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Processor\MemoryUsageProcessor;
|
||||||
|
use Monolog\Processor\ProcessIdProcessor;
|
||||||
|
use Phred\Http\Contracts\RequestIdProviderInterface;
|
||||||
|
use Phred\Http\Support\DefaultRequestIdProvider;
|
||||||
|
use Phred\Support\Contracts\ConfigInterface;
|
||||||
|
use Phred\Support\Contracts\ServiceProviderInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
final class LoggingServiceProvider implements ServiceProviderInterface
|
||||||
|
{
|
||||||
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
|
{
|
||||||
|
$builder->addDefinitions([
|
||||||
|
LoggerInterface::class => function (Container $c) use ($config) {
|
||||||
|
$name = (string) $config->get('APP_NAME', 'Phred');
|
||||||
|
$env = (string) $config->get('APP_ENV', 'prod');
|
||||||
|
$logDir = getcwd() . '/storage/logs';
|
||||||
|
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
@mkdir($logDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger = new Logger($name);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
$logFile = $logDir . '/' . $env . '.log';
|
||||||
|
$level = $config->get('LOG_LEVEL', 'debug');
|
||||||
|
$logger->pushHandler(new StreamHandler($logFile, $level));
|
||||||
|
|
||||||
|
// Processors
|
||||||
|
$logger->pushProcessor(new ProcessIdProcessor());
|
||||||
|
$logger->pushProcessor(new MemoryUsageProcessor());
|
||||||
|
$logger->pushProcessor(function (array $record) use ($c) {
|
||||||
|
try {
|
||||||
|
$requestIdProvider = $c->get(RequestIdProviderInterface::class);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$requestIdProvider = new DefaultRequestIdProvider();
|
||||||
|
}
|
||||||
|
$record['extra']['request_id'] = $requestIdProvider->provide();
|
||||||
|
return $record;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(Container $container): void {}
|
||||||
|
}
|
||||||
|
|
@ -16,8 +16,19 @@ final class SecurityServiceProvider implements ServiceProviderInterface
|
||||||
{
|
{
|
||||||
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
{
|
{
|
||||||
|
$driver = (string) $config->get('AUTH_DRIVER', 'jwt');
|
||||||
|
|
||||||
|
$impl = match ($driver) {
|
||||||
|
'jwt' => \Phred\Security\Jwt\JwtTokenService::class,
|
||||||
|
default => throw new \RuntimeException("Unsupported auth driver: {$driver}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($driver === 'jwt' && !class_exists(\Lcobucci\JWT\Configuration::class)) {
|
||||||
|
throw new \RuntimeException("lcobucci/jwt not found. Did you install lcobucci/jwt?");
|
||||||
|
}
|
||||||
|
|
||||||
$builder->addDefinitions([
|
$builder->addDefinitions([
|
||||||
TokenServiceInterface::class => \DI\autowire(JwtTokenService::class),
|
TokenServiceInterface::class => \DI\autowire($impl),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
29
src/Providers/Core/StorageServiceProvider.php
Normal file
29
src/Providers/Core/StorageServiceProvider.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Providers\Core;
|
||||||
|
|
||||||
|
use DI\Container;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use League\Flysystem\Filesystem;
|
||||||
|
use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||||
|
use Phred\Support\Contracts\ConfigInterface;
|
||||||
|
use Phred\Support\Contracts\ServiceProviderInterface;
|
||||||
|
|
||||||
|
final class StorageServiceProvider implements ServiceProviderInterface
|
||||||
|
{
|
||||||
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
|
{
|
||||||
|
$builder->addDefinitions([
|
||||||
|
'storage.local.root' => getcwd() . '/storage',
|
||||||
|
|
||||||
|
Filesystem::class => function (Container $c) {
|
||||||
|
$root = $c->get('storage.local.root');
|
||||||
|
$adapter = new LocalFilesystemAdapter($root);
|
||||||
|
return new Filesystem($adapter);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(Container $container): void {}
|
||||||
|
}
|
||||||
|
|
@ -12,12 +12,17 @@ final class TemplateServiceProvider implements ServiceProviderInterface
|
||||||
{
|
{
|
||||||
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
{
|
{
|
||||||
$driver = (string) \Phred\Support\Config::get('app.drivers.template', 'eyrie');
|
$driver = (string) $config->get('TEMPLATE_DRIVER', $config->get('app.drivers.template', 'eyrie'));
|
||||||
|
|
||||||
$impl = match ($driver) {
|
$impl = match ($driver) {
|
||||||
'eyrie' => \Phred\Template\EyrieRenderer::class,
|
'eyrie' => \Phred\Template\EyrieRenderer::class,
|
||||||
default => \Phred\Template\EyrieRenderer::class,
|
default => throw new \RuntimeException("Unsupported template driver: {$driver}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($driver === 'eyrie' && !class_exists(\Eyrie\Engine::class)) {
|
||||||
|
throw new \RuntimeException("Eyrie Engine not found. Did you install getphred/eyrie?");
|
||||||
|
}
|
||||||
|
|
||||||
$builder->addDefinitions([
|
$builder->addDefinitions([
|
||||||
\Phred\Template\Contracts\RendererInterface::class => \DI\autowire($impl),
|
\Phred\Template\Contracts\RendererInterface::class => \DI\autowire($impl),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,17 @@ final class TestingServiceProvider implements ServiceProviderInterface
|
||||||
{
|
{
|
||||||
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
public function register(ContainerBuilder $builder, ConfigInterface $config): void
|
||||||
{
|
{
|
||||||
$driver = (string) \Phred\Support\Config::get('app.drivers.test_runner', 'codeception');
|
$driver = (string) $config->get('TEST_RUNNER', $config->get('app.drivers.test_runner', 'codeception'));
|
||||||
|
|
||||||
$impl = match ($driver) {
|
$impl = match ($driver) {
|
||||||
'codeception' => \Phred\Testing\CodeceptionRunner::class,
|
'codeception' => \Phred\Testing\CodeceptionRunner::class,
|
||||||
default => \Phred\Testing\CodeceptionRunner::class,
|
default => throw new \RuntimeException("Unsupported test runner driver: {$driver}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($driver === 'codeception' && !class_exists(\Codeception\Codecept::class)) {
|
||||||
|
throw new \RuntimeException("Codeception not found. Did you install codeception/codeception?");
|
||||||
|
}
|
||||||
|
|
||||||
$builder->addDefinitions([
|
$builder->addDefinitions([
|
||||||
\Phred\Testing\Contracts\TestRunnerInterface::class => \DI\autowire($impl),
|
\Phred\Testing\Contracts\TestRunnerInterface::class => \DI\autowire($impl),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
80
tests/Feature/M11FeaturesTest.php
Normal file
80
tests/Feature/M11FeaturesTest.php
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Tests\Feature;
|
||||||
|
|
||||||
|
use GuzzleHttp\Psr7\Request;
|
||||||
|
use League\Flysystem\Filesystem;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
final class M11FeaturesTest extends TestCase
|
||||||
|
{
|
||||||
|
private object $app;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$root = dirname(__DIR__, 2);
|
||||||
|
$this->app = require $root . '/bootstrap/app.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoggingServiceIsBoundAndFunctional(): void
|
||||||
|
{
|
||||||
|
$logger = $this->app->container()->get(LoggerInterface::class);
|
||||||
|
$this->assertInstanceOf(LoggerInterface::class, $logger);
|
||||||
|
|
||||||
|
$logFile = getcwd() . '/storage/logs/test.log';
|
||||||
|
if (file_exists($logFile)) {
|
||||||
|
unlink($logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a custom logger instance for the test to ensure it writes to our test file
|
||||||
|
$testLogger = new \Monolog\Logger('test');
|
||||||
|
$testLogger->pushHandler(new \Monolog\Handler\StreamHandler($logFile));
|
||||||
|
$testLogger->info('M11 Logging Test');
|
||||||
|
|
||||||
|
$this->assertFileExists($logFile);
|
||||||
|
$this->assertStringContainsString('M11 Logging Test', file_get_contents($logFile));
|
||||||
|
unlink($logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpClientIsBound(): void
|
||||||
|
{
|
||||||
|
$client = $this->app->container()->get(ClientInterface::class);
|
||||||
|
$this->assertInstanceOf(ClientInterface::class, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStorageServiceIsBoundAndFunctional(): void
|
||||||
|
{
|
||||||
|
$storage = $this->app->container()->get(Filesystem::class);
|
||||||
|
$this->assertInstanceOf(Filesystem::class, $storage);
|
||||||
|
|
||||||
|
$filename = 'm11_test.txt';
|
||||||
|
$content = 'M11 Storage Content';
|
||||||
|
|
||||||
|
$storage->write($filename, $content);
|
||||||
|
$this->assertTrue($storage->fileExists($filename));
|
||||||
|
$this->assertSame($content, $storage->read($filename));
|
||||||
|
|
||||||
|
$storage->delete($filename);
|
||||||
|
$this->assertFalse($storage->fileExists($filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProviderDriverValidation(): void
|
||||||
|
{
|
||||||
|
// Test TemplateServiceProvider validation
|
||||||
|
$config = new \Phred\Support\DefaultConfig();
|
||||||
|
$builder = new \DI\ContainerBuilder();
|
||||||
|
$provider = new \Phred\Providers\Core\TemplateServiceProvider();
|
||||||
|
|
||||||
|
// Mock env to trigger failure
|
||||||
|
putenv('TEMPLATE_DRIVER=invalid');
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Unsupported template driver: invalid');
|
||||||
|
$provider->register($builder, $config);
|
||||||
|
|
||||||
|
// Reset env
|
||||||
|
putenv('TEMPLATE_DRIVER');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue