shlinkio / shlink-common
Common tools used by Shlink
Installs: 55 055
Dependents: 1
Suggesters: 0
Security: 0
Stars: 2
Watchers: 2
Forks: 3
Open Issues: 1
Type:project
pkg:composer/shlinkio/shlink-common
Requires
- php: ^8.3
- ext-fileinfo: *
- cakephp/chronos: ^3.2
- doctrine/orm: ^3.5
- fig/http-message-util: ^1.1
- guzzlehttp/guzzle: ^7.9
- laminas/laminas-diactoros: ^3.6
- laminas/laminas-inputfilter: ^2.33
- laminas/laminas-servicemanager: ^3.23
- lcobucci/jwt: ^5.5
- monolog/monolog: ^3.9
- php-amqplib/php-amqplib: ^3.7
- predis/predis: ^2.4
- psr/http-server-middleware: ^1.0
- ramsey/uuid: ^4.9
- shlinkio/shlink-config: ^4.0
- shlinkio/shlink-json: ^1.2
- symfony/cache: ^7.3
- symfony/lock: ^7.1.6
- symfony/mercure: ^0.6
- symfony/string: ^7.3
- symfony/translation-contracts: ^3.6
- symfony/var-exporter: ^7.3
Requires (Dev)
- akrabat/ip-address-middleware: ^2.6
- devster/ubench: ^2.1
- endroid/qr-code: ^6.0.7
- laminas/laminas-stratigility: ^3.13
- mezzio/mezzio-problem-details: ^1.15
- pagerfanta/core: ^3.8
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12.2
- psr/simple-cache: ^3.0
- roave/security-advisories: dev-master
- shlinkio/php-coding-standard: ~2.4.0
- symfony/var-dumper: ^7.3
Suggests
- endroid/qr-code: To use QrCodeResponse
- laminas/laminas-stratigility: To log ErrorHandler errors using the ErrorLogger
- mezzio/mezzio-problem-details: To log ProblemDetailsMiddleware errors using the ErrorLogger
- pagerfanta/core: To use the PagerfantaUtilsTrait
- dev-main
- v7.1.0
- v7.0.1
- v7.0.0
- v6.6.0
- v6.5.0
- v6.4.0
- v6.3.0
- v6.2.0
- v6.1.0
- v6.0.0
- v5.7.1
- v5.7.0
- v5.6.0
- v5.5.1
- v5.5.0
- v5.4.0
- v5.3.1
- v5.3.0
- v5.2.0
- v5.1.0
- v5.0.0
- v4.5.0
- v4.4.0
- 4.3.0
- v4.2.1
- v4.2.0
- v4.1.0
- v4.0.0
- v3.7.0
- v3.6.0
- v3.5.0
- v3.4.0
- v3.3.2
- v3.3.1
- v3.3.0
- v3.2.1
- v3.2.0
- v3.1.0
- v3.0.0
- v2.8.0
- v2.7.0
- v2.6.0
- v2.5.0
- v2.4.0
- v2.3.0
- v2.2.1
- v2.2.0
- v2.1.0
- v2.0.0
- v1.0.0
This package is auto-updated.
Last update: 2025-10-27 08:33:14 UTC
README
This library provides some utils and conventions for web apps. It's main purpose is to be used on Shlink project, but any PHP project can take advantage.
Most of the elements it provides require a PSR-11 container, and it's easy to integrate on mezzio applications thanks to the ConfigProvider it includes.
Install
Install this library using composer:
composer require shlinkio/shlink-common
This library is also a mezzio module which provides its own
ConfigProvider. Add it to your configuration to get everything automatically set up.
Cache
This library provides both PSR-6 and PSR-16 cache adapters, via symfony/cache.
They can be fetched via Psr\Cache\CacheItemPoolInterface and Psr\SimpleCache\CacheInterface.
The concrete implementation they return is different depending on your configuration:
- An
ArrayAdapterinstance when thedebugconfig is set to true or when the APCu extension is not installed and thecache.redisconfig is not defined. - An
ApcuAdapterinstance when nocache.redisis defined and the APCu extension is installed. - A
RedisAdapterinstance when thecache.redisconfig is defined.
The last two adapters will use the namespace defined in cache.namespace config entry.
The three of them will allow setting a default lifetime for those entries which do not explicitly define one, picking it up from cache.default_lifetime.
<?php declare(strict_types=1); return [ 'debug' => false, 'cache' => [ 'namespace' => 'my_namespace', 'default_lifetime' => 86400, // Optional. Defaults to "never expire" 'redis' => [ 'servers' => [ // These should be valid URIs. Make sure credentials are URL-encoded 'tcp://1.1.1.1:6379', 'tcp://2.2.2.2:6379', 'tcp://3.3.3.3:6379/3', // Define a database index to use (https://redis.io/docs/commands/select/) 'tcp://user:pass%40word@4.4.4.4:6379', // Redis ACL (https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/) 'tcp://:password@5.5.5.5:6379', // Redis security (https://redis.io/docs/latest/operate/oss_and_stack/management/security/) 'tls://server_with_encryption:6379', ], 'sentinel_service' => 'my_master', // Optional. 'username' => 'user', // Optional. Ignored if no `sentinel_service` is set. 'password' => 'my_password', // Optional. Ignored if no `sentinel_service` is set. ], ], ];
Redis support
You can allow caching to be done on a redis instance, redis cluster or redis sentinels, by defining some options under cache.redis config.
servers: A list of redis servers. If one is provided, it will be treated as a single instance, and otherwise, a cluster will be assumed.sentinel_service: Lets you enable sentinel mode by providing the master/service name. When provided, the servers will be treated as sentinel instances, not the redis cluster instances.username: Whensentinel_serviceis set, it lets you define the username for the redis cluster instances when using ACL. For non-sentinel contexts, you should provide the username as part of the server URL directly.password: Whensentinel_serviceis set, it lets you define the password for the redis cluster instances. For non-sentinel contexts, you should provide the password as part of the server URL directly.
Redis publishing helper
Also, in order to support publishing in redis pub/sub, a RedisPublishingHelper service is provided, which will use the configuration above in order to connect to the redis instance/cluster.
<?php declare(strict_types=1); use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper; use Shlinkio\Shlink\Common\UpdatePublishing\Update; $helper = $container->get(RedisPublishingHelper::class); $helper->publishUpdate(Update::forTopicAndPayload('some_queue', ['foo' => 'bar']));
Middlewares
This module provides a set of useful middlewares, all registered as services in the container:
-
CloseDbConnectionMiddleware:Should be an early middleware in the pipeline. It makes use of the EntityManager that ensure the database connection is closed at the end of the request.
It should be used when serving an app with a non-blocking IO server (like RoadRunner or FrankenPHP), which persist services between requests.
-
IpAddress(from akrabat/ip-address-middleware package):Improves detection of the remote IP address.
The set of headers which are inspected in order to search for the address can be customized using this configuration:
<?php declare(strict_types=1); return [ 'ip_address_resolution' => [ 'headers_to_inspect' => [ 'CF-Connecting-IP', 'True-Client-IP', 'X-Real-IP', 'Forwarded', 'X-Forwarded-For', 'X-Forwarded', 'X-Cluster-Client-Ip', 'Client-Ip', ], ], ];
-
RequestIdMiddleware: Sets arequest-idattribute to current request, by reading theX-Request-Idheader or falling back to an auto-generated UUID v4.It also implements monolog's
ProcessorInterfaceto set the request ID asextra.request-id.
Doctrine integration
Some doctrine-related services are provided, that can be customized via configuration:
EntityManager
The EntityManager service can be fetched using names em or Doctrine\ORM\EntityManager.
In any case, it will come decorated so that it is reopened automatically after having been closed.
The EntityManager can be customized using this configuration:
<?php declare(strict_types=1); namespace Shlinkio\Shlink\Common; use Doctrine\ORM\Events; return [ 'entity_manager' => [ 'orm' => [ 'proxies_dir' => 'data/proxies', // Directory in which proxies will be persisted 'default_repository_classname' => '', // A FQCN for the class used as repository by default 'entities_mappings' => [ // List of directories from which entities mappings should be read __DIR__ . '/../foo/entities-mappings', __DIR__ . '/../bar/entities-mappings', ], 'types' => [ // List of custom database types to map Doctrine\Type\ChronosDateTimeType::CHRONOS_DATETIME => Doctrine\Type\ChronosDateTimeType::class, ], 'load_mappings_using_functional_style' => true, // Makes loader assume mappings return a function which should be invoked. Defaults to false 'listeners' => [ // Map telling which service listeners to invoke for every ORM event Events::postFlush => ['some_service'], Events::preUpdate => ['foo', 'bar'], ] ], 'connection' => [ // Database connection params 'driver' => 'pdo_mysql', 'host' => 'shlink_db', 'user' => 'DB_USER', 'password' => 'DB_PASSWORD', 'dbname' => 'DB_NAME', 'charset' => 'utf8', ], ], ];
Connections
As well as the EntityManager, there are two Connection objects that can be fetched.
Doctrine\DBAL\Connection: Returns the connection used by the EntityManager, as is.Shlinkio\Shlink\Common\Doctrine\NoDbNameConnection: Returns a connection which is the same used by the EntityManager but without setting the database name. Useful to perform operations like creating the database (which would otherwise fail since the database does not exist yet).
EntityRepository factory
In order to allow multiple repositories per entity, and also to avoid the $this->em->getRepository(MyEntity::class) pattern and instead "promote" injecting repositories, this library provides a EntityRepositoryFactory helper class that can be used like this.
<?php declare(strict_types=1); use Shlinkio\Shlink\Common\Doctrine\EntityRepositoryFactory; return [ 'dependencies' => [ MyEntityRepository::class => [EntityRepositoryFactory::class, MyEntity::class], ], ];
Logger
A few logger-related helpers are provided by this library.
LoggerFactory
The LoggerFactory class is capable of creating Monolog\Logger instances wrapping either stream handlers or rotating file handlers, which should be defined under the logger config entry.
<?php declare(strict_types=1); use Monolog\Level; use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware; use Shlinkio\Shlink\Common\Logger\LoggerFactory; use Shlinkio\Shlink\Common\Logger\LoggerType; return [ 'logger' => [ 'Shlink' => [ 'type' => LoggerType::FILE->value, 'level' => Level::Info->value, 'processors' => [RequestIdMiddleware::class], 'formatter' => [ 'type' => 'console', // 'console' or 'json'. Defaults to 'console' // If 'console' type is defined, you can define the line format 'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%', ], ], 'Access' => [ 'type' => LoggerType::STREAM->value, 'level' => Level::Alert->value, 'formatter' => [ 'line_format' => '[%datetime%] %level_name% - %message%', 'add_new_line' => false, ], ], ], 'dependencies' => [ 'factories' => [ 'ShlinkLogger' => [LoggerFactory::class, 'Shlink'], 'AccessLogger' => [LoggerFactory::class, 'Access'], ], ], ];
Every logger can have these config options:
type: Any value from theLoggerTypeenum, which will make different handlers to be injected in the logger instance.level: Any value from monolog'sLevelenum, which determines the minimum level of the generated logs. Defaults toLevel::Infoif not provided.line_format: The format of the line logs to generate.add_new_line: Whether to add an extra empty line on every log. Defaults totrue.processors: An optional list of extra processors to inject in the generated logger. The values in the array must be service names.destination: Where to send logs. It defaults tophp:stdoutfor stream logs, anddata/log/shlink_log.logfor file logs.
Other logger utils
This module provides some other logger-related utilities:
ExceptionWithNewLineProcessor: A monolog processor which captures the{e}pattern inside log messages, and prepends a new line before it, assuming you are going to replace that with an exception trace.LoggerAwareDelegatorFactory: A ServiceManager delegator factory that checks if the service returned by previous factory is aPsr\Log\LoggerAwareInterfaceinstance. If it is, it sets thePsr\Log\LoggerInterfaceservice on it (if it was registered).ErrorLogger: A callable which expects aPsr\Log\LoggerInterfaceto be injected and uses it to log aThrowablewhen invoked. It will log 5xx errors with error level and 4xx errors with debug level.ErrorHandlerListenerAttachingDelegator: A ServiceManager delegator factory that registers all the services configured undererror_handler.listenersas listeners for a stratigilityErrorHandleror aProblemDetailsMiddleware.BackwardsCompatibleMonologProcessor: It lets you wrap monolog 2 processors withcallable(array): arraysignature to make them compatible with monolog 3 and its newcallable(LogRecord): LogRecordsignature.AccessLogMiddleware: A PSR-15 middleware which logs requests. It expects a PSR-3 logger service to be registered underAccessLogMiddleware::LOGGER_SERVICE_NAME.
HTTP Client
A guzzle HTTP client comes preregistered, under the GuzzleHttp\Client service name, and aliased by httpClient.
It can be customized by adding request and response middlewares using a configuration like this:
<?php declare(strict_types=1); use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; return [ 'http_client' => [ 'request_middlewares' => [ 'some_service_middleware', fn (RequestInterface $req): RequestInterface => $req->withHeader('X-Foo', 'bar'), ], 'response_middlewares' => [ 'some_service_middleware', fn (ResponseInterface $res): ResponseInterface => $res->withHeader('X-Foo', 'bar'), ], ], ];
Middlewares can be registered as static callbacks with a signature like the one from the example or as service names which resolve to a service with that same signature.
Mercure
A helper to publish updates on a mercure hub comes preregistered. You need to provide a configuration like this one:
<?php declare(strict_types=1); return [ 'mercure' => [ // Whether the integration with mercure is enabled or not. // If not explicitly set, the integration is considered enabled if a public URL is set, but next major version // will change the default to false. 'enabled' => true, // A URL publicly available in which the mercure hub can be reached. 'public_hub_url' => null, // Optional. An internal URL in which the mercure hub can be reached. Will fall back to public_hub_url if not provided. 'internal_hub_url' => null, // The JWT secret you provided to the mercure hub as JWT_KEY, so that valid JWTs can be generated. 'jwt_secret' => null, // Optional. The issuer for generated JWTs. Will fall back to "Shlink". 'jwt_issuer' => 'Shlink', ], ];
After that, you can get the publisher from the container, and invoke it to publish updates for specific topics:
<?php declare(strict_types=1); use Symfony\Component\Mercure\Publisher; use Symfony\Component\Mercure\Update; $publisher = $container->get(Publisher::class); $publisher(new Update('some_topic', json_encode([ 'foo' => 'bar', ])));
Find more info about the symfony/mercure component here: https://symfony.com/blog/symfony-gets-real-time-push-capabilities
RabbitMQ
A helper to publish updates on RabbitMQ comes preregistered. You need to provide a configuration like this one:
<?php declare(strict_types=1); return [ 'rabbitmq' => [ // The RabbitMQ server name 'host' => 'my-rabbitmq-server.com', // The RabbitMQ server port 'port' => '5672', // The username credential 'user' => 'username', // The password credential 'password' => 'password', // The vHost 'vhost' => '/', // Tells if connection should be encrypted. Defaults to false if not provided 'use_ssl' => true, ], ];
After that, you can get the helper from the container, and invoke it to publish updates for specific queues:
<?php declare(strict_types=1); use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper; use Shlinkio\Shlink\Common\UpdatePublishing\Update; $helper = $container->get(RabbitMqPublishingHelper::class); $helper->publishUpdate(Update::forTopicAndPayload('some_queue', ['foo' => 'bar']));
Utils
PagerfantaUtilsTrait: A trait providing methods to get useful info fromPagerfanta\Pagerfantaobjects. It requires that you installpagerfanta/core.Paginator: An object extendingPagerfanta, that makes it behave as laminas' Paginator object on regards to be able to set-1as the max results and get all the results in that case. It requires that you installpagerfanta/core.DateRange: An immutable value object wrapping twoChronosdate objects that can be used to represent a time period between two dates.IpAddress: An immutable value object representing an IP address that can be copied into an anonymized instance which removes the last octet.NamespacedStore: Asymfony/lockstore that can wrap another store instance but making sure keys are prefixed with a namespace and namespace separator.