arnapou/psr

Library - KISS PSR classes.

v2.6 2024-04-19 18:58 UTC

This package is auto-updated.

Last update: 2024-04-19 17:00:00 UTC


README

pipeline coverage

KISS (Keep It Simple Stupid) PSR (PHP Standards Recommendations) classes.

Installation

composer require arnapou/psr

packagist 👉️ arnapou/psr

When it is worth to use this library

  • you need simple decorators, proxies, adapters, ... about PSR's
  • you need simple implementations covering the basics

Example PSR-3 Logger

File (rotation optional, inject your custom formatter or PSR-20 Clock Interface)

$logger = new \Arnapou\Psr\Psr3Logger\FileLogger(
    logDirectory: '/path/of/logs',
    logName: 'app.log',
    rotation: \Arnapou\Psr\Psr3Logger\Utils\Rotation::Every12Hours,
);

Decorators

$logger = new \Arnapou\Psr\Psr3Logger\Decorator\FixedLevelLogger($decorated, 'info');
$logger = new \Arnapou\Psr\Psr3Logger\Decorator\MinimumLevelLogger($decorated, 'info');
$logger = new \Arnapou\Psr\Psr3Logger\Decorator\PrefixLogger($decorated, '[My-App]');

// The ContextLogger helps to gather the same context for a bunch of logs (DRY).
// It avoids repeating the context array everywhere.
$logger = new \Arnapou\Psr\Psr3Logger\Decorator\ContextLogger($decorated);
$logger->addContext(['user_id' => 42, 'name' => 'John Doe']);
$logger->debug('Hello');
$logger->info('World');

Example PSR-4 Autoloading

If you use composer, you basically don't need that, but in small edge cases, it can be useful.

$autoloader = new \Arnapou\Psr\Psr4Autoloader\Autoloader();
$autoloader->addNamespace('MyVendor/MyProject', '/path/of/src/for/example');
$autoloader->register();

Example PSR-6 Cache

KISS, here we only have PSR-6 adapter of PSR-16 Simple cache.\ Note that you can inject your own PSR-20 Clock Interface.

$cache = new \Arnapou\Psr\Psr6Cache\CacheItemPool(
    psr16SimpleCache: $psr16SimpleCache, 
    defaultTtl: 900, 
);
$item = $cache->getItem('my-key');
$item->set('my-value');
$item->expiresAfter(300);
$cache->save($item);

Example PSR-7 Http Message

We don't have our own PSR-7 classes, only decorators.

The project uses nyholm/psr7 which a super lightweight PSR-7 implementation, strict and fast.\ Reinventing the wheel here does not bring any simplicity.

Decorator for send (and more)

$response = new \Arnapou\Psr\Psr7HttpMessage\Response($decoratedPsr7Response);
$response->withHeader(\Arnapou\Psr\Psr7HttpMessage\Header\CacheControl::sharedMaxAge(600))
$response->send();

Example PSR-11 Container

Service Locator

$container = new \Arnapou\Psr\Psr11Container\ServiceLocator();
$container->registerFactory(
    'db',
    static function() {
        $pdo = new PDO($dsn, $username, $password);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        return $pdo;
    }
);

$container->get('db')->query('SELECT ....');

Static Services

/**
 * @method static \PDO db()
 */
class Services extends \Arnapou\Psr\Psr11Container\StaticServices
{
    public function getFactories(): iterable
    {
        yield 'db' => static function() {
            $pdo = new PDO($dsn, $username, $password);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            return $pdo;
        };
    }
}

Services::db()->query('SELECT ....');

Example PSR-13 Link

To be honest, I don't know small projects where it makes a difference using this PSR.

Basic objects

$link = new \Arnapou\Psr\Psr13Link\Link('https://arnapou.net/');
$link = new \Arnapou\Psr\Psr13Link\EvolvableLink('https://arnapou.net/');

Example PSR-14 Event Dispatcher

The way we can implement the PSR is very wide.

I basically built some "object bus" with routing of events based on php type hinting.\ In the PSR, an event can be any object, you can stop the propagation implementing a StoppableEventInterface for that object.

This is very simple but efficient and intuitive.

$dispatcher = new \Arnapou\Psr\Psr14EventDispatcher\ClassEventDispatcher();
$dispatcher->listen(function(DateTime $date) {
    echo "DateTime: " . $date->format('Y-m-d') . "\n";
});
$dispatcher->listen(function(DateTimeImmutable $date) {
    echo "DateTimeImmutable: " . $date->format('Y-m-d') . "\n";
});
$dispatcher->listen(function(DateTimeInterface $date) {
    echo "DateTimeInterface: " . $date->format('Y-m-d') . "\n";
});

$dispatcher->dispatch(new DateTime('2023-10-10'));
// This will echo
// DateTimeInterface: 2023-10-10
// DateTime: 2023-10-10

Php Handlers : in order to ease the logging of php crashs, etc ... I made a class which uses my dispatcher

$phpHandlers = new \Arnapou\Psr\Psr14EventDispatcher\PhpHandlers($logger);
$phpHandlers->registerAll();

// You can decide to throw ErrorException on notices
$phpHandlers->throwErrorNotice();

// You can hook to do what you want on exceptions or errors
$phpHandlers->listen(function (\Throwable $throwable) {
    if ($throwable->getCode() === 666) {
        exit;
    }
});

