From b5bf0425f3f114be39b455e7b02ee3a1cd801966 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Sat, 10 Dec 2022 13:14:38 +0100 Subject: [PATCH] Implemented ObjectStorage into Core. After careful deliberation it was decided to merge ObjectStorage into Core, as it's slowly getting used by every part of FuzeWorks. --- .drone.yml | 75 +- composer.json | 3 +- src/Config/config.storage.php | 60 ++ src/FuzeWorks/Exception/StorageException.php | 40 ++ src/FuzeWorks/Factory.php | 676 +++++++++--------- src/FuzeWorks/Storage.php | 140 ++++ .../Storage/Provider/DummyProvider.php | 117 +++ .../Storage/Provider/FileProvider.php | 256 +++++++ .../Storage/Provider/RedisProvider.php | 188 +++++ src/FuzeWorks/Storage/StorageCache.php | 128 ++++ src/FuzeWorks/Storage/iStorageProvider.php | 52 ++ test/core/core_factoryTest.php | 655 ++++++++--------- test/phpunit.xml | 40 +- test/storage/StorageCacheTest.php | 200 ++++++ test/storage/StorageProviderTest.php | 218 ++++++ 15 files changed, 2145 insertions(+), 703 deletions(-) create mode 100644 src/Config/config.storage.php create mode 100644 src/FuzeWorks/Exception/StorageException.php create mode 100644 src/FuzeWorks/Storage.php create mode 100644 src/FuzeWorks/Storage/Provider/DummyProvider.php create mode 100644 src/FuzeWorks/Storage/Provider/FileProvider.php create mode 100644 src/FuzeWorks/Storage/Provider/RedisProvider.php create mode 100644 src/FuzeWorks/Storage/StorageCache.php create mode 100644 src/FuzeWorks/Storage/iStorageProvider.php create mode 100644 test/storage/StorageCacheTest.php create mode 100644 test/storage/StorageProviderTest.php diff --git a/.drone.yml b/.drone.yml index 590c88f..271809d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,24 +1,53 @@ -kind: pipeline -type: docker -name: test - -steps: - - name: composer - image: composer:latest - commands: - - composer install - - - name: php81test - image: registry.i15.nl/i15/fuzephp:8.1-alpine - commands: - - docker-php-ext-enable xdebug - - vendor/bin/phpunit -c test/phpunit.xml - - - name: coverage - image: registry.i15.nl/i15/fuzephp:8.1-alpine - commands: - - docker-php-ext-enable xdebug - - vendor/bin/phpunit -c test/phpunit.xml --coverage-text - -image_pull_secrets: +kind: pipeline +type: docker +name: test + +services: + - name: cache + image: redis + +steps: + - name: composer + image: composer:latest + commands: + - composer install + + - name: PHP81CoreTest + image: registry.i15.nl/i15/fuzephp:8.1-alpine + commands: + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --testsuite core + + - name: PHP81DummyProviderTest + image: registry.i15.nl/i15/fuzephp:8.1-alpine + commands: + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --testsuite storage + environment: + OBJECTSTORAGE_PROVIDER: DummyProvider + + - name: PHP81FileProviderTest + image: registry.i15.nl/i15/fuzephp:8.1-alpine + commands: + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --testsuite storage + environment: + OBJECTSTORAGE_PROVIDER: FileProvider + + - name: PHP81RedisProviderTest + image: registry.i15.nl/i15/fuzephp:8.1-alpine + commands: + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --testsuite storage + environment: + OBJECTSTORAGE_PROVIDER: RedisProvider + OBJECTSTORAGE_REDIS_HOST: cache + + - name: coverage + image: registry.i15.nl/i15/fuzephp:8.1-alpine + commands: + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --testsuite core --coverage-text + +image_pull_secrets: - dockerconfig \ No newline at end of file diff --git a/composer.json b/composer.json index ad22e57..75cc3d4 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ } ], "require": { - "php": ">=8.1.0" + "php": ">=8.1.0", + "psr/simple-cache": "1.0.1" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/src/Config/config.storage.php b/src/Config/config.storage.php new file mode 100644 index 0000000..c2c1fc5 --- /dev/null +++ b/src/Config/config.storage.php @@ -0,0 +1,60 @@ + Core::getEnv('STORAGE_PROVIDER', null), + 'DummyProvider' => [], + 'RedisProvider' => [ + // Type can be 'tcp' or 'unix' + 'socket_type' => Core::getEnv('STORAGE_REDIS_SOCKET_TYPE', 'tcp'), + // If socket_type == 'unix', set the socket here + 'socket' => Core::getEnv('STORAGE_REDIS_SOCKET', null), + // If socket_type == 'tcp', set the host here + 'host' => Core::getEnv('STORAGE_REDIS_HOST', '127.0.0.1'), + // And some standard settings + 'port' => Core::getEnv('STORAGE_REDIS_PORT', 6379), + 'password' => Core::getEnv('STORAGE_REDIS_PASSWORD', null), + 'timeout' => Core::getEnv('STORAGE_REDIS_TIMEOUT', 0), + 'db_index' => Core::getEnv('STORAGE_REDIS_DBINDEX', 0), + ], + 'FileProvider' => [ + // The directory where objects get stored by the FileProvider + 'storage_directory' => Core::getEnv('STORAGE_FILE_DIRECTORY', Core::$tempDir) + ] +]; \ No newline at end of file diff --git a/src/FuzeWorks/Exception/StorageException.php b/src/FuzeWorks/Exception/StorageException.php new file mode 100644 index 0000000..cee77fb --- /dev/null +++ b/src/FuzeWorks/Exception/StorageException.php @@ -0,0 +1,40 @@ +layout; - * - * The Factory class allows the user to replace dependencies on the fly. It is possible for a class - * to replace a dependency, like Logger, on the fly by calling the $factory->newInstance('Logger'); or the - * $factory->setInstance('Logger', $object); This allows for creative ways to do dependency injection, or keep classes - * separated. - * - * It is also possible to load a cloned instance of the Factory class, so that all properties are independant as well, - * all to suit your very needs. - * - * The Factory class is also extendible. This allows classes that extend Factory to access all it's properties. - * - * @author TechFuze - * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) - */ -class Factory -{ - - /** - * The Factory instance that is shared by default when calling Factory::getInstance(); - * - * @var Factory|null Default shared instance - */ - private static ?Factory $sharedFactoryInstance = null; - - /** - * Whether the Factory has been initialized or not - * - * @var bool $initialized - */ - private bool $initialized = false; - - /** - * Config Object - * @var Config - */ - public Config $config; - - /** - * Logger Object - * @var Logger - */ - public Logger $logger; - - /** - * Events Object - * @var Events - */ - public Events $events; - - /** - * Libraries Object - * @var Libraries - */ - public Libraries $libraries; - - /** - * Helpers Object - * @var Helpers - */ - public Helpers $helpers; - - /** - * Plugins Object - * @var Plugins - */ - public Plugins $plugins; - - /** - * Factory instance constructor. Should only really be called once - * @throws FactoryException - */ - public function __construct() - { - // If there is no sharedFactoryInstance, prepare it - if (is_null(self::$sharedFactoryInstance)) - { - // @codeCoverageIgnoreStart - self::$sharedFactoryInstance = $this; - $this->config = new Config(); - $this->logger = new Logger(); - $this->events = new Events(); - $this->libraries = new Libraries(); - $this->helpers = new Helpers(); - $this->plugins = new Plugins(); - - return; - } - // @codeCoverageIgnoreEnd - - // Otherwise, copy the existing instances - $x = self::getInstance(); - foreach ($x as $key => $value) - $this->{$key} = $value; - - } - - /** - * Finalizes the Factory and sends out a coreStartEvent - * - * @return Factory - * @throws CoreException - */ - public function initFactory(): Factory - { - // If already initialized, cancel - if ($this->initialized) - return $this; - - // Load the config file of the FuzeWorks core - try { - $cfg = $this->config->get('core'); - } catch (ConfigException) { - throw new CoreException("Could not initiate Factory. Config 'core' could not be found."); - } - - // Disable events if requested to do so - if (!$cfg->get('enable_events')) - Events::disable(); - - // Initialize all components - foreach ($this as $component) - { - if (!is_object($component)) - continue; - - if (method_exists($component, 'init')) - $component->init(); - } - - // Initialize all plugins - $this->plugins->loadHeadersFromPluginPaths(); - - // Log actions - Logger::logInfo("FuzeWorks initialized. Firing coreStartEvent."); - - // And fire the coreStartEvent - try { - Events::fireEvent('coreStartEvent'); - } catch (EventException $e) { - throw new CoreException("Could not initiate Factory. coreStartEvent threw exception: ".$e->getMessage()); - } - - return $this; - } - - /** - * Get an instance of a componentClass. - * - * @param string|null $instanceName - * @return mixed - * @throws FactoryException - */ - public static function getInstance(string $instanceName = null): mixed - { - if (is_null($instanceName)) - return self::$sharedFactoryInstance; - - // Determine the instance name - $instanceName = strtolower($instanceName); - - if (!isset(self::$sharedFactoryInstance->{$instanceName})) - throw new FactoryException("Could not get instance. Instance was not found."); - - return self::$sharedFactoryInstance->{$instanceName}; - } - - /** - * Create a new instance of one of the loaded classes. - * It reloads the class. It does NOT clone it. - * - * @param string $className The name of the loaded class, WITHOUT the namespace - * @param string $namespace Optional namespace. Defaults to 'FuzeWorks\' - * @return Factory Instance - * @throws FactoryException - */ - public function newInstance(string $className, string $namespace = 'FuzeWorks\\'): self - { - // Determine the class to load - $instanceName = strtolower($className); - $className = $namespace.ucfirst($className); - - if (!isset($this->{$instanceName})) - { - throw new FactoryException("Could not load new instance of '".$instanceName."'. Instance was not found.", 1); - } - elseif (!class_exists($className, false)) - { - throw new FactoryException("Could not load new instance of '".$instanceName."'. Class not found.", 1); - } - - // Remove the current instance - unset($this->{$instanceName}); - - // And set the new one - $this->{$instanceName} = new $className(); - - // Return itself - return $this; - } - - /** - * Clone an instance of one of the loaded classes. - * It clones the class. It does NOT re-create it. - * - * If the $onlyReturn = true is provided, the cloned instance will only be returned, and not set to the factory. - * - * @param string $className The name of the loaded class, WITHOUT the namespace - * @param bool $onlyReturn - * @return mixed - * @throws FactoryException - */ - public static function cloneInstance(string $className, bool $onlyReturn = false): mixed - { - // Determine the class to load - $instanceName = strtolower($className); - - if (!isset(self::$sharedFactoryInstance->{$instanceName})) - throw new FactoryException("Could not clone instance of '".$instanceName."'. Instance was not found.", 1); - - if ($onlyReturn) - return clone self::$sharedFactoryInstance->{$instanceName}; - - // Clone the instance - self::$sharedFactoryInstance->{$instanceName} = clone self::$sharedFactoryInstance->{$instanceName}; - - // Return itself - return self::$sharedFactoryInstance->{$instanceName}; - } - - /** - * Set an instance of one of the loaded classes with your own $object. - * Replace the existing class with one of your own. - * - * @param string $objectName The name of the loaded class, WITHOUT the namespace - * @param mixed $object Object to replace the class with - * @return Factory Instance - */ - public function setInstance(string $objectName, mixed $object): self - { - // Determine the instance name - $instanceName = strtolower($objectName); - - // Unset and set - unset($this->{$instanceName}); - $this->{$instanceName} = $object; - - // Return itself - return $this; - } - - /** - * Remove an instance of one of the loaded classes. - * - * @param string $className The name of the loaded class, WITHOUT the namespace - * @return Factory Factory Instance - * @throws FactoryException - */ - public function removeInstance(string $className): self - { - // Determine the instance name - $instanceName = strtolower($className); - - if (!isset($this->{$instanceName})) - { - throw new FactoryException("Could not remove instance of '".$instanceName."'. Instance was not found.", 1); - } - - // Unset - unset($this->{$instanceName}); - - // Return itself - return $this; - } - - /** - * Returns true if component is part of this Factory. - * - * @param $componentName - * @return bool - */ - public function instanceIsset($componentName): bool - { - return isset($this->{$componentName}); - } -} +layout; + * + * The Factory class allows the user to replace dependencies on the fly. It is possible for a class + * to replace a dependency, like Logger, on the fly by calling the $factory->newInstance('Logger'); or the + * $factory->setInstance('Logger', $object); This allows for creative ways to do dependency injection, or keep classes + * separated. + * + * It is also possible to load a cloned instance of the Factory class, so that all properties are independant as well, + * all to suit your very needs. + * + * The Factory class is also extendible. This allows classes that extend Factory to access all it's properties. + * + * @author TechFuze + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class Factory +{ + + /** + * The Factory instance that is shared by default when calling Factory::getInstance(); + * + * @var Factory|null Default shared instance + */ + private static ?Factory $sharedFactoryInstance = null; + + /** + * Whether the Factory has been initialized or not + * + * @var bool $initialized + */ + private bool $initialized = false; + + /** + * Config Object + * @var Config + */ + public Config $config; + + /** + * Logger Object + * @var Logger + */ + public Logger $logger; + + /** + * Events Object + * @var Events + */ + public Events $events; + + /** + * Libraries Object + * @var Libraries + */ + public Libraries $libraries; + + /** + * Helpers Object + * @var Helpers + */ + public Helpers $helpers; + + /** + * Plugins Object + * @var Plugins + */ + public Plugins $plugins; + + /** + * Storage Object + * + * @var Storage + */ + public Storage $storage; + + /** + * Factory instance constructor. Should only really be called once + * @throws FactoryException + */ + public function __construct() + { + // If there is no sharedFactoryInstance, prepare it + if (is_null(self::$sharedFactoryInstance)) + { + // @codeCoverageIgnoreStart + self::$sharedFactoryInstance = $this; + $this->config = new Config(); + $this->logger = new Logger(); + $this->events = new Events(); + $this->libraries = new Libraries(); + $this->helpers = new Helpers(); + $this->plugins = new Plugins(); + $this->storage = new Storage(); + + return; + } + // @codeCoverageIgnoreEnd + + // Otherwise, copy the existing instances + $x = self::getInstance(); + foreach ($x as $key => $value) + $this->{$key} = $value; + + } + + /** + * Finalizes the Factory and sends out a coreStartEvent + * + * @return Factory + * @throws CoreException + */ + public function initFactory(): Factory + { + // If already initialized, cancel + if ($this->initialized) + return $this; + + // Load the config file of the FuzeWorks core + try { + $cfg = $this->config->get('core'); + } catch (ConfigException) { + throw new CoreException("Could not initiate Factory. Config 'core' could not be found."); + } + + // Disable events if requested to do so + if (!$cfg->get('enable_events')) + Events::disable(); + + // Initialize all components + foreach ($this as $component) + { + if (!is_object($component)) + continue; + + if (method_exists($component, 'init')) + $component->init(); + } + + // Initialize all plugins + $this->plugins->loadHeadersFromPluginPaths(); + + // Log actions + Logger::logInfo("FuzeWorks initialized. Firing coreStartEvent."); + + // And fire the coreStartEvent + try { + Events::fireEvent('coreStartEvent'); + } catch (EventException $e) { + throw new CoreException("Could not initiate Factory. coreStartEvent threw exception: ".$e->getMessage()); + } + + return $this; + } + + /** + * Get an instance of a componentClass. + * + * @param string|null $instanceName + * @return mixed + * @throws FactoryException + */ + public static function getInstance(string $instanceName = null): mixed + { + if (is_null($instanceName)) + return self::$sharedFactoryInstance; + + // Determine the instance name + $instanceName = strtolower($instanceName); + + if (!isset(self::$sharedFactoryInstance->{$instanceName})) + throw new FactoryException("Could not get instance. Instance was not found."); + + return self::$sharedFactoryInstance->{$instanceName}; + } + + /** + * Create a new instance of one of the loaded classes. + * It reloads the class. It does NOT clone it. + * + * @param string $className The name of the loaded class, WITHOUT the namespace + * @param string $namespace Optional namespace. Defaults to 'FuzeWorks\' + * @return Factory Instance + * @throws FactoryException + */ + public function newInstance(string $className, string $namespace = 'FuzeWorks\\'): self + { + // Determine the class to load + $instanceName = strtolower($className); + $className = $namespace.ucfirst($className); + + if (!isset($this->{$instanceName})) + { + throw new FactoryException("Could not load new instance of '".$instanceName."'. Instance was not found.", 1); + } + elseif (!class_exists($className, false)) + { + throw new FactoryException("Could not load new instance of '".$instanceName."'. Class not found.", 1); + } + + // Remove the current instance + unset($this->{$instanceName}); + + // And set the new one + $this->{$instanceName} = new $className(); + + // Return itself + return $this; + } + + /** + * Clone an instance of one of the loaded classes. + * It clones the class. It does NOT re-create it. + * + * If the $onlyReturn = true is provided, the cloned instance will only be returned, and not set to the factory. + * + * @param string $className The name of the loaded class, WITHOUT the namespace + * @param bool $onlyReturn + * @return mixed + * @throws FactoryException + */ + public static function cloneInstance(string $className, bool $onlyReturn = false): mixed + { + // Determine the class to load + $instanceName = strtolower($className); + + if (!isset(self::$sharedFactoryInstance->{$instanceName})) + throw new FactoryException("Could not clone instance of '".$instanceName."'. Instance was not found.", 1); + + if ($onlyReturn) + return clone self::$sharedFactoryInstance->{$instanceName}; + + // Clone the instance + self::$sharedFactoryInstance->{$instanceName} = clone self::$sharedFactoryInstance->{$instanceName}; + + // Return itself + return self::$sharedFactoryInstance->{$instanceName}; + } + + /** + * Set an instance of one of the loaded classes with your own $object. + * Replace the existing class with one of your own. + * + * @param string $objectName The name of the loaded class, WITHOUT the namespace + * @param mixed $object Object to replace the class with + * @return Factory Instance + */ + public function setInstance(string $objectName, mixed $object): self + { + // Determine the instance name + $instanceName = strtolower($objectName); + + // Unset and set + unset($this->{$instanceName}); + $this->{$instanceName} = $object; + + // Return itself + return $this; + } + + /** + * Remove an instance of one of the loaded classes. + * + * @param string $className The name of the loaded class, WITHOUT the namespace + * @return Factory Factory Instance + * @throws FactoryException + */ + public function removeInstance(string $className): self + { + // Determine the instance name + $instanceName = strtolower($className); + + if (!isset($this->{$instanceName})) + { + throw new FactoryException("Could not remove instance of '".$instanceName."'. Instance was not found.", 1); + } + + // Unset + unset($this->{$instanceName}); + + // Return itself + return $this; + } + + /** + * Returns true if component is part of this Factory. + * + * @param $componentName + * @return bool + */ + public function instanceIsset($componentName): bool + { + return isset($this->{$componentName}); + } +} diff --git a/src/FuzeWorks/Storage.php b/src/FuzeWorks/Storage.php new file mode 100644 index 0000000..bd8d9ac --- /dev/null +++ b/src/FuzeWorks/Storage.php @@ -0,0 +1,140 @@ +provider)) + return $this->provider; + + // Load the config, if it isn't loaded yet + if (!isset($this->cfg)) + { + try { + /** @var Config $configs */ + $configs = Factory::getInstance('config'); + $this->cfg = $configs->getConfig('storage')->toArray(); + } catch (ConfigException | FactoryException $e) { + throw new StorageException("Could not get StorageProvider. No config file named 'config.storage.php' could be found."); + } + } + + // Get the currently selected StorageProvider + $selected = $this->cfg['StorageProvider']; + if (is_null($selected)) + throw new StorageException("Could not get StorageProvider. Selected provider is null!"); + + // Try and load the StorageProvider + $class = '\FuzeWorks\Storage\Provider\\' . $selected; + if (!class_exists($class, true)) + throw new StorageException("Could not get StorageProvider. Selected provider '".$selected."' is not recognized."); + + /** @var iStorageProvider $provider */ + $provider = new $class(); + if (!$provider instanceof iStorageProvider) + throw new StorageException("Could not get StorageProvider. Selected provider '".$selected."' is not an instance of iStorageProvider'."); + + // Fetch the parameters + $params = isset($this->cfg[$selected]) && is_array($this->cfg[$selected]) ? $this->cfg[$selected] : []; + if (!$provider->init($params)) + throw new StorageException("Could not get StorageProvider. Selected provider '".$selected."' failed to load."); + + $this->provider = $provider; + return $this->provider; + } + + /** + * Returns a PSR compatible Cache object + * + * @return CacheInterface + * @throws StorageException + */ + public function getCache(): CacheInterface + { + if (isset($this->cache)) + return $this->cache; + + $storageProvider = $this->getStorage(); + $this->cache = new StorageCache($storageProvider); + + return $this->cache; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Storage/Provider/DummyProvider.php b/src/FuzeWorks/Storage/Provider/DummyProvider.php new file mode 100644 index 0000000..5002017 --- /dev/null +++ b/src/FuzeWorks/Storage/Provider/DummyProvider.php @@ -0,0 +1,117 @@ + [], 'data' => []]; + + public function init(array $providerConfig): bool + { + return true; + } + + public function getIndex(): array + { + return $this->data['index']; + } + + public function getItem(string $key) + { + if (!in_array($key, $this->data['index'])) + return null; + + return $this->data['data'][$key]['data']; + } + + public function getItemMeta(string $key): ?array + { + if (!in_array($key, $this->data['index'])) + return null; + + return $this->data['data'][$key]['meta']; + } + + public function getItems(array $keys = []): array + { + $output = []; + foreach ($keys as $key) + $output[$key] = $this->getItem($key); + + return $output; + } + + public function hasItem(string $key): bool + { + return in_array($key, $this->data['index']); + } + + public function clear(): bool + { + return $this->deleteItems($this->getIndex()); + } + + public function deleteItem(string $key): bool + { + // Remove the index + if (($k = array_search($key, $this->data['index'])) !== false) { + unset($this->data['index'][$k]); + } + + // And remove the data + unset($this->data['data'][$key]); + return true; + } + + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) + $this->deleteItem($key); + + return true; + } + + public function save(string $key, $value, array $metaData = []): bool + { + if (!in_array($key, $this->data['index'])) + $this->data['index'][] = $key; + + $this->data['data'][$key] = ['data' => $value, 'meta' => $metaData]; + return true; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Storage/Provider/FileProvider.php b/src/FuzeWorks/Storage/Provider/FileProvider.php new file mode 100644 index 0000000..863d066 --- /dev/null +++ b/src/FuzeWorks/Storage/Provider/FileProvider.php @@ -0,0 +1,256 @@ +directory = rtrim($directory); + $this->indexFile = $this->directory . DS . 'index.fwstorage'; + + // If the index does not exist yet, load it + if (!file_exists($this->indexFile)) + { + if (!$this->write_file($this->indexFile, serialize(['index' => []]))) + throw new StorageException("Could not load FileProvider Storage. Could not write index."); + + chmod($this->indexFile, 0640); + } + + // And finally, load the index + $this->index = unserialize(file_get_contents($this->indexFile))['index']; + + return true; + } + + public function getIndex(): array + { + $out = []; + foreach ($this->index as $key => $val) + $out[] = $key; + + return $out; + } + + public function getItem(string $key) + { + // Convert the key + $file = $this->directory . DS . crc32($key) . '.fwstorage'; + + // If the key could not be found in the index, return null + if (!isset($this->index[$key])) + return null; + + // Check if the file exists. If not, delete the indexed value + if (!file_exists($file)) + { + $this->deleteItem($key); + return null; + } + + // Otherwise try and load the metaData and contents + return unserialize(file_get_contents($file)); + } + + public function getItemMeta(string $key): ?array + { + // If the key could not be found in the index, return null + if (!isset($this->index[$key])) + return null; + + // Otherwise return the meta data + return $this->index[$key]['meta']; + } + + public function getItems(array $keys = []): array + { + $output = []; + foreach ($keys as $key) + $output[$key] = $this->getItem($key); + + return $output; + } + + public function hasItem(string $key): bool + { + return isset($this->index[$key]); + } + + public function clear(): bool + { + $keys = array_keys($this->index); + return $this->deleteItems($keys); + } + + public function deleteItem(string $key): bool + { + // Convert the key + $file = $this->directory . DS . crc32($key) . '.fwstorage'; + + // Remove the file first + if (file_exists($file)) + unlink($file); + + // And remove it from the index + if (isset($this->index[$key])) + unset($this->index[$key]); + + // And commit the index + return $this->commitIndex(); + } + + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) + $this->deleteItem($key); + + return true; + } + + public function save(string $key, $value, array $metaData = []): bool + { + // Convert the key + $file = $this->directory . DS . crc32($key) . '.fwstorage'; + + // Remove the file if it already exists + if (file_exists($file)) + unlink($file); + + // Write everything to the index + $this->index[$key] = ['meta' => $metaData]; + $this->commitIndex(); + + // And write the contents + if ($this->write_file($file, serialize($value))) { + chmod($file, 0640); + return true; + } + + return false; + } + + private function commitIndex(): bool + { + if ($this->write_file($this->indexFile, serialize(['index' => $this->index]))) { + chmod($this->indexFile, 0640); + return true; + } + + return false; + } + + /** + * Write File + * + * Writes data to the file specified in the path. + * Creates a new file if non-existent. + * + * @param string $path File path + * @param string $data Data to write + * @return bool + */ + private function write_file(string $path, string $data): bool + { + if ( ! $fp = @fopen($path, 'wb')) + return false; + + flock($fp, LOCK_EX); + + for ($result = $written = 0, $length = strlen($data); $written < $length; $written += $result) + if (($result = fwrite($fp, substr($data, $written))) === false) + break; + + flock($fp, LOCK_UN); + fclose($fp); + + return is_int($result); + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Storage/Provider/RedisProvider.php b/src/FuzeWorks/Storage/Provider/RedisProvider.php new file mode 100644 index 0000000..7e8fb5b --- /dev/null +++ b/src/FuzeWorks/Storage/Provider/RedisProvider.php @@ -0,0 +1,188 @@ +conn = new Redis(); + + // Afterwards we attempt to connect to the server + $socketType = $providerConfig['socket_type']; + if ($socketType === 'unix') + $success = $this->conn->connect($providerConfig['socket']); + elseif ($socketType === 'tcp') + $success = $this->conn->connect($providerConfig['host'], $providerConfig['port'], $providerConfig['timeout']); + else + $success = false; + + // If failed, throw an exception informing so + if (!$success) + throw new StorageException("Could not load RedisProvider Storage. Unable to connect to server."); + + // If authentication is required, attempt to do so with the provided password + if (isset($providerConfig['password']) && !$this->conn->auth($providerConfig['password'])) + throw new StorageException("Could not load RedisProvider Storage. Authentication failure."); + + // If a db_index is provided, use that one accordingly + if (isset($providerConfig['db_index']) && is_int($providerConfig['db_index'])) + $this->conn->select($providerConfig['db_index']); + + // And if all goes well, report a true + return true; + + // If any sort of failure has occurred along the way, + } catch (RedisException $e) { + throw new StorageException("Could not load RedisProvider Storage. RedisException thrown: '" . $e->getMessage() . "'"); + } + } + + public function getIndex(): array + { + return $this->conn->sMembers('StorageIndex'); + } + + public function getItem(string $key) + { + // If the requested key is not part of the index, this item is not tracked and should therefore + // return null. + if (!$this->conn->sIsMember('StorageIndex', $key)) + return null; + + // If the data doesn't exist, return null + if (!$this->conn->hExists('fwstorage_' . $key, 'data')) + return null; + + return unserialize($this->conn->hGet('fwstorage_' . $key, 'data')); + } + + public function getItemMeta(string $key): ?array + { + // If the requested key is not part of the index, this item is not tracked and should therefore + // return null. + if (!$this->conn->sIsMember('StorageIndex', $key)) + return null; + + // If the data doesn't exist, return null + if (!$this->conn->hExists('fwstorage_' . $key, 'meta')) + return null; + + return unserialize($this->conn->hGet('fwstorage_' . $key, 'meta')); + } + + public function getItems(array $keys = []): array + { + $output = []; + foreach ($keys as $key) + $output[$key] = $this->getItem($key); + + return $output; + } + + public function hasItem(string $key): bool + { + return $this->conn->sIsMember('StorageIndex', $key); + } + + public function clear(): bool + { + return $this->deleteItems($this->getIndex()); + } + + public function deleteItem(string $key): bool + { + // If the requested key is not part of the index, this item is not tracked and should therefore + // return null. + if ($this->conn->sIsMember('StorageIndex', $key)) + $this->conn->sRem('StorageIndex', $key); + + if ($this->conn->exists('fwstorage_' . $key)) + $this->conn->del('fwstorage_' . $key); + + return true; + } + + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) + $this->deleteItem($key); + + return true; + } + + public function save(string $key, $value, array $metaData = []): bool + { + // If the requested key is not part of the index, this item is not tracked and should therefore + // return null. + if (!$this->conn->sIsMember('StorageIndex', $key)) + $this->conn->sAdd('StorageIndex', $key); + + // Write to the hash + $this->conn->hSet('fwstorage_' . $key, 'data', serialize($value)); + $this->conn->hSet('fwstorage_' . $key, 'meta', serialize($metaData)); + + return true; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Storage/StorageCache.php b/src/FuzeWorks/Storage/StorageCache.php new file mode 100644 index 0000000..9054de3 --- /dev/null +++ b/src/FuzeWorks/Storage/StorageCache.php @@ -0,0 +1,128 @@ +provider = $provider; + } + + private function testTTL(string $key) + { + $meta = $this->provider->getItemMeta('fwcache_' . $key); + if (!is_null($meta) && $meta['ttl'] > 0 && time() > $meta['time'] + $meta['ttl']) + $this->provider->deleteItem('fwcache_' . $key); + } + + public function get($key, $default = null) + { + // Remove the item if its TTL has expired + $this->testTTL($key); + + // Fetch the value + $res = $this->provider->getItem('fwcache_' . $key); + + // If there is no value, return the default + return is_null($res) ? $default : $res; + } + + public function set($key, $value, $ttl = null): bool + { + $meta = [ + 'time' => time(), + 'ttl' => is_int($ttl) ? $ttl : 0 + ]; + + return $this->provider->save('fwcache_' . $key, $value, $meta); + } + + public function delete($key): bool + { + return $this->provider->deleteItem('fwcache_' . $key); + } + + public function clear(): bool + { + // Fetch the index set + $index = $this->provider->getIndex(); + foreach ($index as $entry) + { + if (substr($entry, 0, 8) === 'fwcache_') + $this->provider->deleteItem($entry); + } + + return true; + } + + public function getMultiple($keys, $default = null): array + { + $out = []; + foreach ($keys as $key) + { + $out[$key] = $this->get($key, $default); + } + + return $out; + } + + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) + $this->set($key, $value, $ttl); + + return true; + } + + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) + $this->delete($key); + + return true; + } + + public function has($key): bool + { + $this->testTTL($key); + return $this->provider->hasItem('fwcache_' . $key); + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Storage/iStorageProvider.php b/src/FuzeWorks/Storage/iStorageProvider.php new file mode 100644 index 0000000..2f2b78a --- /dev/null +++ b/src/FuzeWorks/Storage/iStorageProvider.php @@ -0,0 +1,52 @@ +assertInstanceOf('FuzeWorks\Factory', Factory::getInstance()); - } - - /** - * @covers ::getInstance - */ - public function testGetInstance() - { - // Add the mock - $mock = $this->getMockBuilder(MockFactory::class)->getMock(); - Factory::getInstance()->setInstance('Mock', $mock); - - // First test a global getInstance Factory - $this->assertInstanceOf('\FuzeWorks\Factory', Factory::getInstance()); - - // Second, test retrieving a component - $this->assertInstanceOf(get_class($mock), Factory::getInstance('Mock')); - } - - /** - * @depends testGetInstance - * @covers ::getInstance - */ - public function testGetInstanceNotFound() - { - $this->expectException(FactoryException::class); - Factory::getInstance('NotFound'); - } - - /** - * @depends testCanLoadFactory - * @covers ::getInstance - */ - public function testLoadSameInstance() - { - $this->assertSame(Factory::getInstance(), Factory::getInstance()); - } - - /** - * @depends testCanLoadFactory - * @covers ::getInstance - * @covers ::cloneInstance - */ - public function testLoadDifferentInstance() - { - // Add the mock - $mock = $this->getMockBuilder(MockFactory::class)->getMock(); - Factory::getInstance()->setInstance('Mock', $mock); - - // First a situation where one is the shared instance and one is a cloned instance - $a = Factory::getInstance('Mock'); - $b = Factory::cloneInstance('Mock'); - $this->assertInstanceOf(get_class($mock), $a); - $this->assertInstanceOf(get_class($mock), $b); - $this->assertNotSame($a,$b); - - // And a situation where both are cloned instances - $a = Factory::cloneInstance('Mock'); - $b = Factory::cloneInstance('Mock'); - $this->assertInstanceOf(get_class($mock), $a); - $this->assertInstanceOf(get_class($mock), $b); - $this->assertNotSame($a,$b); - } - - /** - * @depends testCanLoadFactory - * @covers ::getInstance - * @covers ::setInstance - */ - public function testObjectsSameInstance() - { - // Create mock - $mock = $this->getMockBuilder(MockFactory::class)->setMethods(['mockListener'])->getMock(); - - // Test not set - $this->assertFalse(isset(Factory::getInstance()->mock)); - - // Same instance factories - /** @var Factory $factory1 */ - /** @var Factory $factory2 */ - $factory1 = Factory::getInstance()->setInstance('Mock', $mock); - $factory2 = Factory::getInstance()->setInstance('Mock', $mock); - - // Return the mocks - $this->assertSame($factory1->mock, $factory2->mock); - - // Different instance factories - $factory3 = Factory::getInstance()->setInstance('Mock', $mock); - $factory4 = Factory::getInstance()->setInstance('Mock', $mock); - - // Return the mocks - $this->assertSame($factory3->mock, $factory4->mock); - } - - /** - * @depends testObjectsSameInstance - * @covers ::getInstance - * @covers ::setInstance - * @covers ::cloneInstance - */ - public function testObjectsDifferentInstance() - { - // Create mock - $mock = $this->getMockBuilder(MockFactory::class)->getMock(); - - // Same instance factories - $factory1 = Factory::getInstance()->setInstance('Mock', $mock); - $factory2 = Factory::getInstance()->setInstance('Mock', $mock); - - // Clone the instance in factory2 - $factory2mock = $factory2->cloneInstance('Mock'); - - // Should be true, since both Factories use the same Mock instance - $this->assertSame($factory1->mock, $factory2mock); - - // Different instance factories - $factory3 = Factory::getInstance()->setInstance('Mock', $mock); - $factory4 = Factory::getInstance()->setInstance('Mock', $mock); - - // Should be same for now - $this->assertSame($factory3->mock, $factory4->mock); - - // Clone the instance in factory4 - $factory4mock = $factory4->cloneInstance('Mock', true); - - // Should be false, since both Factories use a different Mock instance - $this->assertNotSame($factory3->mock, $factory4mock); - } - - /** - * @depends testCanLoadFactory - * @covers ::cloneInstance - */ - public function testCloneInstanceWrongClassname() - { - // Get factory - $factory = new Factory; - - // Attempt - $this->expectException(FactoryException::class); - $factory->cloneInstance('fake'); - } - - /** - * @depends testCanLoadFactory - * @covers ::getInstance - * @covers ::newInstance - */ - public function testNewFactoryInstance() - { - // Load the different factories - $factory = new Factory(); - $factory2 = Factory::getInstance(); - - // Test if the objects are different factory instances - $this->assertNotSame($factory, $factory2); - - // And test if all ClassInstances are the same - $this->assertSame($factory->config, $factory2->config); - $this->assertSame($factory->logger, $factory2->logger); - $this->assertSame($factory->events, $factory2->events); - $this->assertSame($factory->libraries, $factory2->libraries); - $this->assertSame($factory->helpers, $factory2->helpers); - - // And test when changing one classInstance - $factory->newInstance('Helpers'); - $this->assertNotSame($factory->helpers, $factory2->helpers); - } - - /** - * @depends testNewFactoryInstance - * @covers ::newInstance - */ - public function testFactoryNewInstanceNotExist() - { - // Load the factory - $factory = new Factory; - - // First, it does not exist - $this->expectException(FactoryException::class); - $factory->newInstance('fake'); - } - - /** - * @depends testNewFactoryInstance - * @covers ::newInstance - */ - public function testFactoryNewInstanceWrongNamespace() - { - // Load the factory - $factory = new Factory; - - // Second, it just fails - $this->expectException(FactoryException::class); - $factory->newInstance('helpers', 'Test\\'); - } - - /** - * @depends testNewFactoryInstance - * @covers ::setInstance - * @covers ::removeInstance - */ - public function testRemoveInstance() - { - // Load the factory - $factory = new Factory; - - // Create the object - $object = new MockObject; - - // Add it to the factory - $factory->setInstance('test', $object); - - // Test if it is there - $this->assertObjectHasAttribute('test', $factory); - $this->assertSame($object, $factory->test); - - // Now remove it - $factory->removeInstance('test'); - - // Assert that it's gone - $this->assertObjectNotHasAttribute('test', $factory); - } - - /** - * @depends testRemoveInstance - * @covers ::removeInstance - */ - public function testRemoveInstanceNotExist() - { - // Load the factory - $factory = new Factory; - - // Test - $this->expectException(FactoryException::class); - $factory->removeInstance('fake'); - } - - /** - * @depends testCanLoadFactory - * @covers ::instanceIsset - * @covers ::setInstance - */ - public function testInstanceIsset() - { - // Load the factory - $factory = new Factory; - - // Test if not set and add instance - $this->assertFalse($factory->instanceIsset('test')); - $factory->setInstance('test', 5); - - // Test if isset and value - $this->assertTrue($factory->instanceIsset('test')); - $this->assertEquals(5, $factory->test); - } - - public function tearDown(): void - { - parent::tearDown(); - - $factory = Factory::getInstance(); - if (isset($factory->mock)) - $factory->removeInstance('mock'); - } - -} - -class MockFactory { - -} - -class MockObject { - +assertInstanceOf('FuzeWorks\Factory', Factory::getInstance()); + } + + /** + * @covers ::getInstance + */ + public function testGetInstance() + { + // Add the mock + $mock = $this->getMockBuilder(MockFactory::class)->getMock(); + Factory::getInstance()->setInstance('Mock', $mock); + + // First test a global getInstance Factory + $this->assertInstanceOf('\FuzeWorks\Factory', Factory::getInstance()); + + // Second, test retrieving a component + $this->assertInstanceOf(get_class($mock), Factory::getInstance('Mock')); + } + + /** + * @depends testGetInstance + * @covers ::getInstance + */ + public function testGetInstanceNotFound() + { + $this->expectException(FactoryException::class); + Factory::getInstance('NotFound'); + } + + /** + * @depends testCanLoadFactory + * @covers ::getInstance + */ + public function testLoadSameInstance() + { + $this->assertSame(Factory::getInstance(), Factory::getInstance()); + } + + /** + * @depends testCanLoadFactory + * @covers ::getInstance + * @covers ::cloneInstance + */ + public function testLoadDifferentInstance() + { + // Add the mock + $mock = $this->getMockBuilder(MockFactory::class)->getMock(); + Factory::getInstance()->setInstance('Mock', $mock); + + // First a situation where one is the shared instance and one is a cloned instance + $a = Factory::getInstance('Mock'); + $b = Factory::cloneInstance('Mock'); + $this->assertInstanceOf(get_class($mock), $a); + $this->assertInstanceOf(get_class($mock), $b); + $this->assertNotSame($a,$b); + + // And a situation where both are cloned instances + $a = Factory::cloneInstance('Mock'); + $b = Factory::cloneInstance('Mock'); + $this->assertInstanceOf(get_class($mock), $a); + $this->assertInstanceOf(get_class($mock), $b); + $this->assertNotSame($a,$b); + } + + /** + * @depends testCanLoadFactory + * @covers ::getInstance + * @covers ::setInstance + */ + public function testObjectsSameInstance() + { + // Create mock + $mock = $this->getMockBuilder(MockFactory::class)->setMethods(['mockListener'])->getMock(); + + // Test not set + $this->assertFalse(isset(Factory::getInstance()->mock)); + + // Same instance factories + /** @var Factory $factory1 */ + /** @var Factory $factory2 */ + $factory1 = Factory::getInstance()->setInstance('Mock', $mock); + $factory2 = Factory::getInstance()->setInstance('Mock', $mock); + + // Return the mocks + $this->assertSame($factory1->mock, $factory2->mock); + + // Different instance factories + $factory3 = Factory::getInstance()->setInstance('Mock', $mock); + $factory4 = Factory::getInstance()->setInstance('Mock', $mock); + + // Return the mocks + $this->assertSame($factory3->mock, $factory4->mock); + } + + /** + * @depends testObjectsSameInstance + * @covers ::getInstance + * @covers ::setInstance + * @covers ::cloneInstance + */ + public function testObjectsDifferentInstance() + { + // Create mock + $mock = $this->getMockBuilder(MockFactory::class)->getMock(); + + // Same instance factories + $factory1 = Factory::getInstance()->setInstance('Mock', $mock); + $factory2 = Factory::getInstance()->setInstance('Mock', $mock); + + // Clone the instance in factory2 + $factory2mock = $factory2->cloneInstance('Mock'); + + // Should be true, since both Factories use the same Mock instance + $this->assertSame($factory1->mock, $factory2mock); + + // Different instance factories + $factory3 = Factory::getInstance()->setInstance('Mock', $mock); + $factory4 = Factory::getInstance()->setInstance('Mock', $mock); + + // Should be same for now + $this->assertSame($factory3->mock, $factory4->mock); + + // Clone the instance in factory4 + $factory4mock = $factory4->cloneInstance('Mock', true); + + // Should be false, since both Factories use a different Mock instance + $this->assertNotSame($factory3->mock, $factory4mock); + } + + /** + * @depends testCanLoadFactory + * @covers ::cloneInstance + */ + public function testCloneInstanceWrongClassname() + { + // Get factory + $factory = new Factory; + + // Attempt + $this->expectException(FactoryException::class); + $factory->cloneInstance('fake'); + } + + /** + * @depends testCanLoadFactory + * @covers ::getInstance + * @covers ::newInstance + */ + public function testNewFactoryInstance() + { + // Load the different factories + $factory = new Factory(); + $factory2 = Factory::getInstance(); + + // Test if the objects are different factory instances + $this->assertNotSame($factory, $factory2); + + // And test if all ClassInstances are the same + $this->assertSame($factory->config, $factory2->config); + $this->assertSame($factory->logger, $factory2->logger); + $this->assertSame($factory->events, $factory2->events); + $this->assertSame($factory->libraries, $factory2->libraries); + $this->assertSame($factory->helpers, $factory2->helpers); + $this->assertSame($factory->storage, $factory2->storage); + + // And test when changing one classInstance + $factory->newInstance('Helpers'); + $this->assertNotSame($factory->helpers, $factory2->helpers); + } + + /** + * @depends testNewFactoryInstance + * @covers ::newInstance + */ + public function testFactoryNewInstanceNotExist() + { + // Load the factory + $factory = new Factory; + + // First, it does not exist + $this->expectException(FactoryException::class); + $factory->newInstance('fake'); + } + + /** + * @depends testNewFactoryInstance + * @covers ::newInstance + */ + public function testFactoryNewInstanceWrongNamespace() + { + // Load the factory + $factory = new Factory; + + // Second, it just fails + $this->expectException(FactoryException::class); + $factory->newInstance('helpers', 'Test\\'); + } + + /** + * @depends testNewFactoryInstance + * @covers ::setInstance + * @covers ::removeInstance + */ + public function testRemoveInstance() + { + // Load the factory + $factory = new Factory; + + // Create the object + $object = new MockObject; + + // Add it to the factory + $factory->setInstance('test', $object); + + // Test if it is there + $this->assertObjectHasAttribute('test', $factory); + $this->assertSame($object, $factory->test); + + // Now remove it + $factory->removeInstance('test'); + + // Assert that it's gone + $this->assertObjectNotHasAttribute('test', $factory); + } + + /** + * @depends testRemoveInstance + * @covers ::removeInstance + */ + public function testRemoveInstanceNotExist() + { + // Load the factory + $factory = new Factory; + + // Test + $this->expectException(FactoryException::class); + $factory->removeInstance('fake'); + } + + /** + * @depends testCanLoadFactory + * @covers ::instanceIsset + * @covers ::setInstance + */ + public function testInstanceIsset() + { + // Load the factory + $factory = new Factory; + + // Test if not set and add instance + $this->assertFalse($factory->instanceIsset('test')); + $factory->setInstance('test', 5); + + // Test if isset and value + $this->assertTrue($factory->instanceIsset('test')); + $this->assertEquals(5, $factory->test); + } + + public function tearDown(): void + { + parent::tearDown(); + + $factory = Factory::getInstance(); + if (isset($factory->mock)) + $factory->removeInstance('mock'); + } + +} + +class MockFactory { + +} + +class MockObject { + } \ No newline at end of file diff --git a/test/phpunit.xml b/test/phpunit.xml index d37cfe4..d293b9a 100644 --- a/test/phpunit.xml +++ b/test/phpunit.xml @@ -1,19 +1,23 @@ - - - - - ../ - - - ../vendor/ - ../test/ - ../src/Layout/ - ../src/Config/ - - - - - ./ - - + + + + + ../ + + + ../vendor/ + ../test/ + ../src/Layout/ + ../src/Config/ + + + + + ./core + ./events + + + ./storage + + \ No newline at end of file diff --git a/test/storage/StorageCacheTest.php b/test/storage/StorageCacheTest.php new file mode 100644 index 0000000..fe23269 --- /dev/null +++ b/test/storage/StorageCacheTest.php @@ -0,0 +1,200 @@ +loadProvider(); + } + + public function tearDown(): void + { + parent::tearDown(); + + // Always clear the cache after every test + $this->cache->clear(); + } + + private function loadProvider() + { + /** @var Storage $objectStorageComponent */ + $objectStorageComponent = Factory::getInstance('storage'); + $this->cache = $objectStorageComponent->getCache(); + } + + public function testFoundation() + { + $this->assertInstanceOf('\Psr\SimpleCache\CacheInterface', $this->cache); + $this->assertInstanceOf('\FuzeWorks\Storage\StorageCache', $this->cache); + } + + /** + * @depends testFoundation + */ + public function testSetGetAndHas() + { + $testData = ['hello', 'world']; + + // First check the data isn't there + $this->assertFalse($this->cache->has('testData')); + + // Then write it + $this->assertTrue($this->cache->set('testData', $testData)); + + // Assert it is there and check its contents + $this->assertTrue($this->cache->has('testData')); + $this->assertEquals(['hello', 'world'], $this->cache->get('testData')); + } + + /** + * @depends testSetGetAndHas + */ + public function testGetDefaultValue() + { + // Verify that no value exists + $this->assertFalse($this->cache->has('testData')); + $this->assertNull($this->cache->get('testData')); + + // And check if the default value is returned + $this->assertEquals('default!', $this->cache->get('testData', 'default!')); + } + + /** + * @depends testSetGetAndHas + */ + public function testDeleteValue() + { + // Verify that none exist + $this->assertFalse($this->cache->has('testData')); + + // Write some data + $this->assertTrue($this->cache->set('testData', 'someValue')); + $this->assertEquals('someValue', $this->cache->get('testData')); + $this->assertTrue($this->cache->has('testData')); + + // Delete it + $this->assertTrue($this->cache->delete('testData')); + $this->assertFalse($this->cache->has('testData')); + } + + /** + * @depends testDeleteValue + */ + public function testClear() + { + // Write some data + $this->assertTrue($this->cache->set('testData1', 'value1')); + $this->assertTrue($this->cache->set('testData2', 'value2')); + + // Then clear it off + $this->assertTrue($this->cache->clear()); + $this->assertFalse($this->cache->has('testData1')); + $this->assertFalse($this->cache->has('testData2')); + } + + /** + * @depends testDeleteValue + */ + public function testMultiple() + { + // First check that none of the variables exist + $this->assertFalse($this->cache->has('testData1')); + $this->assertFalse($this->cache->has('testData2')); + $this->assertFalse($this->cache->has('testData3')); + + // With a get multiple, and default + $this->assertEquals([ + 'testData1' => 'default', + 'testData2' => 'default', + 'testData3' => 'default' + ], $this->cache->getMultiple(['testData1', 'testData2', 'testData3'], 'default')); + + // Write multiple + $this->assertTrue($this->cache->setMultiple([ + 'testData1' => 'value1', + 'testData2' => 'value2', + 'testData3' => 'value3' + ])); + + // Test the contents + $this->assertEquals([ + 'testData1' => 'value1', + 'testData2' => 'value2', + 'testData3' => 'value3' + ], $this->cache->getMultiple(['testData1', 'testData2', 'testData3'], 'default')); + + // And also delete them all + $this->assertTrue($this->cache->deleteMultiple(['testData1', 'testData2', 'testData3'])); + $this->assertFalse($this->cache->has('testData1')); + $this->assertFalse($this->cache->has('testData2')); + $this->assertFalse($this->cache->has('testData3')); + } + + /** + * @depends testSetGetAndHas + */ + public function testTTL() + { + $testData = ['hello', 'world']; + + // First check the data isn't there + $this->assertFalse($this->cache->has('testData')); + + // Then write it + $this->assertTrue($this->cache->set('testData', $testData, 1)); + + // Assert it is there and check its contents + $this->assertTrue($this->cache->has('testData')); + $this->assertEquals(['hello', 'world'], $this->cache->get('testData')); + + // Then wait 2 secs + sleep(2); + + // And check again + $this->assertFalse($this->cache->has('testData')); + } +} \ No newline at end of file diff --git a/test/storage/StorageProviderTest.php b/test/storage/StorageProviderTest.php new file mode 100644 index 0000000..67355bd --- /dev/null +++ b/test/storage/StorageProviderTest.php @@ -0,0 +1,218 @@ +loadProvider(); + } + + public function tearDown(): void + { + parent::tearDown(); + + // Always clear the provider at the end + $this->provider->clear(); + } + + private function loadProvider() + { + /** @var Storage $objectStorageComponent */ + $objectStorageComponent = Factory::getInstance('storage'); + $this->provider = $objectStorageComponent->getStorage(); + } + + public function testFoundation() + { + $this->assertInstanceOf('\FuzeWorks\Storage\iStorageProvider', $this->provider); + } + + /** + * @depends testFoundation + */ + public function testSave() + { + $testData = ['hello', 'world', 'foo' => 'bar']; + + // First assert it does not exist yet + $this->assertNull($this->provider->getItem('testData')); + $this->assertFalse($this->provider->hasItem('testData')); + + // Write the data + $this->assertTrue($this->provider->save('testData', $testData)); + + // Read the data + $this->assertTrue($this->provider->hasItem('testData')); + $this->assertEquals(['hello', 'world', 'foo' => 'bar'], $this->provider->getItem('testData')); + } + + /** + * @depends testSave + */ + public function testDeleteItem() + { + $testData = ['o', 'm', 'g']; + + // First assert that the data does not exist yet + $this->assertNull($this->provider->getItem('testDeleteData')); + + // Write the data + $this->assertTrue($this->provider->save('testDeleteData', $testData)); + + // Read the data + $this->assertEquals(['o', 'm', 'g'], $this->provider->getItem('testDeleteData')); + + // Delete the data + $this->assertTrue($this->provider->deleteItem('testDeleteData')); + + // And test that it is truly gone + $this->assertNull($this->provider->getItem('testDeleteData')); + } + + /** + * @depends testDeleteItem + */ + public function testDeleteItems() + { + $testData = 'lord'; + $testData2 = 'almighty'; + + // First assert that the data does not exist yet + $this->assertNull($this->provider->getItem('testDeleteData1')); + $this->assertNull($this->provider->getItem('testDeleteData2')); + + // Write the data + $this->assertTrue($this->provider->save('testDeleteData1', $testData)); + $this->assertTrue($this->provider->save('testDeleteData2', $testData2)); + + // Read the data + $this->assertEquals( + ['testDeleteData1' => 'lord', 'testDeleteData2' => 'almighty'], + $this->provider->getItems(['testDeleteData1', 'testDeleteData2']) + ); + + // Delete the data + $this->assertTrue($this->provider->deleteItems(['testDeleteData1', 'testDeleteData2'])); + + // And test that it is truly gone + $this->assertNull($this->provider->getItem('testDeleteData1')); + $this->assertNull($this->provider->getItem('testDeleteData2')); + } + + /** + * @depends testDeleteItems + */ + public function testClear() + { + $testData = ['not', 'my', 'department']; + + // First assert it does not exist yet + $this->assertNull($this->provider->getItem('testClearData')); + + $this->provider->save('testClearData', $testData); + + // Test that it can be read + $this->assertEquals(['not', 'my', 'department'], $this->provider->getItem('testClearData')); + + // Then attempt to clean it + $this->assertTrue($this->provider->clear()); + + // And check that it cannot be read + $this->assertNull($this->provider->getItem('testClearData')); + } + + public function testItemNotExist() + { + $this->assertNull($this->provider->getItem('doesNotExist')); + } + + public function testGetMultipleItems() + { + $testData1 = ['tao', 'te', 'ching']; + $testData2 = ['plato', 'aristotle']; + + // First assert they do not exist + $this->assertNull($this->provider->getItem('philo1')); + $this->assertNull($this->provider->getItem('philo2')); + $this->assertEquals(['philo1' => null, 'philo2' => null], $this->provider->getItems(['philo1', 'philo2'])); + + // Then write both + $this->assertTrue($this->provider->save('philo1', $testData1)); + $this->assertTrue($this->provider->save('philo2', $testData2)); + + // Then read + $this->assertEquals([ + 'philo1' => ['tao', 'te', 'ching'], + 'philo2' => ['plato', 'aristotle'] + ], $this->provider->getItems(['philo1', 'philo2'])); + } + + public function testItemMetaData() + { + $testData = ['meine', 'gute']; + $metaData = ['someKey' => 'someValue']; + + // First assert that the data does not exist + $this->assertNull($this->provider->getItem('testData')); + $this->assertNull($this->provider->getItemMeta('testData')); + + // Then save the data + $this->assertTrue($this->provider->save('testData', $testData, $metaData)); + + // Check the metaData + $this->assertEquals(['someKey' => 'someValue'], $this->provider->getItemMeta('testData')); + + // Remove the key + $this->provider->deleteItem('testData'); + $this->assertNull($this->provider->getItemMeta('testData')); + } +} \ No newline at end of file