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

Feature/common module
This commit is contained in:
Alejandro Celaya 2019-08-11 15:18:28 +02:00 committed by GitHub
commit da88ec6807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 616 additions and 354 deletions

View File

@ -1,18 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Expressive; use Zend\Expressive;
use Zend\Expressive\Container; use Zend\Expressive\Container;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
return [ return [
'dependencies' => [ 'dependencies' => [
'factories' => [
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
],
'delegators' => [ 'delegators' => [
Expressive\Application::class => [ Expressive\Application::class => [
Container\ApplicationConfigInjectionDelegator::class, Container\ApplicationConfigInjectionDelegator::class,

View File

@ -7,7 +7,7 @@ use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
use PDO; use PDO;
use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Common\Util\IpAddress;
/** /**
@ -60,7 +60,7 @@ final class Version20180913205455 extends AbstractMigration
try { try {
return (string) IpAddress::fromString($addr)->getObfuscatedCopy(); return (string) IpAddress::fromString($addr)->getObfuscatedCopy();
} catch (WrongIpException $e) { } catch (InvalidArgumentException $e) {
return null; return null;
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api; namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes; 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\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;

View File

@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Util\ExitCodes; 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\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;

View File

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -62,7 +62,7 @@ class ListShortUrlsCommand extends Command
'page', 'page',
'p', 'p',
InputOption::VALUE_OPTIONAL, 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' '1'
) )
->addOption( ->addOption(

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag; namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes; 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\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface; use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;

View File

@ -9,12 +9,12 @@ use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Service\VisitServiceInterface; use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit; namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCodes; 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 Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Util;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use GeoIp2\Database\Reader; use GeoIp2\Database\Reader;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; 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 Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock\Factory as Locker; use Symfony\Component\Lock\Factory as Locker;
use Throwable; use Throwable;

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Console; namespace Shlinkio\Shlink\CLI\Util;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;

View File

@ -9,13 +9,13 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand; use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Service\VisitService; use Shlinkio\Shlink\Core\Service\VisitService;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver; use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;

View File

@ -8,7 +8,7 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand; use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; 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 Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;

View File

@ -11,7 +11,7 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater; 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 Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock; use Symfony\Component\Lock;
use Throwable; use Throwable;

View File

@ -1,13 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Console; namespace ShlinkioTest\Shlink\CLI\Util;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use ReflectionObject; 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\Table;
use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Helper\TableStyle;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -26,7 +26,7 @@ class ShlinkTableTest extends TestCase
} }
/** @test */ /** @test */
public function renderMakesTableToBeRenderedWithProvidedInfo() public function renderMakesTableToBeRenderedWithProvidedInfo(): void
{ {
$headers = []; $headers = [];
$rows = [[]]; $rows = [[]];
@ -53,7 +53,7 @@ class ShlinkTableTest extends TestCase
} }
/** @test */ /** @test */
public function newTableIsCreatedForFactoryMethod() public function newTableIsCreatedForFactoryMethod(): void
{ {
$instance = ShlinkTable::fromOutput($this->prophesize(OutputInterface::class)->reveal()); $instance = ShlinkTable::fromOutput($this->prophesize(OutputInterface::class)->reveal());

21
module/Common/LICENSE Normal file
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
module/Common/README.md Normal file
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',
],
],
];
```

View File

@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common; namespace Shlinkio\Shlink\Common;
use GeoIp2\Database\Reader;
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Client as GuzzleClient;
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -12,7 +11,6 @@ use Symfony\Component\Filesystem\Filesystem;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory; use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\Proxy\LazyServiceFactory;
return [ return [
@ -20,9 +18,8 @@ return [
'factories' => [ 'factories' => [
GuzzleClient::class => InvokableFactory::class, GuzzleClient::class => InvokableFactory::class,
Filesystem::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, Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class, Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
@ -44,24 +41,9 @@ return [
'abstract_factories' => [ 'abstract_factories' => [
Factory\DottedAccessConfigAbstractFactory::class, 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 => [ ConfigAbstractFactory::class => [
Reader::class => ['config.geolite2.db_location'],
Template\Extension\TranslatorExtension::class => ['translator'], Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'], Middleware\LocaleMiddleware::class => ['translator'],
Middleware\CloseDbConnectionMiddleware::class => ['em'], Middleware\CloseDbConnectionMiddleware::class => ['em'],

View File

@ -11,7 +11,7 @@ return [
'entity_manager' => [ 'entity_manager' => [
'orm' => [ 'orm' => [
'types' => [ 'types' => [
Type\ChronosDateTimeType::CHRONOS_DATETIME => Type\ChronosDateTimeType::class, Doctrine\Type\ChronosDateTimeType::CHRONOS_DATETIME => Doctrine\Type\ChronosDateTimeType::class,
], ],
], ],
], ],

View File

@ -4,22 +4,48 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Cache; namespace Shlinkio\Shlink\Common\Cache;
use Doctrine\Common\Cache; use Doctrine\Common\Cache;
use Interop\Container\ContainerInterface; use Predis\Client as PredisClient;
use Shlinkio\Shlink\Core\Options\AppOptions; use Psr\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
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 /** @var callable|null */
{ private $apcuEnabled;
// TODO Make use of the redis cache via RedisFactory when possible
$appOptions = $container->get(AppOptions::class); public function __construct(?callable $apcuEnabled = null)
$adapter = env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache(); {
$adapter->setNamespace((string) $appOptions); $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; 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

@ -16,7 +16,8 @@ class RedisFactory
public function __invoke(ContainerInterface $container): PredisClient 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 = $redisConfig['servers'] ?? [];
$servers = is_string($servers) ? explode(',', $servers) : $servers; $servers = is_string($servers) ? explode(',', $servers) : $servers;

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Type; namespace Shlinkio\Shlink\Common\Doctrine\Type;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use DateTimeInterface; use DateTimeInterface;

View File

@ -3,16 +3,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Entity; namespace Shlinkio\Shlink\Common\Entity;
use Doctrine\ORM\Mapping as ORM;
abstract class AbstractEntity abstract class AbstractEntity
{ {
/** /** @var string */
* @var string
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(name="id", type="bigint", options={"unsigned"=true})
*/
protected $id; protected $id;
public function getId(): string public function getId(): string

View File

@ -3,6 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception; namespace Shlinkio\Shlink\Common\Exception;
use RuntimeException;
use function sprintf; use function sprintf;
/** @deprecated */ /** @deprecated */

View File

@ -22,11 +22,9 @@ class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
/** /**
* Can the factory create an instance for the service? * Can the factory create an instance for the service?
* *
* @param ContainerInterface $container
* @param string $requestedName * @param string $requestedName
* @return bool
*/ */
public function canCreate(ContainerInterface $container, $requestedName) public function canCreate(ContainerInterface $container, $requestedName): bool
{ {
return substr_count($requestedName, '.') > 0; return substr_count($requestedName, '.') > 0;
} }

View File

@ -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();
});
}
}

View File

@ -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'] ?? []);
}
}

View File

@ -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'] ?? []);
}
}

View File

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Common\Image;
use mikehaertl\wkhtmlto\Image; use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\AbstractPluginManager; use Zend\ServiceManager\AbstractPluginManager;
/** @deprecated */
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
{ {
protected $instanceOf = Image::class; protected $instanceOf = Image::class;

View File

@ -10,6 +10,7 @@ use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException; use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\Factory\FactoryInterface;
/** @deprecated */
class ImageBuilderFactory implements FactoryInterface class ImageBuilderFactory implements FactoryInterface
{ {
/** /**

View File

@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common\Image;
use Zend\ServiceManager\ServiceLocatorInterface; use Zend\ServiceManager\ServiceLocatorInterface;
/** @deprecated */
interface ImageBuilderInterface extends ServiceLocatorInterface interface ImageBuilderInterface extends ServiceLocatorInterface
{ {
} }

View File

@ -10,6 +10,7 @@ use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException; use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\Factory\FactoryInterface;
/** @deprecated */
class ImageFactory implements FactoryInterface class ImageFactory implements FactoryInterface
{ {
/** /**

View File

@ -5,29 +5,14 @@ namespace Shlinkio\Shlink\Common\Logger;
use Cascade\Cascade; use Cascade\Cascade;
use Interop\Container\ContainerInterface; use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException; use Monolog\Logger;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
use function count; use function count;
use function explode; use function explode;
class LoggerFactory implements FactoryInterface class LoggerFactory
{ {
/** public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): Logger
* 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->has('config') ? $container->get('config') : []; $config = $container->has('config') ? $container->get('config') : [];
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]); Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);

View File

@ -3,28 +3,17 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware; namespace Shlinkio\Shlink\Common\Middleware;
use Interop\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use RKA\Middleware\IpAddress; 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
{ {
/** public const REQUEST_ATTR = 'remote_address';
* Create an object
* public function __invoke(ContainerInterface $container): IpAddress
* @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
{ {
$config = $container->get('config'); $config = $container->get('config');
$headersToInspect = $config['ip_address_resolution']['headers_to_inspect'] ?? []; $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);
} }
} }

View File

@ -24,8 +24,6 @@ class LocaleMiddleware implements MiddlewareInterface
$this->translator = $translator; $this->translator = $translator;
} }
/** /**
* Process an incoming server request and return a response, optionally delegating * Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response. * to the next middleware component to create the response.

View File

@ -31,12 +31,6 @@ trait PaginatorUtilsTrait
return $transformer === null ? $items : array_map([$transformer, 'transform'], $items); 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 private function isLastPage(Paginator $paginator): bool
{ {
return $paginator->getCurrentPageNumber() >= $paginator->count(); return $paginator->getCurrentPageNumber() >= $paginator->count();

View File

@ -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;
}

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util; namespace Shlinkio\Shlink\Common\Response;
use Fig\Http\Message\StatusCodeInterface as StatusCode; use Fig\Http\Message\StatusCodeInterface as StatusCode;
use finfo; use finfo;

View File

@ -3,11 +3,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util; namespace Shlinkio\Shlink\Common\Util;
use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use function count; use function count;
use function explode; use function explode;
use function implode; use function implode;
use function sprintf;
use function trim; use function trim;
final class IpAddress final class IpAddress
@ -36,14 +37,14 @@ final class IpAddress
/** /**
* @param string $address * @param string $address
* @return IpAddress * @return IpAddress
* @throws WrongIpException * @throws InvalidArgumentException
*/ */
public static function fromString(string $address): self public static function fromString(string $address): self
{ {
$address = trim($address); $address = trim($address);
$parts = explode('.', $address); $parts = explode('.', $address);
if (count($parts) !== self::IPV4_PARTS_COUNT) { 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); return new self(...$parts);

View File

@ -9,7 +9,6 @@ use GuzzleHttp\ClientInterface;
use GuzzleHttp\RequestOptions; use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
use function Shlinkio\Shlink\Common\json_decode; use function Shlinkio\Shlink\Common\json_decode;
use function sprintf; 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 protected function callApiWithKey(string $method, string $uri, array $options = []): ResponseInterface
{ {
$headers = $options[RequestOptions::HEADERS] ?? []; $headers = $options[RequestOptions::HEADERS] ?? [];
$headers[ApiKeyHeaderPlugin::HEADER_NAME] = 'valid_api_key'; $headers['X-Api-Key'] = 'valid_api_key';
$options[RequestOptions::HEADERS] = $headers; $options[RequestOptions::HEADERS] = $headers;
return $this->callApi($method, $uri, $options); return $this->callApi($method, $uri, $options);

View File

@ -3,48 +3,60 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Cache; namespace ShlinkioTest\Shlink\Common\Cache;
use Doctrine\Common\Cache\ApcuCache; use Doctrine\Common\Cache;
use Doctrine\Common\Cache\ArrayCache;
use PHPUnit\Framework\TestCase; 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\CacheFactory;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Common\Cache\RedisFactory;
use Zend\ServiceManager\ServiceManager;
use function putenv;
class CacheFactoryTest extends TestCase class CacheFactoryTest extends TestCase
{ {
/** @var CacheFactory */ /** @var ObjectProphecy */
private $factory; private $container;
/** @var ServiceManager */
private $sm;
public function setUp(): void public function setUp(): void
{ {
$this->factory = new CacheFactory(); $this->container = $this->prophesize(ContainerInterface::class);
$this->sm = new ServiceManager(['services' => [
AppOptions::class => new AppOptions(),
]]);
} }
public static function tearDownAfterClass(): void /**
{ * @test
putenv('APP_ENV'); * @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 provideCacheConfig(): iterable
public function productionReturnsApcAdapter(): void
{ {
putenv('APP_ENV=pro'); yield 'debug true' => [['debug' => true], Cache\ArrayCache::class, ''];
$instance = ($this->factory)($this->sm, ''); yield 'debug false' => [['debug' => false], Cache\ApcuCache::class, ''];
$this->assertInstanceOf(ApcuCache::class, $instance); yield 'no debug' => [[], Cache\ApcuCache::class, ''];
} yield 'with redis' => [['cache' => [
'namespace' => $namespace = 'some_namespace',
/** @test */ 'redis' => [],
public function developmentReturnsArrayAdapter(): void ]], Cache\PredisCache::class, $namespace];
{ yield 'debug false and no apcu' => [['debug' => false], Cache\ArrayCache::class, '', function () {
putenv('APP_ENV=dev'); return false;
$instance = ($this->factory)($this->sm, ''); }];
$this->assertInstanceOf(ArrayCache::class, $instance);
} }
} }

View File

@ -27,7 +27,7 @@ class RedisFactoryTest extends TestCase
* @test * @test
* @dataProvider provideRedisConfig * @dataProvider provideRedisConfig
*/ */
public function createsRedisClientBasedOnConfig(?array $config, string $expectedCluster): void public function createsRedisClientBasedOnRedisConfig(?array $config, string $expectedCluster): void
{ {
$getConfig = $this->container->get('config')->willReturn([ $getConfig = $this->container->get('config')->willReturn([
'redis' => $config, 'redis' => $config,
@ -39,6 +39,24 @@ class RedisFactoryTest extends TestCase
$this->assertInstanceOf($expectedCluster, $client->getOptions()->cluster); $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 public function provideRedisConfig(): iterable
{ {
yield 'no config' => [null, PredisCluster::class]; yield 'no config' => [null, PredisCluster::class];

View File

@ -6,7 +6,7 @@ namespace ShlinkioTest\Shlink\Common\Doctrine;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Doctrine\EntityManagerFactory; use Shlinkio\Shlink\Common\Doctrine\EntityManagerFactory;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;
class EntityManagerFactoryTest extends TestCase class EntityManagerFactoryTest extends TestCase

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Type; namespace ShlinkioTest\Shlink\Common\Doctrine\Type;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use DateTime; use DateTime;
@ -11,7 +11,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use stdClass; use stdClass;
class ChronosDateTimeTypeTest extends TestCase class ChronosDateTimeTypeTest extends TestCase

View File

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

View File

@ -6,7 +6,6 @@ namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use ReflectionObject; use ReflectionObject;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory; use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
use Shlinkio\Shlink\Core\Model\Visitor;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;
class IpAddressMiddlewareFactoryTest extends TestCase class IpAddressMiddlewareFactoryTest extends TestCase
@ -26,7 +25,7 @@ class IpAddressMiddlewareFactoryTest extends TestCase
{ {
$instance = ($this->factory)(new ServiceManager(['services' => [ $instance = ($this->factory)(new ServiceManager(['services' => [
'config' => $config, 'config' => $config,
]]), ''); ]]));
$ref = new ReflectionObject($instance); $ref = new ReflectionObject($instance);
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders'); $checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
@ -40,7 +39,7 @@ class IpAddressMiddlewareFactoryTest extends TestCase
$this->assertTrue($checkProxyHeaders->getValue($instance)); $this->assertTrue($checkProxyHeaders->getValue($instance));
$this->assertEquals([], $trustedProxies->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)); $this->assertEquals($expectedHeadersToInspect, $headersToInspect->getValue($instance));
} }

View File

@ -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();
}
}

View File

@ -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'];
}
}

View File

@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Core;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder; use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */ /** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata); $builder = new ClassMetadataBuilder($metadata);

View File

@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Core;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder; use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */ /** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata); $builder = new ClassMetadataBuilder($metadata);

View File

@ -10,8 +10,8 @@ use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException; use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface; use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait; use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Entity;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use JsonSerializable; use JsonSerializable;
use Shlinkio\Shlink\Common\Entity\AbstractEntity; 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\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation; use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
@ -45,7 +45,7 @@ class Visit extends AbstractEntity implements JsonSerializable
try { try {
return (string) IpAddress::fromString($address)->getObfuscatedCopy(); return (string) IpAddress::fromString($address)->getObfuscatedCopy();
} catch (WrongIpException $e) { } catch (InvalidArgumentException $e) {
return null; return null;
} }
} }

View File

@ -7,9 +7,9 @@ use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;

View File

@ -4,11 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model; namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
final class Visitor final class Visitor
{ {
public const REMOTE_ADDRESS_ATTR = 'remote_address';
/** @var string */ /** @var string */
private $userAgent; private $userAgent;
/** @var string */ /** @var string */
@ -28,7 +27,7 @@ final class Visitor
return new self( return new self(
$request->getHeaderLine('User-Agent'), $request->getHeaderLine('User-Agent'),
$request->getHeaderLine('Referer'), $request->getHeaderLine('Referer'),
$request->getAttribute(self::REMOTE_ADDRESS_ATTR) $request->getAttribute(IpAddressMiddlewareFactory::REQUEST_ATTR)
); );
} }

View File

@ -1,20 +1,20 @@
<?php <?php
declare(strict_types=1); 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 Zend\Paginator\Adapter\AdapterInterface;
use function strip_tags; use function strip_tags;
use function trim; use function trim;
class PaginableRepositoryAdapter implements AdapterInterface class ShortUrlRepositoryAdapter implements AdapterInterface
{ {
public const ITEMS_PER_PAGE = 10; public const ITEMS_PER_PAGE = 10;
/** @var PaginableRepositoryInterface */ /** @var ShortUrlRepositoryInterface */
private $paginableRepository; private $repository;
/** @var null|string */ /** @var null|string */
private $searchTerm; private $searchTerm;
/** @var null|array|string */ /** @var null|array|string */
@ -23,12 +23,12 @@ class PaginableRepositoryAdapter implements AdapterInterface
private $tags; private $tags;
public function __construct( public function __construct(
PaginableRepositoryInterface $paginableRepository, ShortUrlRepositoryInterface $repository,
$searchTerm = null, $searchTerm = null,
array $tags = [], array $tags = [],
$orderBy = null $orderBy = null
) { ) {
$this->paginableRepository = $paginableRepository; $this->repository = $repository;
$this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null; $this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null;
$this->orderBy = $orderBy; $this->orderBy = $orderBy;
$this->tags = $tags; $this->tags = $tags;
@ -43,7 +43,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
*/ */
public function getItems($offset, $itemCountPerPage): array public function getItems($offset, $itemCountPerPage): array
{ {
return $this->paginableRepository->findList( return $this->repository->findList(
$itemCountPerPage, $itemCountPerPage,
$offset, $offset,
$this->searchTerm, $this->searchTerm,
@ -63,6 +63,6 @@ class PaginableRepositoryAdapter implements AdapterInterface
*/ */
public function count(): int public function count(): int
{ {
return $this->paginableRepository->countList($this->searchTerm, $this->tags); return $this->repository->countList($this->searchTerm, $this->tags);
} }
} }

View File

@ -4,10 +4,27 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\Common\Persistence\ObjectRepository;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; 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; public function findOneByShortCode(string $shortCode): ?ShortUrl;
} }

View File

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service; namespace Shlinkio\Shlink\Core\Service;
use Doctrine\ORM; use Doctrine\ORM;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait; use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait;
use Shlinkio\Shlink\Core\Util\TagManagerTrait; use Shlinkio\Shlink\Core\Util\TagManagerTrait;
@ -35,8 +35,8 @@ class ShortUrlService implements ShortUrlServiceInterface
{ {
/** @var ShortUrlRepository $repo */ /** @var ShortUrlRepository $repo */
$repo = $this->em->getRepository(ShortUrl::class); $repo = $this->em->getRepository(ShortUrl::class);
$paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery, $tags, $orderBy)); $paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $searchQuery, $tags, $orderBy));
$paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE) $paginator->setItemCountPerPage(ShortUrlRepositoryAdapter::ITEMS_PER_PAGE)
->setCurrentPageNumber($page); ->setCurrentPageNumber($page);
return $paginator; return $paginator;

View File

@ -10,7 +10,6 @@ use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; 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\EventDispatcher\ShortUrlVisited;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation; use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;

View File

@ -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();
}
}

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.

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.

View File

@ -23,6 +23,8 @@ return [
Psr\EventDispatcherInterface::class => Phly\EventDispatcher::class, Psr\EventDispatcherInterface::class => Phly\EventDispatcher::class,
], ],
'delegators' => [ '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 => [ Psr\ListenerProviderInterface::class => [
LazyServiceFactory::class, LazyServiceFactory::class,
], ],

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.

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',
],
];
```

View File

@ -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'],
],
];

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception; namespace Shlinkio\Shlink\IpGeolocation\Exception;
use RuntimeException as SplRuntimeException; use RuntimeException as SplRuntimeException;

View File

@ -1,13 +1,14 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception; namespace Shlinkio\Shlink\IpGeolocation\Exception;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Throwable; use Throwable;
use function sprintf; use function sprintf;
class WrongIpException extends RuntimeException class WrongIpException extends RuntimeException implements ExceptionInterface
{ {
public static function fromIpAddress($ipAddress, ?Throwable $prev = null): self public static function fromIpAddress($ipAddress, ?Throwable $prev = null): self
{ {

View File

@ -8,7 +8,7 @@ use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions; use GuzzleHttp\RequestOptions;
use PharData; 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\Exception as FilesystemException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Throwable; use Throwable;

View File

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\GeoLite2; namespace Shlinkio\Shlink\IpGeolocation\GeoLite2;
use Shlinkio\Shlink\Common\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
interface DbUpdaterInterface interface DbUpdaterInterface
{ {

View File

@ -3,9 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Resolver; 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\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class ChainIpLocationResolver implements IpLocationResolverInterface class ChainIpLocationResolver implements IpLocationResolverInterface
{ {

View File

@ -3,9 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Resolver; 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\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class EmptyIpLocationResolver implements IpLocationResolverInterface class EmptyIpLocationResolver implements IpLocationResolverInterface
{ {

View File

@ -8,9 +8,8 @@ use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Model\City; use GeoIp2\Model\City;
use GeoIp2\Record\Subdivision; use GeoIp2\Record\Subdivision;
use MaxMind\Db\Reader\InvalidDatabaseException; 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\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use function Functional\first; use function Functional\first;

View File

@ -6,9 +6,8 @@ namespace Shlinkio\Shlink\IpGeolocation\Resolver;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; 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\Model;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use function Shlinkio\Shlink\Common\json_decode; use function Shlinkio\Shlink\Common\json_decode;
use function sprintf; use function sprintf;

View File

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\IpGeolocation\Resolver; 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\Model;
interface IpLocationResolverInterface interface IpLocationResolverInterface

View File

@ -1,16 +1,16 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Exception; namespace ShlinkioTest\Shlink\IpGeolocation\Exception;
use Exception; use Exception;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
class WrongIpExceptionTest extends TestCase class WrongIpExceptionTest extends TestCase
{ {
/** @test */ /** @test */
public function fromIpAddressProperlyCreatesExceptionWithoutPrev() public function fromIpAddressProperlyCreatesExceptionWithoutPrev(): void
{ {
$e = WrongIpException::fromIpAddress('1.2.3.4'); $e = WrongIpException::fromIpAddress('1.2.3.4');
@ -18,8 +18,9 @@ class WrongIpExceptionTest extends TestCase
$this->assertEquals(0, $e->getCode()); $this->assertEquals(0, $e->getCode());
$this->assertNull($e->getPrevious()); $this->assertNull($e->getPrevious());
} }
/** @test */ /** @test */
public function fromIpAddressProperlyCreatesExceptionWithPrev() public function fromIpAddressProperlyCreatesExceptionWithPrev(): void
{ {
$prev = new Exception('Previous error'); $prev = new Exception('Previous error');
$e = WrongIpException::fromIpAddress('1.2.3.4', $prev); $e = WrongIpException::fromIpAddress('1.2.3.4', $prev);

View File

@ -8,7 +8,7 @@ use GuzzleHttp\Exception\ClientException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; 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\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options; use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
use Symfony\Component\Filesystem\Exception as FilesystemException; use Symfony\Component\Filesystem\Exception as FilesystemException;

View File

@ -5,7 +5,7 @@ namespace ShlinkioTest\Shlink\IpGeolocation\Resolver;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; 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\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\ChainIpLocationResolver; use Shlinkio\Shlink\IpGeolocation\Resolver\ChainIpLocationResolver;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;

View File

@ -9,7 +9,7 @@ use GeoIp2\Model\City;
use MaxMind\Db\Reader\InvalidDatabaseException; use MaxMind\Db\Reader\InvalidDatabaseException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; 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\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\GeoLite2LocationResolver; use Shlinkio\Shlink\IpGeolocation\Resolver\GeoLite2LocationResolver;

View File

@ -8,7 +8,7 @@ use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; 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\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver; use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver;

View File

@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory; use Zend\ServiceManager\Factory\InvokableFactory;
@ -32,6 +33,7 @@ return [
Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class, Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\UpdateTagAction::class => ConfigAbstractFactory::class, Action\Tag\UpdateTagAction::class => ConfigAbstractFactory::class,
ImplicitOptionsMiddleware::class => Middleware\EmptyResponseImplicitOptionsMiddlewareFactory::class,
Middleware\BodyParserMiddleware::class => InvokableFactory::class, Middleware\BodyParserMiddleware::class => InvokableFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class, Middleware\PathVersionMiddleware::class => InvokableFactory::class,

View File

@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Rest;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder; use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */ /** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata); $builder = new ClassMetadataBuilder($metadata);

View File

@ -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();
});
}
}

View File

@ -10,9 +10,10 @@ use Psr\Http\Server\RequestHandlerInterface;
use function str_replace; use function str_replace;
/** @deprecated */
class ShortCodePathMiddleware implements MiddlewareInterface 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'; private const NEW_PATH_PREFIX = '/short-urls';
/** /**

View File

@ -8,7 +8,7 @@ use ShlinkioTest\Shlink\Common\ApiTest\ApiTestCase;
class ListShortUrlsTest extends ApiTestCase class ListShortUrlsTest extends ApiTestCase
{ {
/** @test */ /** @test */
public function shortUrlsAreProperlyListed() public function shortUrlsAreProperlyListed(): void
{ {
$resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls'); $resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls');
$respPayload = $this->getJsonResponsePayload($resp); $respPayload = $this->getJsonResponsePayload($resp);

View File

@ -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));
}
}

View File

@ -1,14 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Factory; namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use ReflectionObject; use ReflectionObject;
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory; use Shlinkio\Shlink\Rest\Middleware\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware; use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\ServiceManager;
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
{ {
@ -21,16 +20,16 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
} }
/** @test */ /** @test */
public function serviceIsCreated() public function serviceIsCreated(): void
{ {
$instance = $this->factory->__invoke(new ServiceManager(), ''); $instance = ($this->factory)();
$this->assertInstanceOf(ImplicitOptionsMiddleware::class, $instance); $this->assertInstanceOf(ImplicitOptionsMiddleware::class, $instance);
} }
/** @test */ /** @test */
public function responsePrototypeIsEmptyResponse() public function responsePrototypeIsEmptyResponse(): void
{ {
$instance = $this->factory->__invoke(new ServiceManager(), ''); $instance = ($this->factory)();
$ref = new ReflectionObject($instance); $ref = new ReflectionObject($instance);
$prop = $ref->getProperty('responseFactory'); $prop = $ref->getProperty('responseFactory');

View File

@ -5,9 +5,9 @@ namespace ShlinkioTest\Shlink\Rest\Util;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException; use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;