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
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,

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

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;

View File

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

View File

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

View File

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

View File

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

View File

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

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,
],
],
],

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

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;

View File

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

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

View File

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

View File

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

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 Zend\ServiceManager\AbstractPluginManager;
/** @deprecated */
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
{
protected $instanceOf = Image::class;

View File

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

View File

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

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
{
/**

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' => []]);

View File

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

View File

@ -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.

View File

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

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
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
namespace Shlinkio\Shlink\Common\Response;
use Fig\Http\Message\StatusCodeInterface as StatusCode;
use finfo;

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

View File

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

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

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];

View File

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

View File

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

View File

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

View File

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

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\ORM\Mapping\Builder\ClassMetadataBuilder;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
/** @var $metadata ClassMetadata */
$builder = new ClassMetadataBuilder($metadata);

View File

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

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;

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

View File

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

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

View File

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

View File

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

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;

View File

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

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

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
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
namespace Shlinkio\Shlink\IpGeolocation\Exception;
use RuntimeException as SplRuntimeException;

View File

@ -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
{

View File

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

View File

@ -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
{

View File

@ -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
{

View File

@ -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
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

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;
/** @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';
/**

View File

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

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
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');

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;