Merge pull request #462 from acelaya/feature/external-shlink-common

Feature/external shlink common
This commit is contained in:
Alejandro Celaya 2019-08-12 18:43:34 +02:00 committed by GitHub
commit 456765e55b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 23 additions and 2683 deletions

View file

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* [#450](https://github.com/shlinkio/shlink/issues/450) Added PHP 7.4 to the build matrix, as an allowed-to-fail env. * [#450](https://github.com/shlinkio/shlink/issues/450) Added PHP 7.4 to the build matrix, as an allowed-to-fail env.
* [#443](https://github.com/shlinkio/shlink/issues/443) Split some logic into independent modules. * [#443](https://github.com/shlinkio/shlink/issues/443) Split some logic into independent modules.
* [#451](https://github.com/shlinkio/shlink/issues/451) Updated to infection 0.13.
#### Deprecated #### Deprecated

View file

@ -25,19 +25,20 @@
"endroid/qr-code": "^1.7", "endroid/qr-code": "^1.7",
"firebase/php-jwt": "^4.0", "firebase/php-jwt": "^4.0",
"geoip2/geoip2": "^2.9", "geoip2/geoip2": "^2.9",
"guzzlehttp/guzzle": "^6.2", "guzzlehttp/guzzle": "^6.3",
"lstrojny/functional-php": "^1.8", "lstrojny/functional-php": "^1.9",
"mikehaertl/phpwkhtmltopdf": "^2.2", "mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21", "monolog/monolog": "^1.24",
"ocramius/proxy-manager": "~2.2.2", "ocramius/proxy-manager": "~2.2.2",
"phly/phly-event-dispatcher": "^1.0", "phly/phly-event-dispatcher": "^1.0",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"shlinkio/shlink-common": "^1.0",
"shlinkio/shlink-installer": "^1.2.1", "shlinkio/shlink-installer": "^1.2.1",
"symfony/console": "^4.3", "symfony/console": "^4.3",
"symfony/filesystem": "^4.3", "symfony/filesystem": "^4.3",
"symfony/lock": "^4.3", "symfony/lock": "^4.3",
"symfony/process": "^4.3", "symfony/process": "^4.3",
"theorchard/monolog-cascade": "^0.4", "theorchard/monolog-cascade": "^0.5",
"zendframework/zend-config": "^3.3", "zendframework/zend-config": "^3.3",
"zendframework/zend-config-aggregator": "^1.1", "zendframework/zend-config-aggregator": "^1.1",
"zendframework/zend-diactoros": "^2.1.3", "zendframework/zend-diactoros": "^2.1.3",
@ -56,7 +57,7 @@
"devster/ubench": "^2.0", "devster/ubench": "^2.0",
"eaglewu/swoole-ide-helper": "dev-master", "eaglewu/swoole-ide-helper": "dev-master",
"filp/whoops": "^2.4", "filp/whoops": "^2.4",
"infection/infection": "^0.12.2", "infection/infection": "^0.13.4",
"phpstan/phpstan": "^0.11.2", "phpstan/phpstan": "^0.11.2",
"phpunit/phpcov": "^6.0", "phpunit/phpcov": "^6.0",
"phpunit/phpunit": "^8.3", "phpunit/phpunit": "^8.3",
@ -73,13 +74,11 @@
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src", "Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src", "Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src", "Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\Common\\": "module/Common/src",
"Shlinkio\\Shlink\\EventDispatcher\\": "module/EventDispatcher/src", "Shlinkio\\Shlink\\EventDispatcher\\": "module/EventDispatcher/src",
"Shlinkio\\Shlink\\IpGeolocation\\": "module/IpGeolocation/src/", "Shlinkio\\Shlink\\IpGeolocation\\": "module/IpGeolocation/src/",
"Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/" "Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/"
}, },
"files": [ "files": [
"module/Common/functions/functions.php",
"module/EventDispatcher/functions/functions.php" "module/EventDispatcher/functions/functions.php"
] ]
}, },
@ -92,7 +91,6 @@
"module/Core/test", "module/Core/test",
"module/Core/test-db" "module/Core/test-db"
], ],
"ShlinkioTest\\Shlink\\Common\\": "module/Common/test",
"ShlinkioTest\\Shlink\\EventDispatcher\\": "module/EventDispatcher/test", "ShlinkioTest\\Shlink\\EventDispatcher\\": "module/EventDispatcher/test",
"ShlinkioTest\\Shlink\\IpGeolocation\\": "module/IpGeolocation/test", "ShlinkioTest\\Shlink\\IpGeolocation\\": "module/IpGeolocation/test",
"ShlinkioTest\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/test" "ShlinkioTest\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/test"
@ -108,7 +106,7 @@
"cs": "phpcs", "cs": "phpcs",
"cs:fix": "phpcbf", "cs:fix": "phpcbf",
"stan": "phpstan analyse module/*/src/ --level=5 -c phpstan.neon", "stan": "phpstan analyse module/*/src/ module/*/config config --level=5 -c phpstan.neon",
"test": [ "test": [
"@test:unit", "@test:unit",

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2019 Alejandro Celaya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,89 +0,0 @@
# Shlink Common
This library provides some utils and conventions for web apps. It's main purpose is to be used on [Shlink](https://github.com/shlinkio/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 [expressive] applications thanks to the `ConfigProvider` it includes.
## Install
Install this library using composer:
composer require shlinkio/shlink-common
> This library is also an expressive module which provides its own `ConfigProvider`. Add it to your configuration to get everything automatically set up.
## Cache
A [doctrine cache] adapter is registered, which returns different instances depending on your configuration:
* An `ArrayCache` instance when the `debug` config is set to true or when the APUc extension is not installed and the `cache.redis` config is not defined.
* An `ApcuCache`instance when no `cache.redis` is defined and the APCu extension is installed.
* A `PredisCache` instance when the `cache.redis` config is defined.
Any of the adapters will use the namespace defined in `cache.namespace` config entry.
```php
<?php
declare(strict_types=1);
return [
'debug' => false,
'cache' => [
'namespace' => 'my_namespace',
'redis' => [
'servers' => [
'tcp://1.1.1.1:6379',
'tcp://2.2.2.2:6379',
'tcp://3.3.3.3:6379',
],
],
],
];
```
When the `cache.redis` config is provided, a set of servers is expected. If only one server is provided, this library will treat it as a regular server, but if several servers are defined, it will treat them as a redis cluster and expect the servers to be configured as such.
## Middlewares
This module provides a set of useful middlewares, all registered as services in the container:
* **CloseDatabaseConnectionMiddleware**:
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 Swoole or ReactPHP), which persist services between requests.
* **LocaleMiddleware**:
Sets the locale in the translator, based on the `Accapt-Language` header.
* **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
<?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',
],
],
];
```

View file

@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use Doctrine\Common\Cache as DoctrineCache;
return [
'dependencies' => [
'factories' => [
DoctrineCache\Cache::class => Cache\CacheFactory::class,
Cache\RedisFactory::SERVICE_NAME => Cache\RedisFactory::class,
],
],
];

View file

@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use GuzzleHttp\Client as GuzzleClient;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use RKA\Middleware\IpAddress;
use Symfony\Component\Filesystem\Filesystem;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
'factories' => [
GuzzleClient::class => InvokableFactory::class,
Filesystem::class => InvokableFactory::class,
Translator::class => I18n\TranslatorFactory::class,
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
Middleware\CloseDbConnectionMiddleware::class => ConfigAbstractFactory::class,
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
],
'aliases' => [
'httpClient' => GuzzleClient::class,
'translator' => Translator::class,
'logger' => LoggerInterface::class,
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
],
'abstract_factories' => [
Factory\DottedAccessConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Middleware\CloseDbConnectionMiddleware::class => ['em'],
],
];

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
return [
'entity_manager' => [
'orm' => [
'types' => [
Doctrine\Type\ChronosDateTimeType::CHRONOS_DATETIME => Doctrine\Type\ChronosDateTimeType::class,
],
],
],
'dependencies' => [
'factories' => [
EntityManager::class => Doctrine\EntityManagerFactory::class,
Connection::class => Doctrine\ConnectionFactory::class,
Doctrine\NoDbNameConnectionFactory::SERVICE_NAME => Doctrine\NoDbNameConnectionFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
],
'delegators' => [
EntityManager::class => [
Doctrine\ReopeningEntityManagerDelegator::class,
],
],
],
];

View file

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common\Template\Extension\TranslatorExtension;
return [
'plates' => [
'extensions' => [
TranslatorExtension::class,
],
],
];

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use Zend\Config\Factory;
use Zend\Stdlib\Glob;
use function getenv;
use function json_decode as spl_json_decode;
use function json_last_error;
use function json_last_error_msg;
use function sprintf;
use function strtolower;
use function trim;
use const JSON_ERROR_NONE;
/**
* Gets the value of an environment variable. Supports boolean, empty and null.
* This is basically Laravel's env helper
*
* @param string $key
* @param mixed $default
* @return mixed
* @link https://github.com/laravel/framework/blob/5.2/src/Illuminate/Foundation/helpers.php#L369
*/
function env($key, $default = null)
{
$value = getenv($key);
if ($value === false) {
return $default;
}
switch (strtolower($value)) {
case 'true':
case '(true)':
return true;
case 'false':
case '(false)':
return false;
case 'empty':
case '(empty)':
return '';
case 'null':
case '(null)':
return null;
}
return trim($value);
}
/**
* @throws Exception\InvalidArgumentException
*/
function json_decode(string $json, int $depth = 512, int $options = 0): array
{
$data = spl_json_decode($json, true, $depth, $options);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new Exception\InvalidArgumentException(sprintf('Error decoding JSON: %s', json_last_error_msg()));
}
return $data;
}
/**
* Loads configuration files which match provided glob pattern, and returns the merged result as array
*/
function loadConfigFromGlob(string $globPattern): array
{
return Factory::fromFiles(Glob::glob($globPattern, Glob::GLOB_BRACE));
}

View file

@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Cache;
use Doctrine\Common\Cache;
use Predis\Client as PredisClient;
use Psr\Container\ContainerInterface;
use function extension_loaded;
class CacheFactory
{
/** @var callable|null */
private $apcuEnabled;
public function __construct(?callable $apcuEnabled = null)
{
$this->apcuEnabled = $apcuEnabled ?? function () {
return extension_loaded('apcu');
};
}
public function __invoke(ContainerInterface $container): Cache\CacheProvider
{
$config = $container->get('config');
$adapter = $this->buildAdapter($config, $container);
$adapter->setNamespace($config['cache']['namespace'] ?? '');
return $adapter;
}
private function buildAdapter(array $config, ContainerInterface $container): Cache\CacheProvider
{
$isDebug = (bool) ($config['debug'] ?? false);
$redisConfig = $config['cache']['redis'] ?? null;
$apcuEnabled = ($this->apcuEnabled)();
if ($isDebug || (! $apcuEnabled && $redisConfig === null)) {
return new Cache\ArrayCache();
}
if ($redisConfig === null) {
return new Cache\ApcuCache();
}
/** @var PredisClient $predis */
$predis = $container->get(RedisFactory::SERVICE_NAME);
return new Cache\PredisCache($predis);
}
}

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Cache;
use Predis\Client as PredisClient;
use Psr\Container\ContainerInterface;
use function count;
use function explode;
use function is_string;
class RedisFactory
{
public const SERVICE_NAME = 'Shlinkio\Shlink\Common\Cache\Redis';
public function __invoke(ContainerInterface $container): PredisClient
{
$config = $container->get('config');
$redisConfig = $config['cache']['redis'] ?? $config['redis'] ?? [];
$servers = $redisConfig['servers'] ?? [];
$servers = is_string($servers) ? explode(',', $servers) : $servers;
$options = count($servers) <= 1 ? null : ['cluster' => 'redis'];
return new PredisClient($servers, $options);
}
}

View file

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
class ConfigProvider
{
public function __invoke(): array
{
return loadConfigFromGlob(__DIR__ . '/../config/{,*.}config.php');
}
}

View file

@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Doctrine;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
class ConnectionFactory
{
public function __invoke(ContainerInterface $container): Connection
{
$em = $container->get(EntityManager::class);
return $em->getConnection();
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Doctrine;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Persistence\Mapping\Driver\PHPDriver;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Tools\Setup;
use Psr\Container\ContainerInterface;
class EntityManagerFactory
{
/**
* @throws ORMException
* @throws DBALException
*/
public function __invoke(ContainerInterface $container): EntityManager
{
$globalConfig = $container->get('config');
$isDevMode = (bool) ($globalConfig['debug'] ?? false);
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
$emConfig = $globalConfig['entity_manager'] ?? [];
$connectionConfig = $emConfig['connection'] ?? [];
$ormConfig = $emConfig['orm'] ?? [];
$this->registerTypes($ormConfig);
$config = Setup::createConfiguration($isDevMode, $ormConfig['proxies_dir'] ?? null, $cache);
$config->setMetadataDriverImpl(new PHPDriver($ormConfig['entities_mappings'] ?? []));
return EntityManager::create($connectionConfig, $config);
}
/**
* @throws DBALException
*/
private function registerTypes(array $ormConfig): void
{
$types = $ormConfig['types'] ?? [];
foreach ($types as $name => $className) {
if (! Type::hasType($name)) {
Type::addType($name, $className);
}
}
}
}

View file

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Doctrine;
use Doctrine\DBAL\Connection;
use Psr\Container\ContainerInterface;
class NoDbNameConnectionFactory
{
public const SERVICE_NAME = 'Shlinkio\Shlink\Common\Doctrine\NoDbNameConnection';
public function __invoke(ContainerInterface $container): Connection
{
$conn = $container->get(Connection::class);
$params = $conn->getParams();
unset($params['dbname']);
return new Connection($params, $conn->getDriver(), $conn->getConfiguration(), $conn->getEventManager());
}
}

View file

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Doctrine;
use Doctrine\ORM\Decorator\EntityManagerDecorator;
use Doctrine\ORM\EntityManagerInterface;
class ReopeningEntityManager extends EntityManagerDecorator
{
/** @var callable */
private $emFactory;
public function __construct(EntityManagerInterface $wrapped, callable $emFactory)
{
parent::__construct($wrapped);
$this->emFactory = $emFactory;
}
protected function getWrappedEntityManager(): EntityManagerInterface
{
if (! $this->wrapped->isOpen()) {
$this->wrapped = ($this->emFactory)(
$this->wrapped->getConnection(),
$this->wrapped->getConfiguration(),
$this->wrapped->getEventManager()
);
}
return $this->wrapped;
}
public function flush($entity = null): void
{
$this->getWrappedEntityManager()->flush($entity);
}
public function persist($object): void
{
$this->getWrappedEntityManager()->persist($object);
}
public function remove($object): void
{
$this->getWrappedEntityManager()->remove($object);
}
public function refresh($object): void
{
$this->getWrappedEntityManager()->refresh($object);
}
public function merge($object)
{
return $this->getWrappedEntityManager()->merge($object);
}
}

View file

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Doctrine;
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
class ReopeningEntityManagerDelegator
{
public function __invoke(ContainerInterface $container, string $name, callable $callback): ReopeningEntityManager
{
return new ReopeningEntityManager($callback(), [EntityManager::class, 'create']);
}
}

View file

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Doctrine\Type;
use Cake\Chronos\Chronos;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeImmutableType;
class ChronosDateTimeType extends DateTimeImmutableType
{
public const CHRONOS_DATETIME = 'chronos_datetime';
public function getName(): string
{
return self::CHRONOS_DATETIME;
}
/**
* @throws ConversionException
*/
public function convertToPHPValue($value, AbstractPlatform $platform): ?Chronos
{
if ($value === null) {
return null;
}
$dateTime = parent::convertToPHPValue($value, $platform);
return Chronos::instance($dateTime);
}
/**
* @throws ConversionException
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if (null === $value) {
return $value;
}
if ($value instanceof DateTimeInterface) {
return $value->format($platform->getDateTimeFormatString());
}
throw ConversionException::conversionFailedInvalidType(
$value,
$this->getName(),
['null', DateTimeInterface::class]
);
}
}

View file

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Entity;
abstract class AbstractEntity
{
/** @var string */
protected $id;
public function getId(): string
{
return $this->id;
}
/**
* @internal
*/
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
}

View file

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}

View file

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
use InvalidArgumentException as SplInvalidArgumentException;
class InvalidArgumentException extends SplInvalidArgumentException implements ExceptionInterface
{
}

View file

@ -1,86 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Factory;
use ArrayAccess;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
use function array_shift;
use function explode;
use function is_array;
use function sprintf;
use function substr_count;
class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
{
/**
* Can the factory create an instance for the service?
*
* @param string $requestedName
*/
public function canCreate(ContainerInterface $container, $requestedName): bool
{
return substr_count($requestedName, '.') > 0;
}
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws InvalidArgumentException
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
{
$parts = explode('.', $requestedName);
$serviceName = array_shift($parts);
if (! $container->has($serviceName)) {
throw new ServiceNotCreatedException(sprintf(
'Defined service "%s" could not be found in container after resolving dotted expression "%s".',
$serviceName,
$requestedName
));
}
$array = $container->get($serviceName);
return $this->readKeysFromArray($parts, $array);
}
/**
* @param array $keys
* @param array|\ArrayAccess $array
* @return mixed|null
* @throws InvalidArgumentException
*/
private function readKeysFromArray(array $keys, $array)
{
$key = array_shift($keys);
// When one of the provided keys is not found, throw an exception
if (! isset($array[$key])) {
throw new InvalidArgumentException(sprintf(
'The key "%s" provided in the dotted notation could not be found in the array service',
$key
));
}
$value = $array[$key];
if (! empty($keys) && (is_array($value) || $value instanceof ArrayAccess)) {
$value = $this->readKeysFromArray($keys, $value);
}
return $value;
}
}

View file

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\I18n;
use Interop\Container\ContainerInterface;
use Zend\I18n\Translator\Translator;
class TranslatorFactory
{
public function __invoke(ContainerInterface $container): Translator
{
$config = $container->get('config');
return Translator::factory($config['translator'] ?? []);
}
}

View file

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Lock;
use Interop\Container\ContainerInterface;
use Symfony\Component\Lock\Store\RetryTillSaveStore;
use Symfony\Component\Lock\StoreInterface;
class RetryLockStoreDelegatorFactory
{
public function __invoke(ContainerInterface $container, $name, callable $callback): RetryTillSaveStore
{
/** @var StoreInterface $originalStore */
$originalStore = $callback();
return new RetryTillSaveStore($originalStore);
}
}

View file

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Logger;
use Psr\Container\ContainerInterface;
use Psr\Log;
class LoggerAwareDelegatorFactory
{
public function __invoke(ContainerInterface $container, $name, callable $callback)
{
$instance = $callback();
if ($instance instanceof Log\LoggerAwareInterface) {
$instance->setLogger($container->get(Log\LoggerInterface::class));
}
return $instance;
}
}

View file

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Logger;
use Cascade\Cascade;
use Interop\Container\ContainerInterface;
use Monolog\Logger;
use function count;
use function explode;
class LoggerFactory
{
public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): Logger
{
$config = $container->has('config') ? $container->get('config') : [];
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);
// Compose requested logger name
$loggerName = $options['logger_name'] ?? 'Logger';
$nameParts = explode('_', $requestedName);
if (count($nameParts) > 1) {
$loggerName = $nameParts[1];
}
return Cascade::getLogger($loggerName);
}
}

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Logger\Processor;
use function str_replace;
use function strpos;
use const PHP_EOL;
final class ExceptionWithNewLineProcessor
{
private const EXCEPTION_PLACEHOLDER = '{e}';
public function __invoke(array $record)
{
$message = $record['message'];
$messageHasExceptionPlaceholder = strpos($message, self::EXCEPTION_PLACEHOLDER) !== false;
if ($messageHasExceptionPlaceholder) {
$record['message'] = str_replace(
self::EXCEPTION_PLACEHOLDER,
PHP_EOL . self::EXCEPTION_PLACEHOLDER,
$message
);
}
return $record;
}
}

View file

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CloseDbConnectionMiddleware implements MiddlewareInterface
{
/** @var EntityManagerInterface */
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} finally {
$this->em->getConnection()->close();
$this->em->clear();
}
}
}

View file

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Psr\Container\ContainerInterface;
use RKA\Middleware\IpAddress;
class IpAddressMiddlewareFactory
{
public const REQUEST_ATTR = 'remote_address';
public function __invoke(ContainerInterface $container): IpAddress
{
$config = $container->get('config');
$headersToInspect = $config['ip_address_resolution']['headers_to_inspect'] ?? [];
return new IpAddress(true, [], self::REQUEST_ATTR, $headersToInspect);
}
}

View file

@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as DelegateInterface;
use Zend\I18n\Translator\Translator;
use function count;
use function explode;
class LocaleMiddleware implements MiddlewareInterface
{
private const ACCEPT_LANGUAGE = 'Accept-Language';
/** @var Translator */
private $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate): Response
{
if (! $request->hasHeader(self::ACCEPT_LANGUAGE)) {
return $delegate->handle($request);
}
$locale = $request->getHeaderLine(self::ACCEPT_LANGUAGE);
$this->translator->setLocale($this->normalizeLocale($locale));
return $delegate->handle($request);
}
private function normalizeLocale(string $locale): string
{
$parts = explode('_', $locale);
if (count($parts) > 1) {
return $parts[0];
}
$parts = explode('-', $locale);
if (count($parts) > 1) {
return $parts[0];
}
return $locale;
}
}

View file

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Paginator\Util;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Zend\Paginator\Paginator;
use Zend\Stdlib\ArrayUtils;
use function array_map;
use function sprintf;
trait PaginatorUtilsTrait
{
private function serializePaginator(Paginator $paginator, ?DataTransformerInterface $transformer = null): array
{
return [
'data' => $this->serializeItems(ArrayUtils::iteratorToArray($paginator->getCurrentItems()), $transformer),
'pagination' => [
'currentPage' => $paginator->getCurrentPageNumber(),
'pagesCount' => $paginator->count(),
'itemsPerPage' => $paginator->getItemCountPerPage(),
'itemsInCurrentPage' => $paginator->getCurrentItemCount(),
'totalItems' => $paginator->getTotalItemCount(),
],
];
}
private function serializeItems(array $items, ?DataTransformerInterface $transformer = null): array
{
return $transformer === null ? $items : array_map([$transformer, 'transform'], $items);
}
private function isLastPage(Paginator $paginator): bool
{
return $paginator->getCurrentPageNumber() >= $paginator->count();
}
private function formatCurrentPageMessage(Paginator $paginator, string $pattern): string
{
return sprintf($pattern, $paginator->getCurrentPageNumber(), $paginator->count());
}
}

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Response;
use Fig\Http\Message\StatusCodeInterface as StatusCode;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function base64_decode;
class PixelResponse extends Response
{
private const BASE_64_IMAGE = 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==';
private const CONTENT_TYPE = 'image/gif';
public function __construct(int $status = StatusCode::STATUS_OK, array $headers = [])
{
$headers['content-type'] = self::CONTENT_TYPE;
parent::__construct($this->createBody(), $status, $headers);
}
/**
* Create the message body.
*
* @return StreamInterface
*/
private function createBody(): StreamInterface
{
$body = new Stream('php://temp', 'wb+');
$body->write(base64_decode(self::BASE_64_IMAGE));
$body->rewind();
return $body;
}
}

View file

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Response;
use Endroid\QrCode\QrCode;
use Fig\Http\Message\StatusCodeInterface as StatusCode;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
class QrCodeResponse extends Response
{
use Response\InjectContentTypeTrait;
public function __construct(QrCode $qrCode, int $status = StatusCode::STATUS_OK, array $headers = [])
{
parent::__construct(
$this->createBody($qrCode),
$status,
$this->injectContentType($qrCode->getContentType(), $headers)
);
}
private function createBody(QrCode $qrCode): StreamInterface
{
$body = new Stream('php://temp', 'wb+');
$body->write($qrCode->get());
$body->rewind();
return $body;
}
}

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Response;
use Fig\Http\Message\StatusCodeInterface as StatusCode;
use finfo;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use Zend\Stdlib\ArrayUtils;
use const FILEINFO_MIME;
trait ResponseUtilsTrait
{
private function generateImageResponse(string $imagePath): ResponseInterface
{
return $this->generateBinaryResponse($imagePath);
}
private function generateBinaryResponse(string $path, array $extraHeaders = []): ResponseInterface
{
$body = new Stream($path);
return new Response($body, StatusCode::STATUS_OK, ArrayUtils::merge([
'Content-Type' => (new finfo(FILEINFO_MIME))->file($path),
'Content-Length' => (string) $body->getSize(),
], $extraHeaders));
}
}

View file

@ -1,9 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Rest;
interface DataTransformerInterface
{
public function transform($value): array;
}

View file

@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Template\Extension;
use League\Plates\Engine;
use League\Plates\Extension\ExtensionInterface;
use Zend\I18n\Translator\TranslatorInterface;
class TranslatorExtension implements ExtensionInterface
{
/** @var TranslatorInterface */
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function register(Engine $engine): void
{
$engine->registerFunction('translate', [$this->translator, 'translate']);
$engine->registerFunction('locale', [$this->translator, 'getLocale']);
}
}

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
use Cake\Chronos\Chronos;
final class DateRange
{
/** @var Chronos|null */
private $startDate;
/** @var Chronos|null */
private $endDate;
public function __construct(?Chronos $startDate = null, ?Chronos $endDate = null)
{
$this->startDate = $startDate;
$this->endDate = $endDate;
}
public function getStartDate(): ?Chronos
{
return $this->startDate;
}
public function getEndDate(): ?Chronos
{
return $this->endDate;
}
public function isEmpty(): bool
{
return $this->startDate === null && $this->endDate === null;
}
}

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use function count;
use function explode;
use function implode;
use function sprintf;
use function trim;
final class IpAddress
{
private const IPV4_PARTS_COUNT = 4;
private const OBFUSCATED_OCTET = '0';
public const LOCALHOST = '127.0.0.1';
/** @var string */
private $firstOctet;
/** @var string */
private $secondOctet;
/** @var string */
private $thirdOctet;
/** @var string */
private $fourthOctet;
private function __construct(string $firstOctet, string $secondOctet, string $thirdOctet, string $fourthOctet)
{
$this->firstOctet = $firstOctet;
$this->secondOctet = $secondOctet;
$this->thirdOctet = $thirdOctet;
$this->fourthOctet = $fourthOctet;
}
/**
* @param string $address
* @return IpAddress
* @throws InvalidArgumentException
*/
public static function fromString(string $address): self
{
$address = trim($address);
$parts = explode('.', $address);
if (count($parts) !== self::IPV4_PARTS_COUNT) {
throw new InvalidArgumentException(sprintf('Provided IP "%s" is invalid', $address));
}
return new self(...$parts);
}
public function getObfuscatedCopy(): self
{
return new self(
$this->firstOctet,
$this->secondOctet,
$this->thirdOctet,
self::OBFUSCATED_OCTET
);
}
public function __toString(): string
{
return implode('.', [
$this->firstOctet,
$this->secondOctet,
$this->thirdOctet,
$this->fourthOctet,
]);
}
}

View file

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
use function random_int;
use function sprintf;
use function strlen;
trait StringUtilsTrait
{
private function generateRandomString(int $length = 10): string
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
private function generateV4Uuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
random_int(0, 0xffff),
random_int(0, 0xffff),
// 16 bits for "time_mid"
random_int(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
random_int(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
random_int(0, 0x3fff) | 0x8000,
// 48 bits for "node"
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff)
);
}
}

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Validation;
use Zend\Filter;
use Zend\InputFilter\Input;
use Zend\Validator;
trait InputFactoryTrait
{
private function createInput($name, $required = true): Input
{
$input = new Input($name);
$input->setRequired($required)
->getFilterChain()->attach(new Filter\StripTags())
->attach(new Filter\StringTrim());
return $input;
}
private function createBooleanInput(string $name, bool $required = true): Input
{
$input = $this->createInput($name, $required);
$input->getFilterChain()->attach(new Filter\Boolean());
$input->getValidatorChain()->attach(new Validator\NotEmpty(['type' => [
Validator\NotEmpty::OBJECT,
Validator\NotEmpty::SPACE,
Validator\NotEmpty::NULL,
Validator\NotEmpty::EMPTY_ARRAY,
Validator\NotEmpty::STRING,
]]));
return $input;
}
}

View file

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Validation;
use Cocur\Slugify;
use Zend\Filter\Exception;
use Zend\Filter\FilterInterface;
class SluggerFilter implements FilterInterface
{
/** @var Slugify\SlugifyInterface */
private $slugger;
public function __construct(?Slugify\SlugifyInterface $slugger = null)
{
$this->slugger = $slugger ?: new Slugify\Slugify(['lowercase' => false]);
}
/**
* Returns the result of filtering $value
*
* @param mixed $value
* @throws Exception\RuntimeException If filtering $value is impossible
* @return mixed
*/
public function filter($value)
{
return ! empty($value) ? $this->slugger->slugify($value) : null;
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Cache;
use Doctrine\Common\Cache;
use PHPUnit\Framework\TestCase;
use Predis\ClientInterface;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Cache\CacheFactory;
use Shlinkio\Shlink\Common\Cache\RedisFactory;
class CacheFactoryTest extends TestCase
{
/** @var ObjectProphecy */
private $container;
public function setUp(): void
{
$this->container = $this->prophesize(ContainerInterface::class);
}
/**
* @test
* @dataProvider provideCacheConfig
*/
public function expectedCacheAdapterIsReturned(
array $config,
string $expectedAdapterClass,
string $expectedNamespace,
?callable $apcuEnabled = null
): void {
$factory = new CacheFactory($apcuEnabled);
$getConfig = $this->container->get('config')->willReturn($config);
$getRedis = $this->container->get(RedisFactory::SERVICE_NAME)->willReturn(
$this->prophesize(ClientInterface::class)->reveal()
);
$cache = $factory($this->container->reveal());
$this->assertInstanceOf($expectedAdapterClass, $cache);
$this->assertEquals($expectedNamespace, $cache->getNamespace());
$getConfig->shouldHaveBeenCalledOnce();
$getRedis->shouldHaveBeenCalledTimes($expectedAdapterClass === Cache\PredisCache::class ? 1 :0);
}
public function provideCacheConfig(): iterable
{
yield 'debug true' => [['debug' => true], Cache\ArrayCache::class, ''];
yield 'debug false' => [['debug' => false], Cache\ApcuCache::class, ''];
yield 'no debug' => [[], Cache\ApcuCache::class, ''];
yield 'with redis' => [['cache' => [
'namespace' => $namespace = 'some_namespace',
'redis' => [],
]], Cache\PredisCache::class, $namespace];
yield 'debug false and no apcu' => [['debug' => false], Cache\ArrayCache::class, '', function () {
return false;
}];
}
}

View file

@ -1,79 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Cache;
use PHPUnit\Framework\TestCase;
use Predis\Connection\Aggregate\PredisCluster;
use Predis\Connection\Aggregate\RedisCluster;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Cache\RedisFactory;
class RedisFactoryTest extends TestCase
{
/** @var RedisFactory */
private $factory;
/** @var ObjectProphecy */
private $container;
public function setUp(): void
{
$this->container = $this->prophesize(ContainerInterface::class);
$this->factory = new RedisFactory();
}
/**
* @test
* @dataProvider provideRedisConfig
*/
public function createsRedisClientBasedOnRedisConfig(?array $config, string $expectedCluster): void
{
$getConfig = $this->container->get('config')->willReturn([
'redis' => $config,
]);
$client = ($this->factory)($this->container->reveal());
$getConfig->shouldHaveBeenCalledOnce();
$this->assertInstanceOf($expectedCluster, $client->getOptions()->cluster);
}
/**
* @test
* @dataProvider provideRedisConfig
*/
public function createsRedisClientBasedOnCacheConfig(?array $config, string $expectedCluster): void
{
$getConfig = $this->container->get('config')->willReturn([
'cache' => [
'redis' => $config,
],
]);
$client = ($this->factory)($this->container->reveal());
$getConfig->shouldHaveBeenCalledOnce();
$this->assertInstanceOf($expectedCluster, $client->getOptions()->cluster);
}
public function provideRedisConfig(): iterable
{
yield 'no config' => [null, PredisCluster::class];
yield 'single server as string' => [[
'servers' => 'tcp://127.0.0.1:6379',
], PredisCluster::class];
yield 'single server as array' => [[
'servers' => ['tcp://127.0.0.1:6379'],
], PredisCluster::class];
yield 'cluster of servers' => [[
'servers' => ['tcp://1.1.1.1:6379', 'tcp://2.2.2.2:6379'],
], RedisCluster::class];
yield 'empty cluster of servers' => [[
'servers' => [],
], PredisCluster::class];
yield 'cluster of servers as string' => [[
'servers' => 'tcp://1.1.1.1:6379,tcp://2.2.2.2:6379',
], RedisCluster::class];
}
}

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/** @var ConfigProvider */
private $configProvider;
public function setUp(): void
{
$this->configProvider = new ConfigProvider();
}
/** @test */
public function configIsReturned()
{
$config = $this->configProvider->__invoke();
$this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('plates', $config);
}
}

View file

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Doctrine\ConnectionFactory;
class ConnectionFactoryTest extends TestCase
{
/** @var ConnectionFactory */
private $factory;
/** @var ObjectProphecy */
private $container;
/** @var ObjectProphecy */
private $em;
public function setUp(): void
{
$this->container = $this->prophesize(ContainerInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->container->get(EntityManager::class)->willReturn($this->em->reveal());
$this->factory = new ConnectionFactory();
}
/** @test */
public function properServiceFallbackOccursWhenInvoked(): void
{
$connection = $this->prophesize(Connection::class)->reveal();
$getConnection = $this->em->getConnection()->willReturn($connection);
$result = ($this->factory)($this->container->reveal());
$this->assertSame($connection, $result);
$getConnection->shouldHaveBeenCalledOnce();
$this->container->get(EntityManager::class)->shouldHaveBeenCalledOnce();
}
}

View file

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\ORM\EntityManager;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Doctrine\EntityManagerFactory;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Zend\ServiceManager\ServiceManager;
class EntityManagerFactoryTest extends TestCase
{
/** @var EntityManagerFactory */
private $factory;
public function setUp(): void
{
$this->factory = new EntityManagerFactory();
}
/** @test */
public function serviceIsCreated(): void
{
$sm = new ServiceManager(['services' => [
'config' => [
'debug' => true,
'entity_manager' => [
'orm' => [
'types' => [
ChronosDateTimeType::CHRONOS_DATETIME => ChronosDateTimeType::class,
],
],
'connection' => [
'driver' => 'pdo_sqlite',
],
],
],
]]);
$em = ($this->factory)($sm, EntityManager::class);
$this->assertInstanceOf(EntityManager::class, $em);
}
}

View file

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
class NoDbNameConnectionFactoryTest extends TestCase
{
/** @var NoDbNameConnectionFactory */
private $factory;
/** @var ObjectProphecy */
private $container;
/** @var ObjectProphecy */
private $originalConn;
public function setUp(): void
{
$this->container = $this->prophesize(ContainerInterface::class);
$this->originalConn = $this->prophesize(Connection::class);
$this->container->get(Connection::class)->willReturn($this->originalConn->reveal());
$this->factory = new NoDbNameConnectionFactory();
}
/** @test */
public function createsNewConnectionRemovingDbNameFromOriginalConnectionParams(): void
{
$params = [
'username' => 'foo',
'password' => 'bar',
'dbname' => 'something',
];
$getOriginalParams = $this->originalConn->getParams()->willReturn($params);
$getOriginalDriver = $this->originalConn->getDriver()->willReturn($this->prophesize(Driver::class)->reveal());
$getOriginalConfig = $this->originalConn->getConfiguration()->willReturn(null);
$getOriginalEvents = $this->originalConn->getEventManager()->willReturn(null);
$conn = ($this->factory)($this->container->reveal());
$this->assertEquals([
'username' => 'foo',
'password' => 'bar',
], $conn->getParams());
$getOriginalParams->shouldHaveBeenCalledOnce();
$getOriginalDriver->shouldHaveBeenCalledOnce();
$getOriginalConfig->shouldHaveBeenCalledOnce();
$getOriginalEvents->shouldHaveBeenCalledOnce();
$this->container->get(Connection::class)->shouldHaveBeenCalledOnce();
}
}

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManagerDelegator;
use Zend\ServiceManager\ServiceManager;
class ReopeningEntityManagerDelegatorTest extends TestCase
{
/** @test */
public function decoratesEntityManagerFromCallback(): void
{
$em = $this->prophesize(EntityManagerInterface::class)->reveal();
$result = (new ReopeningEntityManagerDelegator())(new ServiceManager(), '', function () use ($em) {
return $em;
});
$ref = new ReflectionObject($result);
$prop = $ref->getProperty('wrapped');
$prop->setAccessible(true);
$this->assertSame($em, $prop->getValue($result));
}
}

View file

@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManager;
use stdClass;
class ReopeningEntityManagerTest extends TestCase
{
/** @var ReopeningEntityManager */
private $decoratorEm;
/** @var ObjectProphecy */
private $wrapped;
public function setUp(): void
{
$this->wrapped = $this->prophesize(EntityManagerInterface::class);
$this->wrapped->getConnection()->willReturn($this->prophesize(Connection::class));
$this->wrapped->getConfiguration()->willReturn($this->prophesize(Configuration::class));
$this->wrapped->getEventManager()->willReturn($this->prophesize(EventManager::class));
$wrappedMock = $this->wrapped->reveal();
$this->decoratorEm = new ReopeningEntityManager($wrappedMock, function () use ($wrappedMock) {
return $wrappedMock;
});
}
/**
* @test
* @dataProvider provideMethodNames
*/
public function wrappedInstanceIsTransparentlyCalledWhenItIsNotClosed(string $methodName): void
{
$method = $this->wrapped->__call($methodName, [Argument::cetera()])->willReturnArgument();
$isOpen = $this->wrapped->isOpen()->willReturn(true);
$this->decoratorEm->{$methodName}(new stdClass());
$method->shouldHaveBeenCalledOnce();
$isOpen->shouldHaveBeenCalledOnce();
$this->wrapped->getConnection()->shouldNotHaveBeenCalled();
$this->wrapped->getConfiguration()->shouldNotHaveBeenCalled();
$this->wrapped->getEventManager()->shouldNotHaveBeenCalled();
}
/**
* @test
* @dataProvider provideMethodNames
*/
public function wrappedInstanceIsRecreatedWhenItIsClosed(string $methodName): void
{
$method = $this->wrapped->__call($methodName, [Argument::cetera()])->willReturnArgument();
$isOpen = $this->wrapped->isOpen()->willReturn(false);
$this->decoratorEm->{$methodName}(new stdClass());
$method->shouldHaveBeenCalledOnce();
$isOpen->shouldHaveBeenCalledOnce();
$this->wrapped->getConnection()->shouldHaveBeenCalledOnce();
$this->wrapped->getConfiguration()->shouldHaveBeenCalledOnce();
$this->wrapped->getEventManager()->shouldHaveBeenCalledOnce();
}
public function provideMethodNames(): iterable
{
yield 'flush' => ['flush'];
yield 'persist' => ['persist'];
yield 'remove' => ['remove'];
yield 'refresh' => ['refresh'];
yield 'merge' => ['merge'];
}
}

View file

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Doctrine\Type;
use Cake\Chronos\Chronos;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use stdClass;
class ChronosDateTimeTypeTest extends TestCase
{
/** @var ChronosDateTimeType */
private $type;
public function setUp(): void
{
if (! Type::hasType(ChronosDateTimeType::CHRONOS_DATETIME)) {
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
}
$this->type = Type::getType(ChronosDateTimeType::CHRONOS_DATETIME);
}
/** @test */
public function nameIsReturned(): void
{
$this->assertEquals(ChronosDateTimeType::CHRONOS_DATETIME, $this->type->getName());
}
/**
* @test
* @dataProvider provideValues
*/
public function valueIsConverted(?string $value, ?string $expected): void
{
$platform = $this->prophesize(AbstractPlatform::class);
$platform->getDateTimeFormatString()->willReturn('Y-m-d H:i:s');
$result = $this->type->convertToPHPValue($value, $platform->reveal());
if ($expected === null) {
$this->assertNull($result);
} else {
$this->assertInstanceOf($expected, $result);
}
}
public function provideValues(): iterable
{
yield 'null date' => [null, null];
yield 'human friendly date' => ['now', Chronos::class];
yield 'numeric date' => ['2017-01-01', Chronos::class];
}
/**
* @test
* @dataProvider providePhpValues
*/
public function valueIsConvertedToDatabaseFormat(?DateTimeInterface $value, ?string $expected): void
{
$platform = $this->prophesize(AbstractPlatform::class);
$platform->getDateTimeFormatString()->willReturn('Y-m-d');
$this->assertEquals($expected, $this->type->convertToDatabaseValue($value, $platform->reveal()));
}
public function providePhpValues(): iterable
{
yield 'null date' => [null, null];
yield 'DateTimeImmutable date' => [new DateTimeImmutable('2017-01-01'), '2017-01-01'];
yield 'Chronos date' => [Chronos::parse('2017-02-01'), '2017-02-01'];
yield 'DateTime date' => [new DateTime('2017-03-01'), '2017-03-01'];
}
/** @test */
public function exceptionIsThrownIfInvalidValueIsParsedToDatabase(): void
{
$this->expectException(ConversionException::class);
$this->type->convertToDatabaseValue(new stdClass(), $this->prophesize(AbstractPlatform::class)->reveal());
}
}

View file

@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Factory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Factory\DottedAccessConfigAbstractFactory;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\ServiceManager;
class DottedAccessConfigAbstractFactoryTest extends TestCase
{
/** @var DottedAccessConfigAbstractFactory */
private $factory;
public function setUp(): void
{
$this->factory = new DottedAccessConfigAbstractFactory();
}
/**
* @test
* @dataProvider provideDotNames
*/
public function canCreateOnlyServicesWithDot(string $serviceName, bool $canCreate): void
{
$this->assertEquals($canCreate, $this->factory->canCreate(new ServiceManager(), $serviceName));
}
public function provideDotNames(): iterable
{
yield 'with a valid service' => ['foo.bar', true];
yield 'with another valid service' => ['config.something', true];
yield 'with an invalid service' => ['config_something', false];
yield 'with another invalid service' => ['foo', false];
}
/** @test */
public function throwsExceptionWhenFirstPartOfTheServiceIsNotRegistered()
{
$this->expectException(ServiceNotCreatedException::class);
$this->expectExceptionMessage(
'Defined service "foo" could not be found in container after resolving dotted expression "foo.bar"'
);
$this->factory->__invoke(new ServiceManager(), 'foo.bar');
}
/** @test */
public function dottedNotationIsRecursivelyResolvedUntilLastValueIsFoundAndReturned()
{
$expected = 'this is the result';
$result = $this->factory->__invoke(new ServiceManager(['services' => [
'foo' => [
'bar' => ['baz' => $expected],
],
]]), 'foo.bar.baz');
$this->assertEquals($expected, $result);
}
/** @test */
public function exceptionIsThrownIfAnyStepCannotBeResolved()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'The key "baz" provided in the dotted notation could not be found in the array service'
);
$this->factory->__invoke(new ServiceManager(['services' => [
'foo' => [
'bar' => ['something' => 123],
],
]]), 'foo.bar.baz');
}
}

View file

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\I18n;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\I18n\TranslatorFactory;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\ServiceManager;
class TranslatorFactoryTest extends TestCase
{
/** @var TranslatorFactory */
private $factory;
public function setUp(): void
{
$this->factory = new TranslatorFactory();
}
/** @test */
public function serviceIsCreated(): void
{
$instance = ($this->factory)(new ServiceManager(['services' => [
'config' => [],
]]));
$this->assertInstanceOf(Translator::class, $instance);
}
}

View file

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Lock;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use ReflectionObject;
use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory;
use Symfony\Component\Lock\StoreInterface;
use Zend\ServiceManager\ServiceManager;
class RetryLockStoreDelegatorFactoryTest extends TestCase
{
/** @var RetryLockStoreDelegatorFactory */
private $delegator;
/** @var ObjectProphecy */
private $originalStore;
public function setUp(): void
{
$this->originalStore = $this->prophesize(StoreInterface::class)->reveal();
$this->delegator = new RetryLockStoreDelegatorFactory();
}
/** @test */
public function originalStoreIsWrappedInRetryStore(): void
{
$callback = function () {
return $this->originalStore;
};
$result = ($this->delegator)(new ServiceManager(), '', $callback);
$ref = new ReflectionObject($result);
$prop = $ref->getProperty('decorated');
$prop->setAccessible(true);
$this->assertSame($this->originalStore, $prop->getValue($result));
}
}

View file

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Logger;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Container\ContainerInterface;
use Psr\Log;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
use stdClass;
class LoggerAwareDelegatorFactoryTest extends TestCase
{
/** @var LoggerAwareDelegatorFactory */
private $delegator;
/** @var ObjectProphecy */
private $container;
public function setUp(): void
{
$this->container = $this->prophesize(ContainerInterface::class);
$this->delegator = new LoggerAwareDelegatorFactory();
}
/**
* @test
* @dataProvider provideInstances
*/
public function injectsLoggerOnInstanceWhenImplementingLoggerAware($instance, int $expectedCalls): void
{
$callback = function () use ($instance) {
return $instance;
};
$getLogger = $this->container->get(Log\LoggerInterface::class)->willReturn(new Log\NullLogger());
$result = ($this->delegator)($this->container->reveal(), '', $callback);
$this->assertSame($instance, $result);
$getLogger->shouldHaveBeenCalledTimes($expectedCalls);
}
public function provideInstances(): iterable
{
yield 'no logger aware' => [new stdClass(), 0];
yield 'logger aware' => [new class implements Log\LoggerAwareInterface {
public function setLogger(LoggerInterface $logger): void
{
Assert::assertInstanceOf(Log\NullLogger::class, $logger);
}
}, 1];
}
}

View file

@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Logger;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
use Zend\ServiceManager\ServiceManager;
class LoggerFactoryTest extends TestCase
{
/** @var LoggerFactory */
private $factory;
public function setUp(): void
{
$this->factory = new LoggerFactory();
}
/** @test */
public function serviceIsCreated()
{
/** @var Logger $instance */
$instance = $this->factory->__invoke(new ServiceManager(), '');
$this->assertInstanceOf(LoggerInterface::class, $instance);
$this->assertEquals('Logger', $instance->getName());
}
/** @test */
public function nameIsSetFromOptions()
{
/** @var Logger $instance */
$instance = $this->factory->__invoke(new ServiceManager(), '', ['logger_name' => 'Foo']);
$this->assertInstanceOf(LoggerInterface::class, $instance);
$this->assertEquals('Foo', $instance->getName());
}
/** @test */
public function serviceNameOverwritesOptionsLoggerName()
{
/** @var Logger $instance */
$instance = $this->factory->__invoke(new ServiceManager(), 'Logger_Shlink', ['logger_name' => 'Foo']);
$this->assertInstanceOf(LoggerInterface::class, $instance);
$this->assertEquals('Shlink', $instance->getName());
}
}

View file

@ -1,67 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Logger\Processor;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Logger\Processor\ExceptionWithNewLineProcessor;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use function Functional\map;
use function range;
use const PHP_EOL;
class ExceptionWithNewLineProcessorTest extends TestCase
{
use StringUtilsTrait;
/** @var ExceptionWithNewLineProcessor */
private $processor;
public function setUp(): void
{
$this->processor = new ExceptionWithNewLineProcessor();
}
/**
* @test
* @dataProvider provideNoPlaceholderRecords
*/
public function keepsRecordAsIsWhenNoPlaceholderExists(array $record): void
{
$this->assertSame($record, ($this->processor)($record));
}
public function provideNoPlaceholderRecords(): iterable
{
return map(range(1, 5), function () {
return [['message' => $this->generateRandomString()]];
});
}
/**
* @test
* @dataProvider providePlaceholderRecords
*/
public function properlyReplacesExceptionPlaceholderAddingNewLine(array $record, array $expected): void
{
$this->assertEquals($expected, ($this->processor)($record));
}
public function providePlaceholderRecords(): iterable
{
yield [
['message' => 'Hello World with placeholder {e}'],
['message' => 'Hello World with placeholder ' . PHP_EOL . '{e}'],
];
yield [
['message' => '{e} Shlink'],
['message' => PHP_EOL . '{e} Shlink'],
];
yield [
['message' => 'Foo {e} bar'],
['message' => 'Foo ' . PHP_EOL . '{e} bar'],
];
}
}

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Middleware;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use Shlinkio\Shlink\Common\Middleware\CloseDbConnectionMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
class CloseDbConnectionMiddlewareTest extends TestCase
{
/** @var CloseDbConnectionMiddleware */
private $middleware;
/** @var ObjectProphecy */
private $handler;
/** @var ObjectProphecy */
private $em;
/** @var ObjectProphecy */
private $conn;
public function setUp(): void
{
$this->handler = $this->prophesize(RequestHandlerInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->conn = $this->prophesize(Connection::class);
$this->conn->close()->will(function () {
});
$this->em->getConnection()->willReturn($this->conn->reveal());
$this->em->clear()->will(function () {
});
$this->middleware = new CloseDbConnectionMiddleware($this->em->reveal());
}
/** @test */
public function connectionIsClosedWhenMiddlewareIsProcessed(): void
{
$req = new ServerRequest();
$resp = new Response();
$handle = $this->handler->handle($req)->willReturn($resp);
$result = $this->middleware->process($req, $this->handler->reveal());
$this->assertSame($result, $resp);
$this->em->getConnection()->shouldHaveBeenCalledOnce();
$this->conn->close()->shouldHaveBeenCalledOnce();
$this->em->clear()->shouldHaveBeenCalledOnce();
$handle->shouldHaveBeenCalledOnce();
}
/** @test */
public function connectionIsClosedEvenIfExceptionIsThrownOnInnerMiddlewares(): void
{
$req = new ServerRequest();
$expectedError = new RuntimeException();
$this->handler->handle($req)->willThrow($expectedError)
->shouldBeCalledOnce();
$this->em->getConnection()->shouldBeCalledOnce();
$this->conn->close()->shouldBeCalledOnce();
$this->em->clear()->shouldBeCalledOnce();
$this->expectExceptionObject($expectedError);
$this->middleware->process($req, $this->handler->reveal());
}
}

View file

@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
use Zend\ServiceManager\ServiceManager;
class IpAddressMiddlewareFactoryTest extends TestCase
{
private $factory;
public function setUp(): void
{
$this->factory = new IpAddressMiddlewareFactory();
}
/**
* @test
* @dataProvider provideConfigs
*/
public function returnedInstanceIsProperlyConfigured(array $config, array $expectedHeadersToInspect): void
{
$instance = ($this->factory)(new ServiceManager(['services' => [
'config' => $config,
]]));
$ref = new ReflectionObject($instance);
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
$checkProxyHeaders->setAccessible(true);
$trustedProxies = $ref->getProperty('trustedProxies');
$trustedProxies->setAccessible(true);
$attributeName = $ref->getProperty('attributeName');
$attributeName->setAccessible(true);
$headersToInspect = $ref->getProperty('headersToInspect');
$headersToInspect->setAccessible(true);
$this->assertTrue($checkProxyHeaders->getValue($instance));
$this->assertEquals([], $trustedProxies->getValue($instance));
$this->assertEquals(IpAddressMiddlewareFactory::REQUEST_ATTR, $attributeName->getValue($instance));
$this->assertEquals($expectedHeadersToInspect, $headersToInspect->getValue($instance));
}
public function provideConfigs(): iterable
{
$defaultHeadersToInspect = [
'Forwarded',
'X-Forwarded-For',
'X-Forwarded',
'X-Cluster-Client-Ip',
'Client-Ip',
];
yield 'no ip_address_resolution config' => [[], $defaultHeadersToInspect];
yield 'no headers_to_inspect config' => [['ip_address_resolution' => []], $defaultHeadersToInspect];
yield 'null headers_to_inspect' => [['ip_address_resolution' => [
'headers_to_inspect' => null,
]], $defaultHeadersToInspect];
yield 'empty headers_to_inspect' => [['ip_address_resolution' => [
'headers_to_inspect' => [],
]], $defaultHeadersToInspect];
yield 'some headers_to_inspect' => [['ip_address_resolution' => [
'headers_to_inspect' => [
'foo',
'bar',
'baz',
],
]], [
'foo',
'bar',
'baz',
]];
yield 'some other headers_to_inspect' => [['ip_address_resolution' => [
'headers_to_inspect' => [
'something',
'something_else',
],
]], [
'something',
'something_else',
]];
}
}

View file

@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequest;
use Zend\I18n\Translator\Translator;
class LocaleMiddlewareTest extends TestCase
{
/** @var LocaleMiddleware */
private $middleware;
/** @var Translator */
private $translator;
public function setUp(): void
{
$this->translator = Translator::factory(['locale' => 'ru']);
$this->middleware = new LocaleMiddleware($this->translator);
}
/** @test */
public function whenNoHeaderIsPresentLocaleIsNotChanged(): void
{
$this->assertEquals('ru', $this->translator->getLocale());
$this->middleware->process(new ServerRequest(), TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals('ru', $this->translator->getLocale());
}
/** @test */
public function whenTheHeaderIsPresentLocaleIsChanged(): void
{
$this->assertEquals('ru', $this->translator->getLocale());
$request = (new ServerRequest())->withHeader('Accept-Language', 'es');
$this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals('es', $this->translator->getLocale());
}
/**
* @test
* @dataProvider provideLanguages
*/
public function localeGetsNormalized(string $lang, string $expected): void
{
$handler = TestUtils::createReqHandlerMock();
$this->assertEquals('ru', $this->translator->getLocale());
$request = (new ServerRequest())->withHeader('Accept-Language', $lang);
$this->middleware->process($request, $handler->reveal());
$this->assertEquals($expected, $this->translator->getLocale());
}
public function provideLanguages(): iterable
{
yield 'language only' => ['ru', 'ru'];
yield 'country and language with underscore' => ['es_ES', 'es'];
yield 'country and language with dash' => ['en-US', 'en'];
}
}

View file

@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Response;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Response\PixelResponse;
class PixelResponseTest extends TestCase
{
/** @var PixelResponse */
private $resp;
public function setUp(): void
{
$this->resp = new PixelResponse();
}
/** @test */
public function responseHasGifTypeAndIsNotEmpty()
{
$this->assertEquals('image/gif', $this->resp->getHeaderLine('Content-Type'));
$this->assertNotEmpty((string) $this->resp->getBody());
}
}

View file

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Response;
use Endroid\QrCode\QrCode;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
class QrCodeResponseTest extends TestCase
{
/** @test */
public function providedQrCoideIsSetAsBody()
{
$qrCode = new QrCode('Hello');
$resp = new QrCodeResponse($qrCode);
$this->assertEquals($qrCode->getContentType(), $resp->getHeaderLine('Content-Type'));
$this->assertEquals($qrCode->get(), (string) $resp->getBody());
}
}

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Template\Extension;
use League\Plates\Engine;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Shlinkio\Shlink\Common\Template\Extension\TranslatorExtension;
use Zend\I18n\Translator\Translator;
class TranslatorExtensionTest extends TestCase
{
/** @var TranslatorExtension */
private $extension;
public function setUp(): void
{
$this->extension = new TranslatorExtension($this->prophesize(Translator::class)->reveal());
}
/** @test */
public function properFunctionsAreReturned()
{
$engine = $this->prophesize(Engine::class);
$registerTranslate = $engine->registerFunction('translate', Argument::type('callable'))->will(function () {
});
$registerLocale = $engine->registerFunction('locale', Argument::type('array'))->will(function () {
});
$this->extension->register($engine->reveal());
$registerTranslate->shouldHaveBeenCalledOnce();
$registerLocale->shouldHaveBeenCalledOnce();
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Util;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Util\DateRange;
class DateRangeTest extends TestCase
{
/** @test */
public function defaultConstructorSetDatesToNull()
{
$range = new DateRange();
$this->assertNull($range->getStartDate());
$this->assertNull($range->getEndDate());
$this->assertTrue($range->isEmpty());
}
/** @test */
public function providedDatesAreSet()
{
$startDate = Chronos::now();
$endDate = Chronos::now();
$range = new DateRange($startDate, $endDate);
$this->assertSame($startDate, $range->getStartDate());
$this->assertSame($endDate, $range->getEndDate());
$this->assertFalse($range->isEmpty());
}
/**
* @test
* @dataProvider provideDates
*/
public function isConsideredEmptyOnlyIfNoneOfTheDatesIsSet(
?Chronos $startDate,
?Chronos $endDate,
bool $isEmpty
): void {
$range = new DateRange($startDate, $endDate);
$this->assertEquals($isEmpty, $range->isEmpty());
}
public function provideDates(): iterable
{
yield 'both are null' => [null, null, true];
yield 'start is null' => [null, Chronos::now(), false];
yield 'end is null' => [Chronos::now(), null, false];
yield 'none are null' => [Chronos::now(), Chronos::now(), false];
}
}

View file

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Util;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use function Functional\map;
use function range;
use function strlen;
class StringUtilsTraitTest extends TestCase
{
use StringUtilsTrait;
/**
* @test
* @dataProvider provideLengths
*/
public function generateRandomStringGeneratesStringOfProvidedLength(int $length): void
{
$this->assertEquals($length, strlen($this->generateRandomString($length)));
}
public function provideLengths(): array
{
return map(range(10, 50, 5), function (int $i) {
return [$i];
});
}
/** @test */
public function generatesUuidV4()
{
$uuidPattern = '/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/';
$this->assertRegExp($uuidPattern, $this->generateV4Uuid());
$this->assertRegExp($uuidPattern, $this->generateV4Uuid());
$this->assertRegExp($uuidPattern, $this->generateV4Uuid());
$this->assertRegExp($uuidPattern, $this->generateV4Uuid());
$this->assertRegExp($uuidPattern, $this->generateV4Uuid());
}
}

View file

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Util;
use Prophecy\Argument;
use Prophecy\Prophet;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
class TestUtils
{
private static $prophet;
public static function createReqHandlerMock(?ResponseInterface $response = null, ?RequestInterface $request = null)
{
$argument = $request ?: Argument::any();
$delegate = static::getProphet()->prophesize(RequestHandlerInterface::class);
$delegate->handle($argument)->willReturn($response ?: new Response());
return $delegate;
}
/**
* @return Prophet
*/
private static function getProphet()
{
if (static::$prophet === null) {
static::$prophet = new Prophet();
}
return static::$prophet;
}
}

View file

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Validation;
use Cocur\Slugify\SlugifyInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Validation\SluggerFilter;
class SluggerFilterTest extends TestCase
{
/** @var SluggerFilter */
private $filter;
/** @var ObjectProphecy */
private $slugger;
public function setUp(): void
{
$this->slugger = $this->prophesize(SlugifyInterface::class);
$this->filter = new SluggerFilter($this->slugger->reveal());
}
/**
* @test
* @dataProvider provideValuesToFilter
*/
public function providedValueIsFilteredAsExpected($providedValue, $expectedValue): void
{
$slugify = $this->slugger->slugify($providedValue)->willReturn('slug');
$result = $this->filter->filter($providedValue);
$this->assertEquals($expectedValue, $result);
$slugify->shouldHaveBeenCalledTimes($expectedValue !== null ? 1 : 0);
}
public function provideValuesToFilter(): iterable
{
yield 'null' => [null, null];
yield 'empty string' => ['', null];
yield 'not empty string' => ['foo', 'slug'];
}
}

View file

@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Action;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Response\PixelResponse; use Shlinkio\Shlink\Common\Response\PixelResponse;
use Shlinkio\Shlink\Core\Action\PixelAction; use Shlinkio\Shlink\Core\Action\PixelAction;
use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Action\RedirectAction;
@ -13,7 +14,6 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequest; use Zend\Diactoros\ServerRequest;
class PixelActionTest extends TestCase class PixelActionTest extends TestCase
@ -38,7 +38,7 @@ class PixelActionTest extends TestCase
} }
/** @test */ /** @test */
public function imageIsReturned() public function imageIsReturned(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn( $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(
@ -47,7 +47,7 @@ class PixelActionTest extends TestCase
$this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce(); $this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce();
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode); $request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal()); $response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
$this->assertInstanceOf(PixelResponse::class, $response); $this->assertInstanceOf(PixelResponse::class, $response);
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());

View file

@ -14,7 +14,6 @@ use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator; use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest; use Zend\Diactoros\ServerRequest;
@ -39,7 +38,7 @@ class PreviewActionTest extends TestCase
} }
/** @test */ /** @test */
public function invalidShortCodeFallsBackToNextMiddleware() public function invalidShortCodeFallsBackToNextMiddleware(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
@ -52,7 +51,7 @@ class PreviewActionTest extends TestCase
} }
/** @test */ /** @test */
public function correctShortCodeReturnsImageResponse() public function correctShortCodeReturnsImageResponse(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$url = 'foobar.com'; $url = 'foobar.com';
@ -63,7 +62,7 @@ class PreviewActionTest extends TestCase
$resp = $this->action->process( $resp = $this->action->process(
(new ServerRequest())->withAttribute('shortCode', $shortCode), (new ServerRequest())->withAttribute('shortCode', $shortCode),
TestUtils::createReqHandlerMock()->reveal() $this->prophesize(RequestHandlerInterface::class)->reveal()
); );
$this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length')); $this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length'));
@ -71,7 +70,7 @@ class PreviewActionTest extends TestCase
} }
/** @test */ /** @test */
public function invalidShortCodeExceptionFallsBackToNextMiddleware() public function invalidShortCodeExceptionFallsBackToNextMiddleware(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)

View file

@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Options;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest; use Zend\Diactoros\ServerRequest;
@ -43,7 +42,7 @@ class RedirectActionTest extends TestCase
} }
/** @test */ /** @test */
public function redirectionIsPerformedToLongUrl() public function redirectionIsPerformedToLongUrl(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar'; $expectedUrl = 'http://domain.com/foo/bar';
@ -53,7 +52,7 @@ class RedirectActionTest extends TestCase
$this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce(); $this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce();
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode); $request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal()); $response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response); $this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
@ -62,7 +61,7 @@ class RedirectActionTest extends TestCase
} }
/** @test */ /** @test */
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound() public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
@ -79,7 +78,7 @@ class RedirectActionTest extends TestCase
} }
/** @test */ /** @test */
public function redirectToCustomUrlIsReturnedIfConfiguredSoAndShortUrlIsNotFound() public function redirectToCustomUrlIsReturnedIfConfiguredSoAndShortUrlIsNotFound(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$shortCodeToUrl = $this->urlShortener->shortCodeToUrl($shortCode)->willThrow( $shortCodeToUrl = $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(
@ -102,7 +101,7 @@ class RedirectActionTest extends TestCase
} }
/** @test */ /** @test */
public function visitIsNotTrackedIfDisableParamIsProvided() public function visitIsNotTrackedIfDisableParamIsProvided(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar'; $expectedUrl = 'http://domain.com/foo/bar';
@ -113,7 +112,7 @@ class RedirectActionTest extends TestCase
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode) $request = (new ServerRequest())->withAttribute('shortCode', $shortCode)
->withQueryParams(['foobar' => true]); ->withQueryParams(['foobar' => true]);
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal()); $response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response); $this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());

View file

@ -1,5 +1,6 @@
parameters: parameters:
ignoreErrors: ignoreErrors:
- '#League\\Plates\\callback#'
- '#is not subtype of Throwable#' - '#is not subtype of Throwable#'
- '#ObjectManager::flush()#' - '#ObjectManager::flush()#'
- '#\$metadata ClassMetadata#'
- '#Undefined variable: \$metadata#'

View file

@ -6,9 +6,6 @@
colors="true" colors="true"
> >
<testsuites> <testsuites>
<testsuite name="Common">
<directory>./module/Common/test</directory>
</testsuite>
<testsuite name="Core"> <testsuite name="Core">
<directory>./module/Core/test</directory> <directory>./module/Core/test</directory>
</testsuite> </testsuite>