diff --git a/module/Common/README.md b/module/Common/README.md new file mode 100644 index 00000000..3b903f41 --- /dev/null +++ b/module/Common/README.md @@ -0,0 +1,40 @@ +# 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. + +## 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. + * A `PredisCache` instance when the `cache.redis` config is defined. + * An `ArrayCache` instance when no `cache.redis` is defined and the APCu extension is not installed. + * An `ApcuCache`instance when no `cache.redis` is defined and the APCu extension is installed. + + Any of the adapters will use the namespace defined in `cache.namespace` config entry. + + ```php + 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. diff --git a/module/Common/src/Cache/CacheFactory.php b/module/Common/src/Cache/CacheFactory.php index 907621ea..12676dd8 100644 --- a/module/Common/src/Cache/CacheFactory.php +++ b/module/Common/src/Cache/CacheFactory.php @@ -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); + } } diff --git a/module/Common/test/Cache/CacheFactoryTest.php b/module/Common/test/Cache/CacheFactoryTest.php index d83d56b7..9575befc 100644 --- a/module/Common/test/Cache/CacheFactoryTest.php +++ b/module/Common/test/Cache/CacheFactoryTest.php @@ -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; + }]; } }