// Or on shell signals
$phpHandlers->listen(function (\Arnapou\Psr\Psr14EventDispatcher\Event\SignalEvent $event) use ($logger) {
    if ($event->value === SIGINT) {
        $logger->emergency('Somebody CTRL-C the process');
    }
});

// Or on process exit, you also can carry your own infos in the object used
$phpHandlers->shutdownEvent->setValue('AppName', 'my-app');
$phpHandlers->listen(function (\Arnapou\Psr\Psr14EventDispatcher\Event\ShutdownEvent $event) use ($logger) {
    $appName = $event->getValues()['AppName'];
    $logger->debug('The process elapsed '. $event->getElapsedTime() . ' seconds for app ' . $appName);
});

Example PSR-15 Http Handlers

The way we can implement the PSR is very wide.

I basically built a "route handler" which uses internally my PSR-14 Event Dispatcher.\ I should not have to tell you that this is enough simple for small projects without hundreds of routes.\ If you need performance in your big project, you probably don't need this stuff.

This is very simple and 100% PSR dependant.

$handler = new \Arnapou\Psr\Psr15HttpHandlers\HttpRouteHandler();

// Add routes
$handler
    ->addRoute(
        '/users/{id}', 
        function (int $id) {
            return ['user'=> $id]
        }
    )
    ->setRequirement('id', '\d+');

// Get the server Request (here from global by lazyness)
$request = (new \Arnapou\Psr\Psr17HttpFactories\HttpFactory())->createServerRequestFromGlobals();

// Handle and get the response, a NoResponseFound will be thrown if no match
try {
    $response = $handler->handle($request);
} catch (\Arnapou\Psr\Psr15HttpHandlers\Exception\NoResponseFound) {
    $response = new \Arnapou\Psr\Psr7HttpMessage\HtmlResponse('Not Found', 404);
}

// Use the response decorator to send it
(new \Arnapou\Psr\Psr7HttpMessage\Response($response))->send();

Example PSR-16 Simple Cache

There is one thing I didn't implement from the PSR : it is the PSR key characters restriction.\ Because I never saw a case during the past 12 years where it was necessary.\ The only restrictions implemented are technically (ex memcached).

Implementations.

// For runtime, full memory
$cache = new \Arnapou\Psr\Psr16SimpleCache\ArraySimpleCache(defaultTtl: 900);

// Flat file cache with custom serializer and PSR-20 Clock Interface
$cache = new \Arnapou\Psr\Psr16SimpleCache\FileSimpleCache(path: '/path/of/cache', defaultTtl: 900);

// Memcached cache with custom serializer and PSR-20 Clock Interface
$cache = new \Arnapou\Psr\Psr16SimpleCache\MemcachedSimpleCache($memcached);

// Redis cache with custom serializer and PSR-20 Clock Interface
$cache = new \Arnapou\Psr\Psr16SimpleCache\RedisSimpleCache($redis);

// Does nothing at all
$cache = new \Arnapou\Psr\Psr16SimpleCache\NullSimpleCache();

Decorators.

// Helps to reduce I/Os on decorated cache by keeping in memory the values already retrieved
$cache = new \Arnapou\Psr\Psr16SimpleCache\Decorated\RuntimeSimpleCache($decorated);

// Helps to isolate keys in "namespaces" for instance
$cache = new \Arnapou\Psr\Psr16SimpleCache\Decorated\PrefixSimpleCache($decorated, 'some:prefix');

// Forces the ttl to be in the range wanted to avoid silly TTLs
$cache = new \Arnapou\Psr\Psr16SimpleCache\Decorated\TtlRestrictedSimpleCache($decorated, ttlMin: 60, ttlMax: 3600);

// Avoid any writing in that cache
$cache = new \Arnapou\Psr\Psr16SimpleCache\Decorated\ReadonlySimpleCache($decorated, throwException: true);

Example PSR-17 Http Factory

The project uses nyholm/psr7 which a super lightweight PSR-7 implementation, strict and fast.\ Reinventing the wheel here does not bring any simplicity.

Our PSR-17 class is a composition over nyholm/psr7 factory.

$factory = new \Arnapou\Psr\Psr17HttpFactories\HttpFactory();
$serverRequest = $factory->createServerRequestFromGlobals();

Example PSR-18 Http Client

The project uses symfony/http-client which is one of the best client while not being too heavy like other whales.\ Reinventing the wheel here does not bring any simplicity.

Our PSR-18 class is a composition over symfony/http-client and nyholm/psr7 factory.

$client = new \Arnapou\Psr\Psr18HttpClient\HttpClient();
$request = $client->createRequest('GET', 'https://arnapou.net/ip');
$response = $client->sendRequest($request);
echo $response->getBody();

Example PSR-20 Clock Interface

One of the most simple PSR, but very powerful.\ This repo uses this clock interface in loggers and caches.\ It helps a lot for testing for example.

// Basic clock with date and timezone
$clock = new \Arnapou\Psr\Psr20Clock\NowClock();
echo $clock->now()->format('Y-m-d H:i:s') . "\n";

// This clock will always have the same "time".
$clock = \Arnapou\Psr\Psr20Clock\FixedClock::createFromTimestamp(time());
echo $clock->now()->format('Y-m-d H:i:s') . "\n";

Changelog versions

StartTag, BranchPhp
25/11/20232.x, main8.3
12/09/20231.x8.2