diff --git a/_utils/templates/generate-compose-json.php b/_utils/templates/generate-compose-json.php --- a/_utils/templates/generate-compose-json.php +++ b/_utils/templates/generate-compose-json.php @@ -140,7 +140,11 @@ "name" => "Keruald contributors", ], ], + "provide" => [ + "psr/simple-cache-implementation" => "1.0|2.0|3.0", + ], "require" => [ + "psr/simple-cache" => "^1.0|^2.0|^3.0", "ext-intl" => "*", ], "require-dev" => [ @@ -153,6 +157,10 @@ "symfony/yaml" => "^6.0.3", "squizlabs/php_codesniffer" => "^3.6", ], + "suggest" => [ + "ext-memcached" => "*", + "ext-redis" => "*", + ], "replace" => getReplace($metadata["packages"]), "autoload" => getAutoload($metadata["packages_namespaces"]), "scripts" => [ diff --git a/cache/LICENSE b/cache/LICENSE new file mode 100644 --- /dev/null +++ b/cache/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2010, 2023 Sébastien Santoro aka Dereckson +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cache/README.md b/cache/README.md new file mode 100644 --- /dev/null +++ b/cache/README.md @@ -0,0 +1,42 @@ +# keruald/cache + +This library offers a simple layer of abstraction +for cache operations, with concrete implementations. + +This cache implementation is compatible with PSR-16. + +This cache implementation is NOT compatible with PSR-6. + +## Configuration + +To get a cache instance, you need to pass configuration as an array. + +The properties and values depend on the engine you want to use. + +### Memcached + +| Key | Value | Default | +|---------------|--------------------------------|:------------| +| engine | MemcachedCache class reference | | +| server | The memcached hostname | "localhost" | +| port | The memcached port | 11211 | +| sasl_username | The SASL username | | +| sasl_password | The SASL password | "" | + +### Redis + +| Key | Value | Default | +|----------|--------------------------------|:------------| +| engine | MemcachedCache class reference | | +| server | The memcached hostname | "localhost" | +| port | The memcached port | 6379 | +| database | The redis database number | 0 | + +### Void + +This cache allows unit tests or to offer a default cache, +when no other configuration is offered. + +| Key | Value | +|------------|---------------------------| +| engine | VoidCache class reference | diff --git a/cache/composer.json b/cache/composer.json new file mode 100644 --- /dev/null +++ b/cache/composer.json @@ -0,0 +1,29 @@ +{ + "name": "keruald/cache", + "description": "Abstraction layer for cache. Compatible PSR-16.", + "keywords": [ + "keruald", + "cache" + ], + "minimum-stability": "stable", + "license": "BSD-2-Clause", + "authors": [ + { + "name": "Sébastien Santoro", + "email": "dereckson@espace-win.org" + } + ], + "provide": { + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "require": { + "keruald/omnitools": "^0.11" + }, + "require-dev": { + "phpunit/phpunit": "^10.2" + }, + "suggest": { + "ext-memcached": "*", + "ext-redis": "*" + } +} diff --git a/cache/src/Cache.php b/cache/src/Cache.php new file mode 100644 --- /dev/null +++ b/cache/src/Cache.php @@ -0,0 +1,49 @@ +<?php +declare(strict_types=1); + +namespace Keruald\Cache; + +use Keruald\OmniTools\Collections\MultipleOperation; +use Psr\SimpleCache\CacheInterface; + +use DateInterval; + +abstract class Cache implements CacheInterface { + + /// + /// Loader + /// + + public abstract static function load (array $config) : Cache; + + /// + /// Default implementation for CacheInterface -Multiple methods + /// + + public function getMultiple ( + iterable $keys, + mixed $default = null + ) : iterable { + foreach ($keys as $key) { + yield $key => $this->get($key, $default); + } + } + + public function setMultiple ( + iterable $values, + DateInterval|int|null $ttl = null + ) : bool { + return MultipleOperation::do( + $values, + fn($key, $value) => $this->set($key, $value, $ttl) + ); + } + + public function deleteMultiple (iterable $keys) : bool { + return MultipleOperation::do( + $keys, + fn($key) => $this->delete($key) + ); + } + +} diff --git a/cache/src/CacheFactory.php b/cache/src/CacheFactory.php new file mode 100644 --- /dev/null +++ b/cache/src/CacheFactory.php @@ -0,0 +1,44 @@ +<?php +declare(strict_types=1); + +namespace Keruald\Cache; + +use Keruald\Cache\Engines\CacheVoid; +use Keruald\Cache\Exceptions\CacheException; + +/** + * Cache caller + */ +class CacheFactory { + + const DEFAULT_ENGINE = CacheVoid::class; + + /** + * Loads the cache instance, building it according a configuration array. + * + * The correct cache instance to initialize will be determined from the + * 'engine' key. It should match the name of a Cache class. + * + * This method will create an instance of the specified object, + * calling the load static method from this object class. + * + * Example: + * <code> + * $config['engine'] = CacheQuux::class; + * $cache = Cache::load($config); //Cache:load() will call CacheQuux:load(); + * </code> + * + * @return Cache the cache instance + * @throws CacheException + */ + static function load (array $config) : Cache { + $engine = $config["engine"] ?? self::DEFAULT_ENGINE; + + if (!class_exists($engine)) { + throw new CacheException("Can't initialize $engine cache engine. The class can't be found."); + } + + return call_user_func([$engine, 'load'], $config); + } + +} diff --git a/cache/src/Engines/CacheMemcached.php b/cache/src/Engines/CacheMemcached.php new file mode 100644 --- /dev/null +++ b/cache/src/Engines/CacheMemcached.php @@ -0,0 +1,161 @@ +<?php +declare(strict_types=1); + +namespace Keruald\Cache\Engines; + +use Keruald\Cache\Cache; +use Keruald\Cache\Exceptions\CacheException; +use Keruald\Cache\Features\WithPrefix; +use Keruald\OmniTools\Collections\HashMap; +use Keruald\OmniTools\Collections\Vector; + +use Memcached; + +/** + * Memcached cache + * + * /!\ This class uses the Memcached extension AND NOT Memcache. + * + * References: + * + * @link https://www.php.net/manual/en/book.memcached.php + * @link https://memcached.org + */ +class CacheMemcached extends Cache { + + use WithPrefix; + + /// + /// Constants - default value + /// + + const DEFAULT_SERVER = "localhost"; + + const DEFAULT_PORT = 11211; + + /// + /// Properties + /// + + private Memcached $memcached; + + /// + /// Constructors + /// + + public function __construct (Memcached $memcached) { + $this->memcached = $memcached; + } + + public static function load (array $config) : self { + //Checks extension is okay + if (!extension_loaded('memcached')) { + if (extension_loaded('memcache')) { + throw new CacheException("Can't initialize Memcached cache engine: PHP extension memcached not loaded. This class uses the Memcached extension AND NOT the Memcache extension (this one is loaded).</strong>"); + } else { + throw new CacheException("Can't initialize Memcached cache engine: PHP extension memcached not loaded."); + } + } + + $memcached = new Memcached; + $memcached->addServer( + $config["server"] ?? self::DEFAULT_SERVER, + $config["port"] ?? self::DEFAULT_PORT, + ); + + // SASL authentication + if (array_key_exists("sasl_username", $config)) { + $memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); + $memcached->setSaslAuthData( + $config["sasl_username"], + $config["sasl_password"] ?? "", + ); + } + + return new self($memcached); + } + + /// + /// Cache operations + /// + + /** + * Gets the specified key's data + */ + function get (string $key, mixed $default = null) : mixed { + $key = $this->getUnsafePrefix() . $key; + + $result = $this->memcached->get($key); + + return match ($result) { + false => $default, + default => unserialize($result), + }; + } + + /** + * Sets the specified data at the specified key + */ + function set ( + string $key, + mixed $value, + null|int|\DateInterval $ttl = null + ) : bool { + $key = $this->getUnsafePrefix() . $key; + + return $this->memcached->set($key, serialize($value)); + } + + /** + * Deletes the specified key's data + * + * @param string $key the key to delete + */ + function delete (string $key) : bool { + $key = $this->getUnsafePrefix() . $key; + + return $this->memcached->delete($key); + } + + public function clear () : bool { + $keys = $this->memcached->getAllKeys(); + + if ($keys === false) { + return false; + } + + if ($this->hasPrefix()) { + // Restrict to our keys, as we don't use Memcached::OPT_PREFIX_KEY + $prefix = $this->getUnsafePrefix(); + $keys = Vector::from($keys) + ->filter(fn($key) => str_starts_with($key, $prefix)) + ->toArray(); + } + + $result = $this->memcached->deleteMulti($keys); + return self::areAllOperationsSuccessful($result); + } + + public function has (string $key) : bool { + $key = $this->getUnsafePrefix() . $key; + + $this->memcached->get($key); + + return match ($this->memcached->getResultCode()) { + Memcached::RES_NOTFOUND => false, + default => true, + }; + } + + /// + /// Helper methods + /// + + private static function areAllOperationsSuccessful (array $result) : bool { + return HashMap::from($result) + ->all(function ($key, $value) { + return $value === true; // can be true or Memcached::RES_* + }); + } + +} diff --git a/cache/src/Engines/CacheRedis.php b/cache/src/Engines/CacheRedis.php new file mode 100644 --- /dev/null +++ b/cache/src/Engines/CacheRedis.php @@ -0,0 +1,163 @@ +<?php + +namespace Keruald\Cache\Engines; + +use Keruald\Cache\Cache; +use Keruald\Cache\Exceptions\CacheException; + +use DateInterval; +use DateTimeImmutable; + +use Redis; +use RedisException; + +class CacheRedis extends Cache { + + /// + /// Constants - default value + /// + + const DEFAULT_SERVER = "localhost"; + + const DEFAULT_PORT = 6379; + + /// + /// Properties + /// + + private Redis $redis; + + /// + /// Constructors + /// + + public function __construct (Redis $client) { + $this->redis = $client; + } + + public static function load (array $config) : Cache { + //Checks extension is okay + if (!extension_loaded("redis")) { + throw new CacheException("Can't initialize Redis cache engine: PHP extension redis not loaded."); + } + + $client = new Redis(); + try { + $client->connect( + $config["server"] ?? self::DEFAULT_SERVER, + $config["port"] ?? self::DEFAULT_PORT, + ); + + if (array_key_exists("database", $config)) { + $client->select($config["database"]); + } + + } catch (RedisException $ex) { + throw new CacheException( + "Can't initialize Redis cache engine: Can't connect to Redis server", + 0, + $ex + ); + } + + return new self($client); + } + + /// + /// Cache operations + /// + + public function get (string $key, mixed $default = null) : mixed { + try { + $value = $this->redis->get($key); + } catch (RedisException $ex) { + throw new CacheException("Can't get item", 0, $ex); + } + + return match ($value) { + false => $default, + default => unserialize($value), + }; + } + + function set ( + string $key, + mixed $value, + null|int|DateInterval $ttl = null + ) : bool { + try { + if ($ttl === null) { + $this->redis->set($key, serialize($value)); + } else { + $this->redis->setex($key, self::parse_interval($ttl), $value); + } + } catch (RedisException $ex) { + throw new CacheException("Can't set item", 0, $ex); + } + + return true; + } + + public function delete (string $key) : bool { + try { + $countDeleted = $this->redis->del($key); + } catch (RedisException $e) { + throw new CacheException("Can't delete item", 0, $ex); + } + + return $countDeleted === 1; + } + + public function clear () : bool { + try { + $this->redis->flushDB(); + } catch (RedisException $e) { + throw new CacheException("Can't clear cache", 0, $ex); + } + + return true; + } + + public function has (string $key) : bool { + try { + $count = $this->redis->exists($key); + } catch (RedisException $e) { + throw new CacheException("Can't check item", 0, $ex); + } + + return $count === 1; + } + + /// + /// Overrides for multiple operations + /// + + public function deleteMultiple (iterable $keys) : bool { + $keys = [...$keys]; + $expectedCount = count($keys); + + try { + $countDeleted = $this->redis->del($keys); + } catch (RedisException $e) { + throw new CacheException("Can't delete items", 0, $ex); + } + + return $countDeleted === $expectedCount; + } + + /// + /// Helper methods + /// + + private static function parse_interval (DateInterval|int $ttl) : int { + if (is_integer($ttl)) { + return $ttl; + } + + $start = new DateTimeImmutable; + $end = $start->add($ttl); + + return $end->getTimestamp() - $start->getTimestamp(); + } + +} diff --git a/cache/src/Engines/CacheVoid.php b/cache/src/Engines/CacheVoid.php new file mode 100644 --- /dev/null +++ b/cache/src/Engines/CacheVoid.php @@ -0,0 +1,69 @@ +<?php +declare(strict_types=1); + +namespace Keruald\Cache\Engines; + +use Keruald\Cache\Cache; + +use DateInterval; + +/** + * "blackhole" void cache + * + * This class doesn't cache information, it's void wrapper + * get will always return null + * set and delete do nothing + * + * It will be used by default if no cache is specified. + */ +class CacheVoid extends Cache { + + static function load ($config) : self { + return new static; + } + + function get (string $key, mixed $default = null) : mixed { + return $default; + } + + function set ( + string $key, + mixed $value, + null|int|DateInterval $ttl = null, + ) : bool { + return true; + } + + function delete (string $key) : bool { + return true; + } + + public function clear () : bool { + return true; + } + + public function has (string $key) : bool { + return false; + } + + public function getMultiple ( + iterable $keys, + mixed $default = null + ) : iterable { + foreach ($keys as $key) { + yield $key => $default; + } + } + + public function setMultiple ( + iterable $values, + null|int|DateInterval $ttl = null, + ) : bool { + return true; + } + + public function deleteMultiple (iterable $keys) : bool { + return true; + } + +} diff --git a/cache/src/Exceptions/CacheException.php b/cache/src/Exceptions/CacheException.php new file mode 100644 --- /dev/null +++ b/cache/src/Exceptions/CacheException.php @@ -0,0 +1,11 @@ +<?php + +namespace Keruald\Cache\Exceptions; + +use Psr\SimpleCache\CacheException as CacheExceptionInterface; + +use RuntimeException; + +class CacheException extends RuntimeException implements CacheExceptionInterface { + +} diff --git a/cache/src/Features/WithPrefix.php b/cache/src/Features/WithPrefix.php new file mode 100644 --- /dev/null +++ b/cache/src/Features/WithPrefix.php @@ -0,0 +1,58 @@ +<?php +declare(strict_types=1); + +namespace Keruald\Cache\Features; + +use InvalidArgumentException; + +trait WithPrefix { + + /// + /// Properties + /// + + private string $prefix = ""; + + /// + /// Getters and setters + /// + + public function getPrefix () : string { + if ($this->prefix === "") { + throw new InvalidArgumentException("This cache doesn't use prefix"); + } + + return $this->prefix; + } + + protected function getUnsafePrefix () : string { + return $this->prefix; + } + + public function hasPrefix () : bool { + return $this->prefix !== ""; + } + + /** + * Allows to share ab instance with several applications + * by prefixing the keys. + * + * @throws InvalidArgumentException + */ + public function setPrefix (string $prefix) : self { + if ($prefix === "") { + throw new InvalidArgumentException("Prefix must be a non-empty string"); + } + + $this->prefix = $prefix; + + return $this; + } + + public function clearPrefix () : self { + $this->prefix = ""; + + return $this; + } + +} diff --git a/cache/tests/CacheDummy.php b/cache/tests/CacheDummy.php new file mode 100644 --- /dev/null +++ b/cache/tests/CacheDummy.php @@ -0,0 +1,9 @@ +<?php + +namespace Keruald\Cache\Tests; + +use Keruald\Cache\Engines\CacheVoid; + +class CacheDummy extends CacheVoid { + +} diff --git a/cache/tests/CacheFactoryTest.php b/cache/tests/CacheFactoryTest.php new file mode 100644 --- /dev/null +++ b/cache/tests/CacheFactoryTest.php @@ -0,0 +1,37 @@ +<?php + +namespace Keruald\Cache\Tests; + +use Keruald\Cache\CacheFactory; +use Keruald\Cache\Engines\CacheVoid; + +use PHPUnit\Framework\TestCase; +use Psr\SimpleCache\CacheException; + +class CacheFactoryTest extends TestCase { + + public function testLoad () { + $config = [ + "engine" => CacheDummy::class, + ]; + $cache = CacheFactory::load($config); + + $this->assertInstanceOf(CacheDummy::class, $cache); + } + + public function testLoadDefaultsToVoid () { + $cache = CacheFactory::load([]); + + $this->assertInstanceOf(CacheVoid::class, $cache); + } + + public function testLoadWithNonExistentClass () { + $config = [ + "engine" => "Acme\\Nonexistent", + ]; + + $this->expectException(CacheException::class); + CacheFactory::load($config); + } + +} diff --git a/cache/tests/Engines/CacheMemcachedTest.php b/cache/tests/Engines/CacheMemcachedTest.php new file mode 100644 --- /dev/null +++ b/cache/tests/Engines/CacheMemcachedTest.php @@ -0,0 +1,102 @@ +<?php + +namespace Keruald\Cache\Tests\Engines; + +use Keruald\Cache\Engines\CacheMemcached; +use Keruald\OmniTools\Collections\HashMap; + +use Keruald\OmniTools\Network\SocketAddress; +use PHPUnit\Framework\TestCase; + +use Memcached; + +class CacheMemcachedTest extends TestCase { + + private CacheMemcached $cache; + + protected function setUp () : void { + + if (!extension_loaded("memcached")) { + $this->markTestSkipped("Memcached extension is required to test."); + } + + if (!SocketAddress::from("127.0.0.1", 11211)->isOpen()) { + $this->markTestSkipped("Memcached server can't be reached."); + } + + $memcached = new Memcached(); + $memcached->addServer("127.0.0.1", 11211); + + $this->cache = new CacheMemcached($memcached); + } + + public function testSet () { + $result = $this->cache->set("foo", "bar"); + + $this->assertTrue($result); + } + + public function testGet () { + $this->cache->set("foo", "bar"); + + $this->assertEquals("bar", $this->cache->get("foo")); + } + + public function testHas () { + $result = $this->cache->set("foo", "bar"); + + $this->assertTrue($this->cache->has("foo")); + } + + public function testDelete () { + $this->cache->set("foo", "bar"); + $result = $this->cache->delete("foo"); + + $this->assertTrue($result); + } + + public function testClear () { + $result = $this->cache->clear(); + + $this->assertTrue($result); + } + + public function testGetMultiple () { + $expected = [ + "foo" => "bar", + "bar" => "baz", + ]; + + $this->cache->set("foo", "bar"); + $this->cache->set("bar", "baz"); + + $results = $this->cache->getMultiple(["foo", "bar"]); + $results = HashMap::from($results)->toArray(); + + $this->assertEquals($expected, $results); + } + + public function testDeleteMultiple () { + $this->cache->set("foo", "bar"); + $this->cache->set("bar", "baz"); + + $result = $this->cache->deleteMultiple(["foo", "bar"]); + + $this->assertTrue($result); + } + + public function testSetMultiple () { + $result = $this->cache->setMultiple([ + "foo" => "bar", + "bar" => "baz", + ]); + + $this->assertTrue($result); + } + + public function testLoad () { + $cache = CacheMemcached::load([]); + + $this->assertInstanceOf(CacheMemcached::class, $cache); + } +} diff --git a/cache/tests/Engines/CacheRedisTest.php b/cache/tests/Engines/CacheRedisTest.php new file mode 100644 --- /dev/null +++ b/cache/tests/Engines/CacheRedisTest.php @@ -0,0 +1,102 @@ +<?php + +namespace Keruald\Cache\Tests\Engines; + +use Keruald\Cache\Engines\CacheRedis; +use Keruald\OmniTools\Collections\HashMap; + +use Keruald\OmniTools\Network\SocketAddress; +use PHPUnit\Framework\TestCase; + +use Redis; + +class CacheRedisTest extends TestCase { + + private CacheRedis $cache; + + protected function setUp () : void { + + if (!extension_loaded("redis")) { + $this->markTestSkipped("Redis extension is required to test."); + } + + if (!SocketAddress::from("127.0.0.1", 6379)->isOpen()) { + $this->markTestSkipped("Redis server can't be reached."); + } + + $Redis = new Redis(); + $Redis->connect("127.0.0.1", 6379); + + $this->cache = new CacheRedis($Redis); + } + + public function testSet () { + $result = $this->cache->set("foo", "bar"); + + $this->assertTrue($result); + } + + public function testGet () { + $this->cache->set("foo", "bar"); + + $this->assertEquals("bar", $this->cache->get("foo")); + } + + public function testHas () { + $result = $this->cache->set("foo", "bar"); + + $this->assertTrue($this->cache->has("foo")); + } + + public function testDelete () { + $this->cache->set("foo", "bar"); + $result = $this->cache->delete("foo"); + + $this->assertTrue($result); + } + + public function testClear () { + $result = $this->cache->clear(); + + $this->assertTrue($result); + } + + public function testGetMultiple () { + $expected = [ + "foo" => "bar", + "bar" => "baz", + ]; + + $this->cache->set("foo", "bar"); + $this->cache->set("bar", "baz"); + + $results = $this->cache->getMultiple(["foo", "bar"]); + $results = HashMap::from($results)->toArray(); + + $this->assertEquals($expected, $results); + } + + public function testDeleteMultiple () { + $this->cache->set("foo", "bar"); + $this->cache->set("bar", "baz"); + + $result = $this->cache->deleteMultiple(["foo", "bar"]); + + $this->assertTrue($result); + } + + public function testSetMultiple () { + $result = $this->cache->setMultiple([ + "foo" => "bar", + "bar" => "baz", + ]); + + $this->assertTrue($result); + } + + public function testLoad () { + $cache = CacheRedis::load([]); + + $this->assertInstanceOf(CacheRedis::class, $cache); + } +} diff --git a/cache/tests/Engines/CacheVoidTest.php b/cache/tests/Engines/CacheVoidTest.php new file mode 100644 --- /dev/null +++ b/cache/tests/Engines/CacheVoidTest.php @@ -0,0 +1,78 @@ +<?php + +namespace Keruald\Cache\Tests\Engines; + +use Keruald\Cache\Engines\CacheVoid; +use Keruald\OmniTools\Collections\HashMap; + +use PHPUnit\Framework\TestCase; + +class CacheVoidTest extends TestCase { + + private CacheVoid $cache; + + protected function setUp () : void { + $this->cache = new CacheVoid; + } + + public function testSet () { + $result = $this->cache->set("foo", "bar"); + + $this->assertTrue($result); + } + + public function testDelete () { + $result = $this->cache->delete("foo"); + + $this->assertTrue($result); + } + + public function testClear () { + $result = $this->cache->clear(); + + $this->assertTrue($result); + } + + public function testGetMultiple () { + $expected = [ + "foo" => null, + "bar" => null, + ]; + + $results = $this->cache->getMultiple(["foo", "bar"]); + $results = HashMap::from($results)->toArray(); + + $this->assertEquals($expected, $results); + } + + public function testDeleteMultiple () { + $result = $this->cache->deleteMultiple(["foo", "bar"]); + + $this->assertTrue($result); + } + + public function testGet () { + $this->assertNull($this->cache->get("foo")); + } + + public function testSetMultiple () { + $result = $this->cache->setMultiple([ + "foo" => "bar", + "bar" => "baz", + ]); + + $this->assertTrue($result); + } + + public function testHas () { + $result = $this->cache->has("foo"); + + $this->assertFalse($result); + } + + public function testLoad () { + $cache = CacheVoid::load([]); + + $this->assertInstanceOf(CacheVoid::class, $cache); + } +} diff --git a/composer.json b/composer.json --- a/composer.json +++ b/composer.json @@ -17,7 +17,11 @@ "name": "Keruald contributors" } ], + "provide": { + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, "require": { + "psr/simple-cache": "^1.0|^2.0|^3.0", "ext-intl": "*" }, "require-dev": { @@ -30,7 +34,12 @@ "symfony/yaml": "^6.0.3", "squizlabs/php_codesniffer": "^3.6" }, + "suggest": { + "ext-memcached": "*", + "ext-redis": "*" + }, "replace": { + "keruald/cache": "0.1.0", "keruald/commands": "0.0.1", "keruald/database": "0.4.0", "keruald/omnitools": "0.11.0", @@ -38,6 +47,8 @@ }, "autoload": { "psr-4": { + "Keruald\\Cache\\": "cache/src/", + "Keruald\\Cache\\Tests\\": "cache/tests/", "Keruald\\Commands\\": "commands/src/", "Keruald\\Commands\\Tests\\": "commands/tests/", "Keruald\\Database\\": "database/src/", diff --git a/metadata.yml b/metadata.yml --- a/metadata.yml +++ b/metadata.yml @@ -22,12 +22,14 @@ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - packages: + - cache - commands - database - omnitools - report packages_namespaces: + cache: Keruald\Cache commands: Keruald\Commands database: Keruald\Database omnitools: Keruald\OmniTools