Merge pull request #456 from acelaya/feature/common-module

Feature/common module
This commit is contained in:
Alejandro Celaya authored and GitHub committed 2019-08-11 15:18:28 +02:00
commit da88ec6807
87 files changed
+616 -354

No files matched your search

-6
View File
@@ -1,18 +1,12 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
return [
'dependencies' => [
'factories' => [
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
],
'delegators' => [
Expressive\Application::class => [
Container\ApplicationConfigInjectionDelegator::class,
+2 -2
View File
@@ -7,7 +7,7 @@ use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use PDO;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\IpAddress;
/**
@@ -60,7 +60,7 @@ final class Version20180913205455 extends AbstractMigration
try {
return (string) IpAddress::fromString($addr)->getObfuscatedCopy();
} catch (WrongIpException $e) {
} catch (InvalidArgumentException $e) {
return null;
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
@@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Symfony\Component\Console\Command\Command;
@@ -62,7 +62,7 @@ class ListShortUrlsCommand extends Command
'page',
'p',
InputOption::VALUE_OPTIONAL,
sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE),
sprintf('The first page to list (%s items per page)', ShortUrlRepositoryAdapter::ITEMS_PER_PAGE),
'1'
)
->addOption(
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
@@ -9,12 +9,12 @@ use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use Symfony\Component\Console\Helper\ProgressBar;
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Util;
use Cake\Chronos\Chronos;
use GeoIp2\Database\Reader;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock\Factory as Locker;
use Throwable;
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Console;
namespace Shlinkio\Shlink\CLI\Util;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\OutputInterface;
@@ -9,13 +9,13 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Service\VisitService;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver;
use Symfony\Component\Console\Application;
@@ -8,7 +8,7 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
@@ -11,7 +11,7 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock;
use Throwable;
@@ -1,13 +1,13 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Console;
namespace ShlinkioTest\Shlink\CLI\Util;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use ReflectionObject;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableStyle;
use Symfony\Component\Console\Output\OutputInterface;
@@ -26,7 +26,7 @@ class ShlinkTableTest extends TestCase
}
/** @test */
public function renderMakesTableToBeRenderedWithProvidedInfo()
public function renderMakesTableToBeRenderedWithProvidedInfo(): void
{
$headers = [];
$rows = [[]];
@@ -53,7 +53,7 @@ class ShlinkTableTest extends TestCase
}
/** @test */
public function newTableIsCreatedForFactoryMethod()
public function newTableIsCreatedForFactoryMethod(): void
{
$instance = ShlinkTable::fromOutput($this->prophesize(OutputInterface::class)->reveal());
+21
View File
@@ -0,0 +1,21 @@
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.
+89
View File
@@ -0,0 +1,89 @@
# 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',
],
],
];
```
+1 -19
View File
@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use GeoIp2\Database\Reader;
use GuzzleHttp\Client as GuzzleClient;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
@@ -12,7 +11,6 @@ use Symfony\Component\Filesystem\Filesystem;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\Proxy\LazyServiceFactory;
return [
@@ -20,9 +18,8 @@ return [
'factories' => [
GuzzleClient::class => InvokableFactory::class,
Filesystem::class => InvokableFactory::class,
Reader::class => ConfigAbstractFactory::class,
Translator::class => Factory\TranslatorFactory::class,
Translator::class => I18n\TranslatorFactory::class,
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
@@ -44,24 +41,9 @@ return [
'abstract_factories' => [
Factory\DottedAccessConfigAbstractFactory::class,
],
'delegators' => [
// The GeoLite2 db reader has to be lazy so that it does not try to load the DB file at app bootstrapping.
// By doing so, it would fail the first time shlink tries to download it.
Reader::class => [
LazyServiceFactory::class,
],
],
'lazy_services' => [
'class_map' => [
Reader::class => Reader::class,
],
],
],
ConfigAbstractFactory::class => [
Reader::class => ['config.geolite2.db_location'],
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Middleware\CloseDbConnectionMiddleware::class => ['em'],
+1 -1
View File
@@ -11,7 +11,7 @@ return [
'entity_manager' => [
'orm' => [
'types' => [
Type\ChronosDateTimeType::CHRONOS_DATETIME => Type\ChronosDateTimeType::class,
Doctrine\Type\ChronosDateTimeType::CHRONOS_DATETIME => Doctrine\Type\ChronosDateTimeType::class,
],
],
],
+37 -11
View File
@@ -4,22 +4,48 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Cache;
use Doctrine\Common\Cache;
use Interop\Container\ContainerInterface;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Zend\ServiceManager\Factory\FactoryInterface;
use Predis\Client as PredisClient;
use Psr\Container\ContainerInterface;
use function Shlinkio\Shlink\Common\env;
use function extension_loaded;
class CacheFactory implements FactoryInterface
class CacheFactory
{
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): Cache\Cache
{
// TODO Make use of the redis cache via RedisFactory when possible
/** @var callable|null */
private $apcuEnabled;
$appOptions = $container->get(AppOptions::class);
$adapter = env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
$adapter->setNamespace((string) $appOptions);
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);
}
}
+2 -1
View File
@@ -16,7 +16,8 @@ class RedisFactory
public function __invoke(ContainerInterface $container): PredisClient
{
$redisConfig = $container->get('config')['redis'] ?? [];
$config = $container->get('config');
$redisConfig = $config['cache']['redis'] ?? $config['redis'] ?? [];
$servers = $redisConfig['servers'] ?? [];
$servers = is_string($servers) ? explode(',', $servers) : $servers;
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Type;
namespace Shlinkio\Shlink\Common\Doctrine\Type;
use Cake\Chronos\Chronos;
use DateTimeInterface;
+1 -8
View File
@@ -3,16 +3,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Entity;
use Doctrine\ORM\Mapping as ORM;
abstract class AbstractEntity
{
/**
* @var string
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(name="id", type="bigint", options={"unsigned"=true})
*/
/** @var string */
protected $id;
public function getId(): string
@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
use RuntimeException;
use function sprintf;
/** @deprecated */
@@ -22,11 +22,9 @@ class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
/**
* Can the factory create an instance for the service?
*
* @param ContainerInterface $container
* @param string $requestedName
* @return bool
*/
public function canCreate(ContainerInterface $container, $requestedName)
public function canCreate(ContainerInterface $container, $requestedName): bool
{
return substr_count($requestedName, '.') > 0;
}
@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class EmptyResponseImplicitOptionsMiddlewareFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
{
return new ImplicitOptionsMiddleware(function () {
return new EmptyResponse();
});
}
}
@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class TranslatorFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
{
$config = $container->get('config');
return Translator::factory($config['translator'] ?? []);
}
}
@@ -0,0 +1,16 @@
<?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'] ?? []);
}
}
+1
View File
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Common\Image;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\AbstractPluginManager;
/** @deprecated */
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
{
protected $instanceOf = Image::class;
@@ -10,6 +10,7 @@ use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
/** @deprecated */
class ImageBuilderFactory implements FactoryInterface
{
/**
@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common\Image;
use Zend\ServiceManager\ServiceLocatorInterface;
/** @deprecated */
interface ImageBuilderInterface extends ServiceLocatorInterface
{
}
+1
View File
@@ -10,6 +10,7 @@ use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
/** @deprecated */
class ImageFactory implements FactoryInterface
{
/**
+3 -18
View File
@@ -5,29 +5,14 @@ namespace Shlinkio\Shlink\Common\Logger;
use Cascade\Cascade;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
use Monolog\Logger;
use function count;
use function explode;
class LoggerFactory implements FactoryInterface
class LoggerFactory
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): Logger
{
$config = $container->has('config') ? $container->get('config') : [];
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);
@@ -3,28 +3,17 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Interop\Container\ContainerInterface;
use Psr\Container\ContainerInterface;
use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Core\Model\Visitor;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class IpAddressMiddlewareFactory implements FactoryInterface
class IpAddressMiddlewareFactory
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
*/
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): IpAddress
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, [], Visitor::REMOTE_ADDRESS_ATTR, $headersToInspect);
return new IpAddress(true, [], self::REQUEST_ATTR, $headersToInspect);
}
}
@@ -24,8 +24,6 @@ class LocaleMiddleware implements MiddlewareInterface
$this->translator = $translator;
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
@@ -31,12 +31,6 @@ trait PaginatorUtilsTrait
return $transformer === null ? $items : array_map([$transformer, 'transform'], $items);
}
/**
* Checks if provided paginator is in last page
*
* @param Paginator $paginator
* @return bool
*/
private function isLastPage(Paginator $paginator): bool
{
return $paginator->getCurrentPageNumber() >= $paginator->count();
@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Repository;
interface PaginableRepositoryInterface
{
/**
* Gets a list of elements using provided filtering data
*
* @param int|null $limit
* @param int|null $offset
* @param string|null $searchTerm
* @param array $tags
* @param string|array|null $orderBy
* @return array
*/
public function findList(
?int $limit = null,
?int $offset = null,
?string $searchTerm = null,
array $tags = [],
$orderBy = null
): array;
/**
* Counts the number of elements in a list using provided filtering data
*
* @param string|null $searchTerm
* @param array $tags
* @return int
*/
public function countList(?string $searchTerm = null, array $tags = []): int;
}
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
namespace Shlinkio\Shlink\Common\Response;
use Fig\Http\Message\StatusCodeInterface as StatusCode;
use finfo;
+4 -3
View File
@@ -3,11 +3,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use function count;
use function explode;
use function implode;
use function sprintf;
use function trim;
final class IpAddress
@@ -36,14 +37,14 @@ final class IpAddress
/**
* @param string $address
* @return IpAddress
* @throws WrongIpException
* @throws InvalidArgumentException
*/
public static function fromString(string $address): self
{
$address = trim($address);
$parts = explode('.', $address);
if (count($parts) !== self::IPV4_PARTS_COUNT) {
throw WrongIpException::fromIpAddress($address);
throw new InvalidArgumentException(sprintf('Provided IP "%s" is invalid', $address));
}
return new self(...$parts);
@@ -9,7 +9,6 @@ use GuzzleHttp\ClientInterface;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
use function Shlinkio\Shlink\Common\json_decode;
use function sprintf;
@@ -48,7 +47,7 @@ abstract class ApiTestCase extends TestCase implements StatusCodeInterface, Requ
protected function callApiWithKey(string $method, string $uri, array $options = []): ResponseInterface
{
$headers = $options[RequestOptions::HEADERS] ?? [];
$headers[ApiKeyHeaderPlugin::HEADER_NAME] = 'valid_api_key';
$headers['X-Api-Key'] = 'valid_api_key';
$options[RequestOptions::HEADERS] = $headers;
return $this->callApi($method, $uri, $options);
+42 -30
View File
@@ -3,48 +3,60 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Cache;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
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\Core\Options\AppOptions;
use Zend\ServiceManager\ServiceManager;
use function putenv;
use Shlinkio\Shlink\Common\Cache\RedisFactory;
class CacheFactoryTest extends TestCase
{
/** @var CacheFactory */
private $factory;
/** @var ServiceManager */
private $sm;
/** @var ObjectProphecy */
private $container;
public function setUp(): void
{
$this->factory = new CacheFactory();
$this->sm = new ServiceManager(['services' => [
AppOptions::class => new AppOptions(),
]]);
$this->container = $this->prophesize(ContainerInterface::class);
}
public static function tearDownAfterClass(): void
{
putenv('APP_ENV');
/**
* @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);
}
/** @test */
public function productionReturnsApcAdapter(): void
public function provideCacheConfig(): iterable
{
putenv('APP_ENV=pro');
$instance = ($this->factory)($this->sm, '');
$this->assertInstanceOf(ApcuCache::class, $instance);
}
/** @test */
public function developmentReturnsArrayAdapter(): void
{
putenv('APP_ENV=dev');
$instance = ($this->factory)($this->sm, '');
$this->assertInstanceOf(ArrayCache::class, $instance);
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;
}];
}
}
+19 -1
View File
@@ -27,7 +27,7 @@ class RedisFactoryTest extends TestCase
* @test
* @dataProvider provideRedisConfig
*/
public function createsRedisClientBasedOnConfig(?array $config, string $expectedCluster): void
public function createsRedisClientBasedOnRedisConfig(?array $config, string $expectedCluster): void
{
$getConfig = $this->container->get('config')->willReturn([
'redis' => $config,
@@ -39,6 +39,24 @@ class RedisFactoryTest extends TestCase
$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];
@@ -6,7 +6,7 @@ namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\ORM\EntityManager;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Doctrine\EntityManagerFactory;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Zend\ServiceManager\ServiceManager;
class EntityManagerFactoryTest extends TestCase
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Type;
namespace ShlinkioTest\Shlink\Common\Doctrine\Type;
use Cake\Chronos\Chronos;
use DateTime;
@@ -11,7 +11,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use stdClass;
class ChronosDateTimeTypeTest extends TestCase
@@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Factory;
namespace ShlinkioTest\Shlink\Common\I18n;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Shlinkio\Shlink\Common\I18n\TranslatorFactory;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\ServiceManager;
@@ -19,11 +19,11 @@ class TranslatorFactoryTest extends TestCase
}
/** @test */
public function serviceIsCreated()
public function serviceIsCreated(): void
{
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
$instance = ($this->factory)(new ServiceManager(['services' => [
'config' => [],
]]), '');
]]));
$this->assertInstanceOf(Translator::class, $instance);
}
}
@@ -6,7 +6,6 @@ namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
use Shlinkio\Shlink\Core\Model\Visitor;
use Zend\ServiceManager\ServiceManager;
class IpAddressMiddlewareFactoryTest extends TestCase
@@ -26,7 +25,7 @@ class IpAddressMiddlewareFactoryTest extends TestCase
{
$instance = ($this->factory)(new ServiceManager(['services' => [
'config' => $config,
]]), '');
]]));
$ref = new ReflectionObject($instance);
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
@@ -40,7 +39,7 @@ class IpAddressMiddlewareFactoryTest extends TestCase
$this->assertTrue($checkProxyHeaders->getValue($instance));
$this->assertEquals([], $trustedProxies->getValue($instance));
$this->assertEquals(Visitor::REMOTE_ADDRESS_ATTR, $attributeName->getValue($instance));
$this->assertEquals(IpAddressMiddlewareFactory::REQUEST_ATTR, $attributeName->getValue($instance));
$this->assertEquals($expectedHeadersToInspect, $headersToInspect->getValue($instance));
}
@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Paginator\Adapter;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
class PaginableRepositoryAdapterTest extends TestCase
{
/** @var PaginableRepositoryAdapter */
private $adapter;
/** @var ObjectProphecy */
private $repo;
public function setUp(): void
{
$this->repo = $this->prophesize(PaginableRepositoryInterface::class);
$this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', ['foo', 'bar'], 'order');
}
/** @test */
public function getItemsFallbacksToFindList()
{
$this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledOnce();
$this->adapter->getItems(5, 10);
}
/** @test */
public function countFallbacksToCountList()
{
$this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledOnce();
$this->adapter->count();
}
}
@@ -0,0 +1,44 @@
<?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'];
}
}
@@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Core;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata);
@@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Core;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata);
+1 -1
View File
@@ -10,8 +10,8 @@ use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
+2 -2
View File
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Entity;
use Cake\Chronos\Chronos;
use JsonSerializable;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
@@ -45,7 +45,7 @@ class Visit extends AbstractEntity implements JsonSerializable
try {
return (string) IpAddress::fromString($address)->getObfuscatedCopy();
} catch (WrongIpException $e) {
} catch (InvalidArgumentException $e) {
return null;
}
}
@@ -7,9 +7,9 @@ use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
+2 -3
View File
@@ -4,11 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
final class Visitor
{
public const REMOTE_ADDRESS_ATTR = 'remote_address';
/** @var string */
private $userAgent;
/** @var string */
@@ -28,7 +27,7 @@ final class Visitor
return new self(
$request->getHeaderLine('User-Agent'),
$request->getHeaderLine('Referer'),
$request->getAttribute(self::REMOTE_ADDRESS_ATTR)
$request->getAttribute(IpAddressMiddlewareFactory::REQUEST_ATTR)
);
}
@@ -1,20 +1,20 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Paginator\Adapter;
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Zend\Paginator\Adapter\AdapterInterface;
use function strip_tags;
use function trim;
class PaginableRepositoryAdapter implements AdapterInterface
class ShortUrlRepositoryAdapter implements AdapterInterface
{
public const ITEMS_PER_PAGE = 10;
/** @var PaginableRepositoryInterface */
private $paginableRepository;
/** @var ShortUrlRepositoryInterface */
private $repository;
/** @var null|string */
private $searchTerm;
/** @var null|array|string */
@@ -23,12 +23,12 @@ class PaginableRepositoryAdapter implements AdapterInterface
private $tags;
public function __construct(
PaginableRepositoryInterface $paginableRepository,
ShortUrlRepositoryInterface $repository,
$searchTerm = null,
array $tags = [],
$orderBy = null
) {
$this->paginableRepository = $paginableRepository;
$this->repository = $repository;
$this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null;
$this->orderBy = $orderBy;
$this->tags = $tags;
@@ -43,7 +43,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
*/
public function getItems($offset, $itemCountPerPage): array
{
return $this->paginableRepository->findList(
return $this->repository->findList(
$itemCountPerPage,
$offset,
$this->searchTerm,
@@ -63,6 +63,6 @@ class PaginableRepositoryAdapter implements AdapterInterface
*/
public function count(): int
{
return $this->paginableRepository->countList($this->searchTerm, $this->tags);
return $this->repository->countList($this->searchTerm, $this->tags);
}
}
@@ -4,10 +4,27 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\Common\Persistence\ObjectRepository;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
interface ShortUrlRepositoryInterface extends ObjectRepository, PaginableRepositoryInterface
interface ShortUrlRepositoryInterface extends ObjectRepository
{
/**
* Gets a list of elements using provided filtering data
*
* @param string|array|null $orderBy
*/
public function findList(
?int $limit = null,
?int $offset = null,
?string $searchTerm = null,
array $tags = [],
$orderBy = null
): array;
/**
* Counts the number of elements in a list using provided filtering data
*/
public function countList(?string $searchTerm = null, array $tags = []): int;
public function findOneByShortCode(string $shortCode): ?ShortUrl;
}
+3 -3
View File
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service;
use Doctrine\ORM;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait;
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
@@ -35,8 +35,8 @@ class ShortUrlService implements ShortUrlServiceInterface
{
/** @var ShortUrlRepository $repo */
$repo = $this->em->getRepository(ShortUrl::class);
$paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery, $tags, $orderBy));
$paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE)
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $searchQuery, $tags, $orderBy));
$paginator->setItemCountPerPage(ShortUrlRepositoryAdapter::ITEMS_PER_PAGE)
->setCurrentPageNumber($page);
return $paginator;
@@ -10,7 +10,6 @@ use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
@@ -19,6 +18,7 @@ use Shlinkio\Shlink\Core\EventDispatcher\LocateShortUrlVisit;
use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Paginator\Adapter;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
class ShortUrlRepositoryAdapterTest extends TestCase
{
/** @var ShortUrlRepositoryAdapter */
private $adapter;
/** @var ObjectProphecy */
private $repo;
public function setUp(): void
{
$this->repo = $this->prophesize(ShortUrlRepositoryInterface::class);
$this->adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), 'search', ['foo', 'bar'], 'order');
}
/** @test */
public function getItemsFallbacksToFindList(): void
{
$this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledOnce();
$this->adapter->getItems(5, 10);
}
/** @test */
public function countFallbacksToCountList(): void
{
$this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledOnce();
$this->adapter->count();
}
}
+21
View File
@@ -0,0 +1,21 @@
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.
+13
View File
@@ -0,0 +1,13 @@
# Shlink Event Dispatcher
This library provides a PSR-14 EventDispatcher which is capable of dispatching both regular listeners and async listeners which are run using [swoole]'s task system.
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-event-dispatcher
> This library is also an expressive module which provides its own `ConfigProvider`. Add it to your configuration to get everything automatically set up.
@@ -23,6 +23,8 @@ return [
Psr\EventDispatcherInterface::class => Phly\EventDispatcher::class,
],
'delegators' => [
// The listener provider has to be lazy, because it uses the Swoole server to generate AsyncEventListeners
// Without making this lazy, CLI commands which depend on the EventDispatcher fail
Psr\ListenerProviderInterface::class => [
LazyServiceFactory::class,
],
+21
View File
@@ -0,0 +1,21 @@
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.
+30
View File
@@ -0,0 +1,30 @@
# Shlink IP Address Geolocation module
Shlink module with tools to geolocate an IP address using different strategies.
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-ip-geolocation
> This library is also an expressive module which provides its own `ConfigProvider`. Add it to your configuration to get everything automatically set up.
## *TODO*
```php
<?php
declare(strict_types=1);
return [
'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
'temp_dir' => sys_get_temp_dir(),
// 'download_from' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz',
],
];
```
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation;
use GeoIp2\Database\Reader;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Proxy\LazyServiceFactory;
return [
'dependencies' => [
'factories' => [
Reader::class => ConfigAbstractFactory::class,
],
'delegators' => [
// The GeoLite2 db reader has to be lazy so that it does not try to load the DB file at app bootstrapping.
// By doing so, it would fail the first time shlink tries to download it.
Reader::class => [
LazyServiceFactory::class,
],
],
'lazy_services' => [
'class_map' => [
Reader::class => Reader::class,
],
],
],
ConfigAbstractFactory::class => [
Reader::class => ['config.geolite2.db_location'],
],
];
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
namespace Shlinkio\Shlink\IpGeolocation\Exception;
use RuntimeException as SplRuntimeException;
@@ -1,13 +1,14 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
namespace Shlinkio\Shlink\IpGeolocation\Exception;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Throwable;
use function sprintf;
class WrongIpException extends RuntimeException
class WrongIpException extends RuntimeException implements ExceptionInterface
{
public static function fromIpAddress($ipAddress, ?Throwable $prev = null): self
{
@@ -8,7 +8,7 @@ use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use PharData;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Symfony\Component\Filesystem\Exception as FilesystemException;
use Symfony\Component\Filesystem\Filesystem;
use Throwable;
@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\GeoLite2;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
interface DbUpdaterInterface
{
@@ -3,9 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class ChainIpLocationResolver implements IpLocationResolverInterface
{
@@ -3,9 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class EmptyIpLocationResolver implements IpLocationResolverInterface
{
@@ -8,9 +8,8 @@ use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Model\City;
use GeoIp2\Record\Subdivision;
use MaxMind\Db\Reader\InvalidDatabaseException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use function Functional\first;
@@ -6,9 +6,8 @@ namespace Shlinkio\Shlink\IpGeolocation\Resolver;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use function Shlinkio\Shlink\Common\json_decode;
use function sprintf;
@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model;
interface IpLocationResolverInterface
@@ -1,16 +1,16 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Exception;
namespace ShlinkioTest\Shlink\IpGeolocation\Exception;
use Exception;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
class WrongIpExceptionTest extends TestCase
{
/** @test */
public function fromIpAddressProperlyCreatesExceptionWithoutPrev()
public function fromIpAddressProperlyCreatesExceptionWithoutPrev(): void
{
$e = WrongIpException::fromIpAddress('1.2.3.4');
@@ -18,8 +18,9 @@ class WrongIpExceptionTest extends TestCase
$this->assertEquals(0, $e->getCode());
$this->assertNull($e->getPrevious());
}
/** @test */
public function fromIpAddressProperlyCreatesExceptionWithPrev()
public function fromIpAddressProperlyCreatesExceptionWithPrev(): void
{
$prev = new Exception('Previous error');
$e = WrongIpException::fromIpAddress('1.2.3.4', $prev);
@@ -8,7 +8,7 @@ use GuzzleHttp\Exception\ClientException;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
use Symfony\Component\Filesystem\Exception as FilesystemException;
@@ -5,7 +5,7 @@ namespace ShlinkioTest\Shlink\IpGeolocation\Resolver;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\ChainIpLocationResolver;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
@@ -9,7 +9,7 @@ use GeoIp2\Model\City;
use MaxMind\Db\Reader\InvalidDatabaseException;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\GeoLite2LocationResolver;
@@ -8,7 +8,7 @@ use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver;
@@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
@@ -32,6 +33,7 @@ return [
Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\UpdateTagAction::class => ConfigAbstractFactory::class,
ImplicitOptionsMiddleware::class => Middleware\EmptyResponseImplicitOptionsMiddlewareFactory::class,
Middleware\BodyParserMiddleware::class => InvokableFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class,
@@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Rest;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata);
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
class EmptyResponseImplicitOptionsMiddlewareFactory
{
public function __invoke()
{
return new ImplicitOptionsMiddleware(function () {
return new EmptyResponse();
});
}
}
@@ -10,9 +10,10 @@ use Psr\Http\Server\RequestHandlerInterface;
use function str_replace;
/** @deprecated */
class ShortCodePathMiddleware implements MiddlewareInterface
{
private const OLD_PATH_PREFIX = '/short-codes';
private const OLD_PATH_PREFIX = '/short-codes'; // Old path is deprecated. Remove this middleware on v2
private const NEW_PATH_PREFIX = '/short-urls';
/**
@@ -8,7 +8,7 @@ use ShlinkioTest\Shlink\Common\ApiTest\ApiTestCase;
class ListShortUrlsTest extends ApiTestCase
{
/** @test */
public function shortUrlsAreProperlyListed()
public function shortUrlsAreProperlyListed(): void
{
$resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls');
$respPayload = $this->getJsonResponsePayload($resp);
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace ShlinkioApiTest\Shlink\Rest\Action;
use ShlinkioTest\Shlink\Common\ApiTest\ApiTestCase;
use function explode;
class OptionsRequestTest extends ApiTestCase
{
/** @test */
public function optionsRequestsReturnEmptyResponse(): void
{
$resp = $this->callApi(self::METHOD_OPTIONS, '/short-urls');
$this->assertEquals(self::STATUS_NO_CONTENT, $resp->getStatusCode());
$this->assertEmpty((string) $resp->getBody());
}
/** @test */
public function optionsRequestsReturnAllowedMethodsForEndpoint(): void
{
$resp = $this->callApi(self::METHOD_OPTIONS, '/short-urls');
$allowedMethods = $resp->getHeaderLine('Allow');
$this->assertEquals([
self::METHOD_GET,
self::METHOD_POST,
], explode(',', $allowedMethods));
}
}
@@ -1,14 +1,13 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Factory;
namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Shlinkio\Shlink\Rest\Middleware\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\ServiceManager;
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
{
@@ -21,16 +20,16 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
}
/** @test */
public function serviceIsCreated()
public function serviceIsCreated(): void
{
$instance = $this->factory->__invoke(new ServiceManager(), '');
$instance = ($this->factory)();
$this->assertInstanceOf(ImplicitOptionsMiddleware::class, $instance);
}
/** @test */
public function responsePrototypeIsEmptyResponse()
public function responsePrototypeIsEmptyResponse(): void
{
$instance = $this->factory->__invoke(new ServiceManager(), '');
$instance = ($this->factory)();
$ref = new ReflectionObject($instance);
$prop = $ref->getProperty('responseFactory');
+1 -1
View File
@@ -5,9 +5,9 @@ namespace ShlinkioTest\Shlink\Rest\Util;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use Shlinkio\Shlink\Rest\Util\RestUtils;