From 8ea1cbc52fbc797757d998e9b6e2a5a53dc484ef Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Wed, 11 Mar 2020 23:04:01 -0400 Subject: [PATCH] Put Ion Namespace back in the codebase directly --- composer.json | 9 +- src/Ion/ArrayWrapper.php | 38 +++ src/Ion/Config.php | 120 ++++++++ src/Ion/ConfigInterface.php | 56 ++++ src/Ion/Di/Container.php | 228 ++++++++++++++ src/Ion/Di/ContainerAware.php | 53 ++++ src/Ion/Di/ContainerAwareInterface.php | 40 +++ src/Ion/Di/ContainerInterface.php | 100 ++++++ src/Ion/Di/Exception/ContainerException.php | 28 ++ src/Ion/Di/Exception/NotFoundException.php | 28 ++ src/Ion/Enum.php | 59 ++++ src/Ion/Exception/ConfigException.php | 26 ++ src/Ion/Exception/DoubleRenderException.php | 38 +++ src/Ion/Friend.php | 155 ++++++++++ src/Ion/Json.php | 139 +++++++++ src/Ion/JsonException.php | 27 ++ src/Ion/Model.php | 25 ++ src/Ion/Model/DB.php | 55 ++++ src/Ion/StringWrapper.php | 38 +++ src/Ion/Transformer/AbstractTransformer.php | 69 +++++ src/Ion/Transformer/TransformerInterface.php | 31 ++ src/Ion/Type/ArrayType.php | 286 ++++++++++++++++++ src/Ion/Type/StringType.php | 42 +++ src/Ion/View.php | 143 +++++++++ src/Ion/View/HtmlView.php | 81 +++++ src/Ion/View/HttpView.php | 102 +++++++ src/Ion/View/JsonView.php | 54 ++++ src/Ion/ViewInterface.php | 75 +++++ src/Ion/XML.php | 279 +++++++++++++++++ src/Ion/functions.php | 29 ++ tests/Ion/BaseModelTest.php | 28 ++ tests/Ion/ConfigTest.php | 151 +++++++++ tests/Ion/Di/ContainerAwareTest.php | 55 ++++ tests/Ion/Di/ContainerTest.php | 205 +++++++++++++ tests/Ion/EnumTest.php | 83 +++++ .../Exception/DoubleRenderExceptionTest.php | 31 ++ tests/Ion/FriendTest.php | 75 +++++ tests/Ion/Ion_TestCase.php | 133 ++++++++ tests/Ion/JsonTest.php | 89 ++++++ tests/Ion/Model/BaseDBModelTest.php | 29 ++ tests/Ion/StringWrapperTest.php | 39 +++ tests/Ion/TestSessionHandler.php | 71 +++++ .../Transformer/AbstractTransformerTest.php | 157 ++++++++++ tests/Ion/Type/ArrayTypeTest.php | 215 +++++++++++++ tests/Ion/Type/StringTypeTest.php | 66 ++++ tests/Ion/View/HtmlViewTest.php | 42 +++ tests/Ion/View/HttpViewTest.php | 95 ++++++ tests/Ion/View/JsonViewTest.php | 57 ++++ tests/Ion/XMLTest.php | 88 ++++++ tests/Ion/bootstrap.php | 41 +++ tests/Ion/di.php | 48 +++ tests/Ion/functionsTest.php | 33 ++ tests/Ion/mocks.php | 191 ++++++++++++ .../Ion/test_data/XML/minifiedXmlTestFile.xml | 2 + tests/Ion/test_data/XML/xmlTestFile.xml | 22 ++ tests/Ion/test_data/invalid_json.json | 1 + tests/Ion/test_data/valid_json.json | 5 + tests/Ion/test_views/test_view.php | 1 + 58 files changed, 4504 insertions(+), 2 deletions(-) create mode 100644 src/Ion/ArrayWrapper.php create mode 100644 src/Ion/Config.php create mode 100644 src/Ion/ConfigInterface.php create mode 100644 src/Ion/Di/Container.php create mode 100644 src/Ion/Di/ContainerAware.php create mode 100644 src/Ion/Di/ContainerAwareInterface.php create mode 100644 src/Ion/Di/ContainerInterface.php create mode 100644 src/Ion/Di/Exception/ContainerException.php create mode 100644 src/Ion/Di/Exception/NotFoundException.php create mode 100644 src/Ion/Enum.php create mode 100644 src/Ion/Exception/ConfigException.php create mode 100644 src/Ion/Exception/DoubleRenderException.php create mode 100644 src/Ion/Friend.php create mode 100644 src/Ion/Json.php create mode 100644 src/Ion/JsonException.php create mode 100644 src/Ion/Model.php create mode 100644 src/Ion/Model/DB.php create mode 100644 src/Ion/StringWrapper.php create mode 100644 src/Ion/Transformer/AbstractTransformer.php create mode 100644 src/Ion/Transformer/TransformerInterface.php create mode 100644 src/Ion/Type/ArrayType.php create mode 100644 src/Ion/Type/StringType.php create mode 100644 src/Ion/View.php create mode 100644 src/Ion/View/HtmlView.php create mode 100644 src/Ion/View/HttpView.php create mode 100644 src/Ion/View/JsonView.php create mode 100644 src/Ion/ViewInterface.php create mode 100644 src/Ion/XML.php create mode 100644 src/Ion/functions.php create mode 100644 tests/Ion/BaseModelTest.php create mode 100644 tests/Ion/ConfigTest.php create mode 100644 tests/Ion/Di/ContainerAwareTest.php create mode 100644 tests/Ion/Di/ContainerTest.php create mode 100644 tests/Ion/EnumTest.php create mode 100644 tests/Ion/Exception/DoubleRenderExceptionTest.php create mode 100644 tests/Ion/FriendTest.php create mode 100644 tests/Ion/Ion_TestCase.php create mode 100644 tests/Ion/JsonTest.php create mode 100644 tests/Ion/Model/BaseDBModelTest.php create mode 100644 tests/Ion/StringWrapperTest.php create mode 100644 tests/Ion/TestSessionHandler.php create mode 100644 tests/Ion/Transformer/AbstractTransformerTest.php create mode 100644 tests/Ion/Type/ArrayTypeTest.php create mode 100644 tests/Ion/Type/StringTypeTest.php create mode 100644 tests/Ion/View/HtmlViewTest.php create mode 100644 tests/Ion/View/HttpViewTest.php create mode 100644 tests/Ion/View/JsonViewTest.php create mode 100644 tests/Ion/XMLTest.php create mode 100644 tests/Ion/bootstrap.php create mode 100644 tests/Ion/di.php create mode 100644 tests/Ion/functionsTest.php create mode 100644 tests/Ion/mocks.php create mode 100644 tests/Ion/test_data/XML/minifiedXmlTestFile.xml create mode 100644 tests/Ion/test_data/XML/xmlTestFile.xml create mode 100644 tests/Ion/test_data/invalid_json.json create mode 100644 tests/Ion/test_data/valid_json.json create mode 100644 tests/Ion/test_views/test_view.php diff --git a/composer.json b/composer.json index 8e955fe1..692c1c19 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "license": "MIT", "autoload": { "files": [ + "src/Ion/functions.php", "src/AnimeClient/constants.php", "src/AnimeClient/AnimeClient.php" ], @@ -13,7 +14,8 @@ }, "autoload-dev": { "psr-4": { - "Aviat\\AnimeClient\\Tests\\": "tests/", + "Aviat\\AnimeClient\\Tests\\": "tests/AnimeClient", + "Aviat\\Ion\\Tests\\": "tests/Ion", "CodeIgniter\\": "build/CodeIgniter/" } }, @@ -23,12 +25,15 @@ "aura/router": "^3.0", "aura/session": "^2.0", "aviat/banker": "^2.0.0", - "aviat/ion": "^3.0.0", + "aviat/query": "^2.5.1", + "danielstjules/stringy": "^3.1.0", + "ext-dom": "*", "ext-iconv": "*", "ext-json": "*", "ext-gd":"*", "ext-pdo": "*", "laminas/laminas-diactoros": "^2.0.0", + "laminas/laminas-httphandlerrunner": "^1.0", "maximebf/consolekit": "^1.0", "monolog/monolog": "^2.0.1", "php": "^7.3", diff --git a/src/Ion/ArrayWrapper.php b/src/Ion/ArrayWrapper.php new file mode 100644 index 00000000..d9e761d2 --- /dev/null +++ b/src/Ion/ArrayWrapper.php @@ -0,0 +1,38 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use Aviat\Ion\Type\ArrayType; + +/** + * Wrapper to shortcut creating ArrayType objects + */ +trait ArrayWrapper { + + /** + * Convenience method for wrapping an array + * with the array type class + * + * @param array $arr + * @return ArrayType + */ + public function arr(array $arr): ArrayType + { + return new ArrayType($arr); + } +} +// End of ArrayWrapper.php \ No newline at end of file diff --git a/src/Ion/Config.php b/src/Ion/Config.php new file mode 100644 index 00000000..03c7ffcc --- /dev/null +++ b/src/Ion/Config.php @@ -0,0 +1,120 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use Aviat\Ion\Exception\ConfigException; +use Aviat\Ion\Type\ArrayType; +use InvalidArgumentException; + +/** + * Wrapper for configuration values + */ +class Config implements ConfigInterface { + + use ArrayWrapper; + + /** + * Config object + * + * @var ArrayType + */ + protected $map; + + /** + * Constructor + * + * @param array $configArray + */ + public function __construct(array $configArray = []) + { + $this->map = $this->arr($configArray); + } + + /** + * Does the config item exist? + * + * @param string|int|array $key + * @return bool + */ + public function has($key): bool + { + return $this->map->hasKey($key); + } + + /** + * Get a config value + * + * @param array|string|null $key + * @return mixed + * @throws ConfigException + */ + public function get($key = NULL) + { + if (\is_array($key)) + { + return $this->map->getDeepKey($key); + } + + return $this->map->get($key); + } + + /** + * Remove a config value + * + * @param string|array $key + * @return void + */ + public function delete($key): void + { + if (\is_array($key)) + { + $this->map->setDeepKey($key, NULL); + } + else + { + $pos =& $this->map->get($key); + $pos = NULL; + } + } + + /** + * Set a config value + * + * @param integer|string|array $key + * @param mixed $value + * @throws InvalidArgumentException + * @return ConfigInterface + */ + public function set($key, $value): ConfigInterface + { + if (\is_array($key)) + { + $this->map->setDeepKey($key, $value); + } + else if (is_scalar($key) && ! empty($key)) + { + $this->map->set($key, $value); + } + else + { + throw new InvalidArgumentException('Key must be integer, string, or array, and cannot be empty'); + } + + return $this; + } +} +// End of config.php \ No newline at end of file diff --git a/src/Ion/ConfigInterface.php b/src/Ion/ConfigInterface.php new file mode 100644 index 00000000..19e394da --- /dev/null +++ b/src/Ion/ConfigInterface.php @@ -0,0 +1,56 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +/** + * Standard interface for retrieving/setting configuration values + */ +interface ConfigInterface { + /** + * Does the config item exist? + * + * @param string|int|array $key + * @return bool + */ + public function has($key): bool; + + /** + * Get a config value + * + * @param array|string|null $key + * @return mixed + */ + public function get($key = NULL); + + /** + * Set a config value + * + * @param integer|string|array $key + * @param mixed $value + * @throws \InvalidArgumentException + * @return ConfigInterface + */ + public function set($key, $value): self; + + /** + * Remove a config value + * + * @param string|array $key + * @return void + */ + public function delete($key): void; +} \ No newline at end of file diff --git a/src/Ion/Di/Container.php b/src/Ion/Di/Container.php new file mode 100644 index 00000000..3ea33be9 --- /dev/null +++ b/src/Ion/Di/Container.php @@ -0,0 +1,228 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Di; + +use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; +use Psr\Log\LoggerInterface; + +/** + * Dependency container + */ +class Container implements ContainerInterface { + + /** + * Array of container Generator functions + * + * @var Callable[] + */ + protected $container = []; + + /** + * Array of object instances + * + * @var array + */ + protected $instances = []; + + /** + * Map of logger instances + * + * @var array + */ + protected $loggers = []; + + /** + * Constructor + * + * @param array $values (optional) + */ + public function __construct(array $values = []) + { + $this->container = $values; + $this->loggers = []; + } + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id - Identifier of the entry to look for. + * + * @throws NotFoundException - No entry was found for this identifier. + * @throws ContainerException - Error while retrieving the entry. + * + * @return mixed Entry. + */ + public function get($id) + { + if ( ! \is_string($id)) + { + throw new ContainerException('Id must be a string'); + } + + if ($this->has($id)) + { + // Return an object instance, if it already exists + if (array_key_exists($id, $this->instances)) + { + return $this->instances[$id]; + } + + // If there isn't already an instance, create one + $obj = $this->getNew($id); + $this->instances[$id] = $obj; + return $obj; + } + + throw new NotFoundException("Item '{$id}' does not exist in container."); + } + + /** + * Get a new instance of the specified item + * + * @param string $id - Identifier of the entry to look for. + * @param array $args - Optional arguments for the factory callable + * @throws NotFoundException - No entry was found for this identifier. + * @throws ContainerException - Error while retrieving the entry. + * @return mixed + */ + public function getNew($id, array $args = NULL) + { + if ( ! \is_string($id)) + { + throw new ContainerException('Id must be a string'); + } + + if ($this->has($id)) + { + // By default, call a factory with the Container + $args = \is_array($args) ? $args : [$this]; + $obj = \call_user_func_array($this->container[$id], $args); + + // Check for container interface, and apply the container to the object + // if applicable + return $this->applyContainer($obj); + } + + throw new NotFoundException("Item '{$id}' does not exist in container."); + } + + /** + * Add a factory to the container + * + * @param string $id + * @param Callable $value - a factory callable for the item + * @return ContainerInterface + */ + public function set(string $id, Callable $value): ContainerInterface + { + $this->container[$id] = $value; + return $this; + } + + /** + * Set a specific instance in the container for an existing factory + * + * @param string $id + * @param mixed $value + * @throws NotFoundException - No entry was found for this identifier. + * @return ContainerInterface + */ + public function setInstance(string $id, $value): ContainerInterface + { + if ( ! $this->has($id)) + { + throw new NotFoundException("Factory '{$id}' does not exist in container. Set that first."); + } + + $this->instances[$id] = $value; + return $this; + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * @param string $id Identifier of the entry to look for. + * @return boolean + */ + public function has($id): bool + { + return array_key_exists($id, $this->container); + } + + /** + * Determine whether a logger channel is registered + * + * @param string $id The logger channel + * @return boolean + */ + public function hasLogger(string $id = 'default'): bool + { + return array_key_exists($id, $this->loggers); + } + + /** + * Add a logger to the Container + * + * @param LoggerInterface $logger + * @param string $id The logger 'channel' + * @return ContainerInterface + */ + public function setLogger(LoggerInterface $logger, string $id = 'default'): ContainerInterface + { + $this->loggers[$id] = $logger; + return $this; + } + + /** + * Retrieve a logger for the selected channel + * + * @param string $id The logger to retrieve + * @return LoggerInterface|null + */ + public function getLogger(string $id = 'default'): ?LoggerInterface + { + return $this->hasLogger($id) + ? $this->loggers[$id] + : NULL; + } + + /** + * Check if object implements ContainerAwareInterface + * or uses ContainerAware trait, and if so, apply the container + * to that object + * + * @param mixed $obj + * @return mixed + */ + private function applyContainer($obj) + { + $trait_name = ContainerAware::class; + $interface_name = ContainerAwareInterface::class; + + $uses_trait = \in_array($trait_name, class_uses($obj), TRUE); + $implements_interface = \in_array($interface_name, class_implements($obj), TRUE); + + if ($uses_trait || $implements_interface) + { + $obj->setContainer($this); + } + + return $obj; + } +} +// End of Container.php \ No newline at end of file diff --git a/src/Ion/Di/ContainerAware.php b/src/Ion/Di/ContainerAware.php new file mode 100644 index 00000000..40b664b2 --- /dev/null +++ b/src/Ion/Di/ContainerAware.php @@ -0,0 +1,53 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Di; + +/** + * Trait implementation of ContainerAwareInterface + */ +trait ContainerAware { + + /** + * Di Container + * + * @var ContainerInterface + */ + protected $container; + + /** + * Set the container for the current object + * + * @param ContainerInterface $container + * @return self + */ + public function setContainer(ContainerInterface $container): self + { + $this->container = $container; + return $this; + } + + /** + * Get the container object + * + * @return ContainerInterface + */ + public function getContainer(): ContainerInterface + { + return $this->container; + } +} +// End of ContainerAware.php \ No newline at end of file diff --git a/src/Ion/Di/ContainerAwareInterface.php b/src/Ion/Di/ContainerAwareInterface.php new file mode 100644 index 00000000..041b05de --- /dev/null +++ b/src/Ion/Di/ContainerAwareInterface.php @@ -0,0 +1,40 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Di; + +/** + * Interface for a class that is aware of the Di Container + */ +interface ContainerAwareInterface { + + /** + * Set the container for the current object + * + * @param ContainerInterface $container + * @return void + */ + public function setContainer(ContainerInterface $container); + + /** + * Get the container object + * + * @return ContainerInterface + */ + public function getContainer(): ContainerInterface; + +} +// End of ContainerAwareInterface.php \ No newline at end of file diff --git a/src/Ion/Di/ContainerInterface.php b/src/Ion/Di/ContainerInterface.php new file mode 100644 index 00000000..780868e7 --- /dev/null +++ b/src/Ion/Di/ContainerInterface.php @@ -0,0 +1,100 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Di; + +use Psr\Log\LoggerInterface; + +/** + * Interface for the Dependency Injection Container + * + * Based on container-interop interface, but return types and + * scalar type hints make the interface incompatible to the PHP parser + * + * @see https://github.com/container-interop/container-interop + */ +interface ContainerInterface { + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id Identifier of the entry to look for. + * @throws Exception\NotFoundException No entry was found for this identifier. + * @throws Exception\ContainerException Error while retrieving the entry. + * @return mixed Entry. + */ + public function get($id); + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * @param string $id Identifier of the entry to look for. + * @return boolean + */ + public function has($id): bool; + + /** + * Add a factory to the container + * + * @param string $id + * @param Callable $value - a factory callable for the item + * @return ContainerInterface + */ + public function set(string $id, Callable $value): ContainerInterface; + + /** + * Set a specific instance in the container for an existing factory + * + * @param string $id + * @param mixed $value + * @return ContainerInterface + */ + public function setInstance(string $id, $value): ContainerInterface; + + /** + * Get a new instance of the specified item + * + * @param string $id + * @return mixed + */ + public function getNew($id); + + /** + * Determine whether a logger channel is registered + * + * @param string $id The logger channel + * @return boolean + */ + public function hasLogger(string $id = 'default'): bool; + + /** + * Add a logger to the Container + * + * @param LoggerInterface $logger + * @param string $id The logger 'channel' + * @return ContainerInterface + */ + public function setLogger(LoggerInterface $logger, string $id = 'default'): ContainerInterface; + + /** + * Retrieve a logger for the selected channel + * + * @param string $id The logger to retrieve + * @return LoggerInterface|null + */ + public function getLogger(string $id = 'default'): ?LoggerInterface; +} \ No newline at end of file diff --git a/src/Ion/Di/Exception/ContainerException.php b/src/Ion/Di/Exception/ContainerException.php new file mode 100644 index 00000000..ee0e7f93 --- /dev/null +++ b/src/Ion/Di/Exception/ContainerException.php @@ -0,0 +1,28 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Di\Exception; + +use Exception; +use Interop\Container\Exception\ContainerException as InteropContainerException; + +/** + * Generic exception for Di Container + */ +class ContainerException extends Exception implements InteropContainerException { + +} +// End of ContainerException.php \ No newline at end of file diff --git a/src/Ion/Di/Exception/NotFoundException.php b/src/Ion/Di/Exception/NotFoundException.php new file mode 100644 index 00000000..220a7f26 --- /dev/null +++ b/src/Ion/Di/Exception/NotFoundException.php @@ -0,0 +1,28 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Di\Exception; + +use Interop\Container\Exception\NotFoundException as InteropNotFoundException; + +/** + * Exception for Di Container when trying to access a + * key that doesn't exist in the container + */ +class NotFoundException extends ContainerException implements InteropNotFoundException { + +} +// End of NotFoundException.php \ No newline at end of file diff --git a/src/Ion/Enum.php b/src/Ion/Enum.php new file mode 100644 index 00000000..a3cf643a --- /dev/null +++ b/src/Ion/Enum.php @@ -0,0 +1,59 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use ReflectionClass; + +/** + * Class emulating an enumeration type + */ +abstract class Enum { + + /** + * Return the list of constant values for the Enum + * + * @return array + * @throws \ReflectionException + */ + public static function getConstList(): array + { + static $self; + + if ($self === NULL) + { + $class = static::class; + $self = new $class; + } + + $reflect = new ReflectionClass($self); + return $reflect->getConstants(); + } + + /** + * Verify that a constant value is valid + * + * @param mixed $key + * @return boolean + * @throws \ReflectionException + */ + public static function isValid($key): bool + { + $values = array_values(static::getConstList()); + return \in_array($key, $values, TRUE); + } +} +// End of Enum.php \ No newline at end of file diff --git a/src/Ion/Exception/ConfigException.php b/src/Ion/Exception/ConfigException.php new file mode 100644 index 00000000..b26f0e76 --- /dev/null +++ b/src/Ion/Exception/ConfigException.php @@ -0,0 +1,26 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Exception; + +use InvalidArgumentException; + +/** + * Exception for bad configuration + */ +class ConfigException extends InvalidArgumentException { + +} \ No newline at end of file diff --git a/src/Ion/Exception/DoubleRenderException.php b/src/Ion/Exception/DoubleRenderException.php new file mode 100644 index 00000000..9fd5cbee --- /dev/null +++ b/src/Ion/Exception/DoubleRenderException.php @@ -0,0 +1,38 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Exception; + +use Exception; +use LogicException; + +/** + * Exception called when a view is attempted to be sent twice + */ +class DoubleRenderException extends LogicException { + + /** + * DoubleRenderException constructor. + * + * @param string $message + * @param int $code + * @param Exception|null $previous + */ + public function __construct(string $message = 'A view can only be rendered once, because headers can only be sent once.', int $code = 0, Exception $previous = NULL) + { + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/src/Ion/Friend.php b/src/Ion/Friend.php new file mode 100644 index 00000000..fc2f80c2 --- /dev/null +++ b/src/Ion/Friend.php @@ -0,0 +1,155 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use BadMethodCallException; +use InvalidArgumentException; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +/** + * Friend class for testing + */ +class Friend { + + /** + * Object to create a friend of + * @var mixed + */ + private $_friend_; + + /** + * Reflection class of the object + * @var ReflectionClass + */ + private $_reflect_; + + /** + * Create a friend object + * + * @param mixed $obj + * @throws InvalidArgumentException + * @throws \ReflectionException + */ + public function __construct($obj) + { + if ( ! \is_object($obj)) + { + throw new InvalidArgumentException('Friend must be an object'); + } + + $this->_friend_ = $obj; + $this->_reflect_ = new ReflectionClass($obj); + } + + /** + * Retrieve a friend's property + * + * @param string $key + * @return mixed + */ + public function __get(string $key) + { + if ($this->__isset($key)) + { + $property = $this->_get_property($key); + + if ($property !== NULL) + { + return $property->getValue($this->_friend_); + } + } + + return NULL; + } + + /** + * See if a property exists on the friend + * + * @param string $name + * @return bool + */ + public function __isset(string $name): bool + { + return $this->_reflect_->hasProperty($name); + } + + /** + * Set a friend's property + * + * @param string $key + * @param mixed $value + * @return void + */ + public function __set(string $key, $value) + { + if ($this->__isset($key)) + { + $property = $this->_get_property($key); + + if ($property !== NULL) + { + $property->setValue($this->_friend_, $value); + } + } + } + + /** + * Calls a protected or private method on the friend + * + * @param string $method + * @param array $args + * @return mixed + * @throws BadMethodCallException + * @throws \ReflectionException + */ + public function __call(string $method, array $args) + { + if ( ! $this->_reflect_->hasMethod($method)) + { + throw new BadMethodCallException("Method '{$method}' does not exist"); + } + + $friendMethod = new ReflectionMethod($this->_friend_, $method); + $friendMethod->setAccessible(TRUE); + return $friendMethod->invokeArgs($this->_friend_, $args); + } + + /** + * Iterates over parent classes to get a ReflectionProperty + * + * @param string $name + * @return ReflectionProperty|null + */ + private function _get_property(string $name): ?ReflectionProperty + { + try + { + $property = $this->_reflect_->getProperty($name); + $property->setAccessible(TRUE); + return $property; + } + // Return NULL on any exception, so no further logic needed + // in the catch block + catch (\Exception $e) + { + return NULL; + } + } +} +// End of Friend.php \ No newline at end of file diff --git a/src/Ion/Json.php b/src/Ion/Json.php new file mode 100644 index 00000000..ef78907d --- /dev/null +++ b/src/Ion/Json.php @@ -0,0 +1,139 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use Aviat\Ion\Type\StringType; + +/** + * Helper class for json convenience methods + */ +class Json { + + /** + * Encode data in json format + * + * @param mixed $data + * @param int $options + * @param int $depth + * @throws JsonException + * @return string + */ + public static function encode($data, $options = 0, $depth = 512): string + { + $json = json_encode($data, $options, $depth); + self::check_json_error(); + return $json; + } + + /** + * Encode data in json format and save to a file + * + * @param string $filename + * @param mixed $data + * @param int $jsonOptions - Options to pass to json_encode + * @param int $fileOptions - Options to pass to file_get_contents + * @throws JsonException + * @return int + */ + public static function encodeFile(string $filename, $data, int $jsonOptions = 0, int $fileOptions = 0): int + { + $json = self::encode($data, $jsonOptions); + return file_put_contents($filename, $json, $fileOptions); + } + + /** + * Decode data from json + * + * @param string|null $json + * @param bool $assoc + * @param int $depth + * @param int $options + * @throws JsonException + * @return mixed + */ + public static function decode($json, bool $assoc = TRUE, int $depth = 512, int $options = 0) + { + // Don't try to decode null + if ($json === NULL) + { + return NULL; + } + + $data = json_decode($json, $assoc, $depth, $options); + + self::check_json_error(); + return $data; + } + + /** + * Decode json data loaded from the passed filename + * + * @param string $filename + * @param bool $assoc + * @param int $depth + * @param int $options + * @throws JsonException + * @return mixed + */ + public static function decodeFile(string $filename, bool $assoc = TRUE, int $depth = 512, int $options = 0) + { + $json = file_get_contents($filename); + return self::decode($json, $assoc, $depth, $options); + } + + /** + * Determines whether a string is valid json + * + * @param string $string + * @throws \InvalidArgumentException + * @return boolean + */ + public static function isJson(string $string): bool + { + return StringType::create($string)->isJson(); + } + + /** + * Call the json error functions to check for errors encoding/decoding + * + * @throws JsonException + * @return void + */ + protected static function check_json_error(): void + { + $constant_map = [ + JSON_ERROR_NONE => 'JSON_ERROR_NONE', + JSON_ERROR_DEPTH => 'JSON_ERROR_DEPTH', + JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH', + JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR', + JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX', + JSON_ERROR_UTF8 => 'JSON_ERROR_UTF8', + JSON_ERROR_RECURSION => 'JSON_ERROR_RECURSION', + JSON_ERROR_INF_OR_NAN => 'JSON_ERROR_INF_OR_NAN', + JSON_ERROR_UNSUPPORTED_TYPE => 'JSON_ERROR_UNSUPPORTED_TYPE' + ]; + + $error = json_last_error(); + $message = json_last_error_msg(); + + if (JSON_ERROR_NONE !== $error) + { + throw new JsonException("{$constant_map[$error]} - {$message}", $error); + } + } +} +// End of JSON.php \ No newline at end of file diff --git a/src/Ion/JsonException.php b/src/Ion/JsonException.php new file mode 100644 index 00000000..a8d288fe --- /dev/null +++ b/src/Ion/JsonException.php @@ -0,0 +1,27 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use InvalidArgumentException; + +/** + * Exceptions thrown by the Json class + */ +class JsonException extends InvalidArgumentException { + +} +// End of JsonException.php \ No newline at end of file diff --git a/src/Ion/Model.php b/src/Ion/Model.php new file mode 100644 index 00000000..bdc8009f --- /dev/null +++ b/src/Ion/Model.php @@ -0,0 +1,25 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +/** + * Common base for all Models + */ +class Model { + use StringWrapper; +} +// End of Model.php diff --git a/src/Ion/Model/DB.php b/src/Ion/Model/DB.php new file mode 100644 index 00000000..93888cc6 --- /dev/null +++ b/src/Ion/Model/DB.php @@ -0,0 +1,55 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Model; + +use Aviat\Ion\ConfigInterface; +use Aviat\Ion\Model as BaseModel; + +/** + * Base model for database interaction + */ +class DB extends BaseModel { + /** + * The query builder object + * @var object $db + */ + protected $db; + + /** + * The config manager + * @var ConfigInterface + */ + protected $config; + + /** + * The database connection information array + * @var array $db_config + */ + protected $db_config; + + /** + * Constructor + * + * @param ConfigInterface $config + */ + public function __construct(ConfigInterface $config) + { + $this->config = $config; + $this->db_config = (array)$config->get('database'); + } +} +// End of DB.php diff --git a/src/Ion/StringWrapper.php b/src/Ion/StringWrapper.php new file mode 100644 index 00000000..e019da20 --- /dev/null +++ b/src/Ion/StringWrapper.php @@ -0,0 +1,38 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use Aviat\Ion\Type\StringType; + +/** + * Trait to add convenience method for creating StringType objects + */ +trait StringWrapper { + + /** + * Wrap the String in the Stringy class + * + * @param string $str + * @throws \InvalidArgumentException + * @return StringType + */ + public function string($str): StringType + { + return StringType::create($str); + } +} +// End of StringWrapper.php \ No newline at end of file diff --git a/src/Ion/Transformer/AbstractTransformer.php b/src/Ion/Transformer/AbstractTransformer.php new file mode 100644 index 00000000..4b41767d --- /dev/null +++ b/src/Ion/Transformer/AbstractTransformer.php @@ -0,0 +1,69 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Transformer; + +use Aviat\Ion\StringWrapper; + +use BadMethodCallException; + +/** + * Base class for data transformation + */ +abstract class AbstractTransformer implements TransformerInterface { + + use StringWrapper; + + /** + * Mutate the data structure + * + * @param array|object $item + * @return mixed + */ + abstract public function transform($item); + + /** + * Transform a set of structures + * + * @param iterable $collection + * @return array + */ + public function transformCollection(iterable $collection): array + { + $list = (array)$collection; + return array_map([$this, 'transform'], $list); + } + + /** + * Untransform a set of structures + * + * Requires an 'untransform' method in the extending class + * + * @param iterable $collection + * @return array + */ + public function untransformCollection(iterable $collection): array + { + if ( ! method_exists($this, 'untransform')) + { + throw new BadMethodCallException('untransform() method does not exist.'); + } + + $list = (array)$collection; + return array_map([$this, 'untransform'], $list); + } +} +// End of AbstractTransformer.php \ No newline at end of file diff --git a/src/Ion/Transformer/TransformerInterface.php b/src/Ion/Transformer/TransformerInterface.php new file mode 100644 index 00000000..bc6d58e0 --- /dev/null +++ b/src/Ion/Transformer/TransformerInterface.php @@ -0,0 +1,31 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Transformer; + +/** + * Interface for data transformation classes + */ +interface TransformerInterface { + + /** + * Mutate the data structure + * + * @param array|object $item + * @return mixed + */ + public function transform($item); +} \ No newline at end of file diff --git a/src/Ion/Type/ArrayType.php b/src/Ion/Type/ArrayType.php new file mode 100644 index 00000000..fe1e82cd --- /dev/null +++ b/src/Ion/Type/ArrayType.php @@ -0,0 +1,286 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Type; + +use InvalidArgumentException; + +/** + * Wrapper class for native array methods for convenience + * + * @method array chunk(int $size, bool $preserve_keys = FALSE) + * @method array pluck(mixed $column_key, mixed $index_key = NULL) + * @method array filter(callable $callback = NULL, int $flag = 0) + */ +class ArrayType { + + /** + * The current array + * + * @var array + */ + protected $arr; + + /** + * Map generated methods to their native implementations + * + * @var array + */ + protected $nativeMethods = [ + 'chunk' => 'array_chunk', + 'diff' => 'array_diff', + 'filter' => 'array_filter', + 'flip' => 'array_flip', + 'intersect' => 'array_intersect', + 'key_diff' => 'array_diff_key', + 'keys' => 'array_keys', + 'merge' => 'array_merge', + 'pad' => 'array_pad', + 'pluck' => 'array_column', + 'product' => 'array_product', + 'random' => 'array_rand', + 'reduce' => 'array_reduce', + 'reverse' => 'array_reverse', + 'sum' => 'array_sum', + 'unique' => 'array_unique', + 'values' => 'array_values', + ]; + + /** + * Native methods that modify the passed in array + * + * @var array + */ + protected $nativeInPlaceMethods = [ + 'shuffle' => 'shuffle', + 'shift' => 'array_shift', + 'unshift' => 'array_unshift', + 'push' => 'array_push', + 'pop' => 'array_pop', + ]; + + /** + * Create an ArrayType wrapper class + * + * @param array $arr + */ + public function __construct(array &$arr) + { + $this->arr =& $arr; + } + + /** + * Call one of the dynamically created methods + * + * @param string $method + * @param array $args + * @return mixed + * @throws InvalidArgumentException + */ + public function __call(string $method, array $args) + { + // Simple mapping for the majority of methods + if (array_key_exists($method, $this->nativeMethods)) + { + $func = $this->nativeMethods[$method]; + // Set the current array as the first argument of the method + return $func($this->arr, ...$args); + } + + // Mapping for in-place methods + if (array_key_exists($method, $this->nativeInPlaceMethods)) + { + $func = $this->nativeInPlaceMethods[$method]; + $func($this->arr); + return $this->arr; + } + + throw new InvalidArgumentException("Method '{$method}' does not exist"); + } + + /** + * Does the passed key exist in the current array? + * + * @param int|string|array $key + * @return bool + */ + public function hasKey($key): bool + { + if (\is_array($key)) + { + $pos =& $this->arr; + + foreach($key as $level) + { + if ( ! array_key_exists($level, $pos)) + { + return FALSE; + } + + $pos =& $pos[$level]; + } + + return TRUE; + } + + return array_key_exists($key, $this->arr); + } + + /** + * Fill an array with the specified value + * + * @param int $start_index + * @param int $num + * @param mixed $value + * @return array + */ + public function fill(int $start_index, int $num, $value): array + { + return array_fill($start_index, $num, $value); + } + + /** + * Call a callback on each item of the array + * + * @param callable $callback + * @return array + */ + public function map(callable $callback): array + { + return array_map($callback, $this->arr); + } + + /** + * Find an array key by its associated value + * + * @param mixed $value + * @param bool $strict + * @return false|integer|string + */ + public function search($value, bool $strict = TRUE) + { + return array_search($value, $this->arr, $strict); + } + + /** + * Determine if the array has the passed value + * + * @param mixed $value + * @param bool $strict + * @return bool + */ + public function has($value, bool $strict = TRUE): bool + { + return \in_array($value, $this->arr, $strict); + } + + /** + * Return the array, or a key + * + * @param string|integer|null $key + * @return mixed + */ + public function &get($key = NULL) + { + $value = NULL; + if ($key === NULL) + { + $value =& $this->arr; + } + else + { + if ($this->hasKey($key)) + { + $value =& $this->arr[$key]; + } + } + + return $value; + } + + /** + * Set a key on the array + * + * @param mixed $key + * @param mixed $value + * @return ArrayType + */ + public function set($key, $value): ArrayType + { + $this->arr[$key] = $value; + return $this; + } + + /** + * Return a reference to the value of an arbitrary key on the array + * + * @example $arr = new ArrayType([0 => ['data' => ['foo' => 'bar']]]); + * $val = $arr->getDeepKey([0, 'data', 'foo']); + * // returns 'bar' + * @param array $key An array of keys of the array + * @return mixed + */ + public function &getDeepKey(array $key) + { + $pos =& $this->arr; + + foreach ($key as $level) + { + if (empty($pos) || ! is_array($pos)) + { + // Directly returning a NULL value here will + // result in a reference error. This isn't + // excess code, just what's required for this + // unique situation. + $pos = NULL; + return $pos; + } + $pos =& $pos[$level]; + } + + return $pos; + } + + /** + * Sets the value of an arbitrarily deep key in the array + * and returns the modified array + * + * @param array $key + * @param mixed $value + * @return array + */ + public function setDeepKey(array $key, $value): array + { + $pos =& $this->arr; + + // Iterate through the levels of the array, + // create the levels if they don't exist + foreach ($key as $level) + { + if ( ! \is_array($pos) && empty($pos)) + { + $pos = []; + $pos[$level] = []; + } + $pos =& $pos[$level]; + } + + $pos = $value; + + return $this->arr; + } +} +// End of ArrayType.php \ No newline at end of file diff --git a/src/Ion/Type/StringType.php b/src/Ion/Type/StringType.php new file mode 100644 index 00000000..ab54bff6 --- /dev/null +++ b/src/Ion/Type/StringType.php @@ -0,0 +1,42 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Type; + +use Stringy\Stringy; + +/** + * Wrapper around Stringy + */ +class StringType extends Stringy { + + /** + * See if two strings match, despite being delimited differently, + * such as camelCase, PascalCase, kebab-case, or snake_case. + * + * @param string $strToMatch + * @throws \InvalidArgumentException + * @return boolean + */ + public function fuzzyCaseMatch(string $strToMatch): bool + { + $firstStr = (string)self::create($this->str)->dasherize(); + $secondStr = (string)self::create($strToMatch)->dasherize(); + + return $firstStr === $secondStr; + } +} +// End of StringType.php \ No newline at end of file diff --git a/src/Ion/View.php b/src/Ion/View.php new file mode 100644 index 00000000..ff9e53e0 --- /dev/null +++ b/src/Ion/View.php @@ -0,0 +1,143 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use Psr\Http\Message\ResponseInterface; + +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Exception\DoubleRenderException; + +/** + * Base view response class + */ +abstract class View + // partially + implements ViewInterface { + + use Di\ContainerAware; + use StringWrapper; + + /** + * HTTP response Object + * + * @var ResponseInterface + */ + public $response; + + /** + * If the view has sent output via + * __toString or send method + * + * @var boolean + */ + protected $hasRendered = FALSE; + + /** + * Constructor + * + * @param ContainerInterface $container + * @throws Di\Exception\ContainerException + * @throws Di\Exception\NotFoundException + */ + public function __construct(ContainerInterface $container) + { + $this->setContainer($container); + $this->response = $container->get('response'); + } + + /** + * Send output to client + */ + public function __destruct() + { + if ( ! $this->hasRendered) + { + $this->send(); + } + } + + /** + * Return rendered output as string. Renders the view, + * and any attempts to call again will result in a DoubleRenderException + * + * @throws DoubleRenderException + * @return string + */ + public function __toString(): string + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + $this->hasRendered = TRUE; + return $this->getOutput(); + } + + /** + * Add an http header + * + * @param string $name + * @param string|string[] $value + * @throws \InvalidArgumentException + * @return ViewInterface + */ + public function addHeader(string $name, $value): ViewInterface + { + $this->response = $this->response->withHeader($name, $value); + return $this; + } + + /** + * Set the output string + * + * @param mixed $string + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @return ViewInterface + */ + public function setOutput($string): ViewInterface + { + $this->response->getBody()->write($string); + + return $this; + } + + /** + * Append additional output. + * + * @param string $string + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @return ViewInterface + */ + public function appendOutput(string $string): ViewInterface + { + return $this->setOutput($string); + } + + /** + * Get the current output as a string. Does not + * render view or send headers. + * + * @return string + */ + public function getOutput(): string + { + return (string)$this->response->getBody(); + } +} +// End of View.php \ No newline at end of file diff --git a/src/Ion/View/HtmlView.php b/src/Ion/View/HtmlView.php new file mode 100644 index 00000000..b9939ee1 --- /dev/null +++ b/src/Ion/View/HtmlView.php @@ -0,0 +1,81 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\View; + +use Aura\Html\HelperLocator; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Di\Exception\ContainerException; +use Aviat\Ion\Di\Exception\NotFoundException; + +/** + * View class for outputting HTML + */ +class HtmlView extends HttpView { + + /** + * HTML generator/escaper helper + * + * @var HelperLocator + */ + protected $helper; + + /** + * Response mime type + * + * @var string + */ + protected $contentType = 'text/html'; + + /** + * Create the Html View + * + * @param ContainerInterface $container + * @throws ContainerException + * @throws NotFoundException + */ + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + $this->helper = $container->get('html-helper'); + } + + /** + * Render a basic html Template + * + * @param string $path + * @param array $data + * @return string + */ + public function renderTemplate(string $path, array $data): string + { + $data['helper'] = $this->helper; + $data['escape'] = $this->helper->escape(); + $data['container'] = $this->container; + + ob_start(); + extract($data, \EXTR_OVERWRITE); + include_once $path; + $buffer = ob_get_clean(); + + + // Very basic html minify, that won't affect content between html tags + $buffer = preg_replace('/>\s+ <', $buffer); + + return $buffer; + } +} +// End of HtmlView.php \ No newline at end of file diff --git a/src/Ion/View/HttpView.php b/src/Ion/View/HttpView.php new file mode 100644 index 00000000..15c43a89 --- /dev/null +++ b/src/Ion/View/HttpView.php @@ -0,0 +1,102 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\View; + +use Zend\Diactoros\Response; +use Zend\HttpHandlerRunner\Emitter\SapiEmitter; + +use Aviat\Ion\Exception\DoubleRenderException; +use Aviat\Ion\View as BaseView; + +/** + * Base view class for Http output + */ +class HttpView extends BaseView { + + /** + * Response mime type + * + * @var string + */ + protected $contentType = ''; + + /** + * Do a redirect + * + * @param string $url + * @param int $code + * @param array $headers + * @throws \InvalidArgumentException + * @return void + */ + public function redirect(string $url, int $code = 302, array $headers = []): void + { + $this->response = new Response\RedirectResponse($url, $code, $headers); + } + + /** + * Set the status code of the request + * + * @param int $code + * @throws \InvalidArgumentException + * @return HttpView + */ + public function setStatusCode(int $code): HttpView + { + $this->response = $this->response->withStatus($code) + ->withProtocolVersion('1.1'); + return $this; + } + + /** + * Send output to client. As it renders the view, + * any attempt to call again will result in a DoubleRenderException. + * + * @throws DoubleRenderException + * @throws \InvalidArgumentException + * @return void + */ + public function send(): void + { + $this->output(); + } + + /** + * Send the appropriate response + * + * @throws DoubleRenderException + * @throws \InvalidArgumentException + * @return void + */ + protected function output(): void + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + + $this->response = $this->response + ->withHeader('Content-type', "{$this->contentType};charset=utf-8") + ->withHeader('X-Content-Type-Options', 'nosniff') + ->withHeader('X-XSS-Protection', '1;mode=block') + ->withHeader('X-Frame-Options', 'SAMEORIGIN'); + + (new SapiEmitter())->emit($this->response); + + $this->hasRendered = TRUE; + } +} \ No newline at end of file diff --git a/src/Ion/View/JsonView.php b/src/Ion/View/JsonView.php new file mode 100644 index 00000000..3716c06e --- /dev/null +++ b/src/Ion/View/JsonView.php @@ -0,0 +1,54 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\View; + +use Aviat\Ion\Json; +use Aviat\Ion\JsonException; +use Aviat\Ion\ViewInterface; + +/** + * View class to serialize Json + */ +class JsonView extends HttpView { + + /** + * Response mime type + * + * @var string + */ + protected $contentType = 'application/json'; + + /** + * Set the output string + * + * @param mixed $string + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @throws JsonException + * @return ViewInterface + */ + public function setOutput($string): ViewInterface + { + if ( ! \is_string($string)) + { + $string = Json::encode($string); + } + + return parent::setOutput($string); + } +} +// End of JsonView.php \ No newline at end of file diff --git a/src/Ion/ViewInterface.php b/src/Ion/ViewInterface.php new file mode 100644 index 00000000..c47b8243 --- /dev/null +++ b/src/Ion/ViewInterface.php @@ -0,0 +1,75 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use Aviat\Ion\Exception\DoubleRenderException; + +/** + * View Interface abstracting a Response + */ +interface ViewInterface { + /** + * Return rendered output as string. Renders the view, + * and any attempts to call again will result in a DoubleRenderException + * + * @throws DoubleRenderException + * @return string + */ + public function __toString(): string; + + /** + * Set the output string + * + * @param mixed $string + * @return ViewInterface + */ + public function setOutput($string): self; + + /** + * Append additional output. + * + * @param string $string + * @return ViewInterface + */ + public function appendOutput(string $string): self; + + /** + * Add an http header + * + * @param string $name + * @param string|string[] $value + * @return ViewInterface + */ + public function addHeader(string $name, $value): self; + + /** + * Get the current output as a string. Does not + * render view or send headers. + * + * @return string + */ + public function getOutput(): string; + + /** + * Send output to client. As it renders the view, + * any attempt to call again will result in a DoubleRenderException. + * + * @throws DoubleRenderException + * @return void + */ + public function send(): void; +} \ No newline at end of file diff --git a/src/Ion/XML.php b/src/Ion/XML.php new file mode 100644 index 00000000..ec1c9d8a --- /dev/null +++ b/src/Ion/XML.php @@ -0,0 +1,279 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +use DOMDocument, DOMNode, DOMNodeList, InvalidArgumentException; + +/** + * XML <=> PHP Array codec + */ +final class XML { + + /** + * XML representation of the data + * + * @var string + */ + private $xml; + + /** + * PHP array version of the data + * + * @var array + */ + private $data; + + /** + * XML constructor + * + * @param string $xml + * @param array $data + */ + public function __construct(string $xml = '', array $data = []) + { + $this->setXML($xml)->setData($data); + } + + /** + * Serialize the data to an xml string + * + * @return string + */ + public function __toString(): string + { + return static::toXML($this->getData()); + } + + /** + * Get the data parsed from the XML + * + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * Set the data to create xml from + * + * @param array $data + * @return self + */ + public function setData(array $data): self + { + $this->data = $data; + return $this; + } + + /** + * Get the xml created from the data + * + * @return string + */ + public function getXML(): string + { + return $this->xml; + } + + /** + * Set the xml to parse the data from + * + * @param string $xml + * @return self + */ + public function setXML(string $xml): self + { + $this->xml = $xml; + return $this; + } + + /** + * Parse an xml document string to a php array + * + * @param string $xml + * @return array + */ + public static function toArray(string $xml): array + { + $data = []; + + $xml = static::stripXMLWhitespace($xml); + + $dom = new DOMDocument(); + $hasLoaded = @$dom->loadXML($xml); + + if ( ! $hasLoaded) + { + throw new InvalidArgumentException('Failed to load XML'); + } + + $root = $dom->documentElement; + + $data[$root->tagName] = []; + + if ($root->hasChildNodes()) + { + static::childNodesToArray($data[$root->tagName], $root->childNodes); + } + + return $data; + } + + /** + * Transform the array into XML + * + * @param array $data + * @return string + */ + public static function toXML(array $data): string + { + $dom = new DOMDocument(); + $dom->encoding = 'UTF-8'; + + static::arrayPropertiesToXmlNodes($dom, $dom, $data); + + return $dom->saveXML(); + } + + /** + * Parse the xml document string to a php array + * + * @return array + */ + public function parse(): array + { + $xml = $this->getXML(); + $data = static::toArray($xml); + return $this->setData($data)->getData(); + } + + /** + * Transform the array into XML + * + * @return string + */ + public function createXML(): string + { + return static::toXML($this->getData()); + } + + /** + * Strip whitespace from raw xml to remove irrelevant text nodes + * + * @param string $xml + * @return string + */ + private static function stripXMLWhitespace(string $xml): string + { + // Get rid of unimportant text nodes by removing + // whitespace characters from between xml tags, + // except for the xml declaration tag, Which looks + // something like: + /* */ + return preg_replace('/([^?])>\s+<', $xml); + } + + /** + * Recursively create array structure based on xml structure + * + * @param array $root A reference to the current array location + * @param DOMNodeList $nodeList The current NodeList object + * @return void + */ + private static function childNodesToArray(array &$root, DOMNodelist $nodeList): void + { + $length = $nodeList->length; + for ($i = 0; $i < $length; $i++) + { + $el = $nodeList->item($i); + $current =& $root[$el->nodeName]; + + // It's a top level element! + if (( ! $el->hasChildNodes()) || ($el->childNodes->item(0) instanceof \DomText)) + { + $current = $el->textContent; + continue; + } + + // An empty value at the current root + if ($current === NULL) + { + $current = []; + static::childNodesToArray($current, $el->childNodes); + continue; + } + + $keys = array_keys($current); + + // Wrap the array in a containing array + // if there are only string keys + if ( ! is_numeric($keys[0])) + { + // But if there is only one key, don't wrap it in + // an array, just recurse to parse the child nodes + if (count($current) === 1) + { + static::childNodesToArray($current, $el->childNodes); + continue; + } + $current = [$current]; + } + + $current[] = []; + $index = count($current) - 1; + + static::childNodesToArray($current[$index], $el->childNodes); + } + } + + /** + * Recursively create xml nodes from array properties + * + * @param DOMDocument $dom The current DOM object + * @param DOMNode $parent The parent element to append children to + * @param array $data The data for the current node + * @return void + */ + private static function arrayPropertiesToXmlNodes(DOMDocument $dom, DOMNode $parent, array $data): void + { + foreach ($data as $key => $props) + { + // 'Flatten' the array as you create the xml + if (is_numeric($key)) + { + foreach ($props as $k => $p) + { + break; + } + } + + $node = $dom->createElement($key); + + if (\is_array($props)) + { + static::arrayPropertiesToXmlNodes($dom, $node, $props); + } else + { + $tNode = $dom->createTextNode((string)$props); + $node->appendChild($tNode); + } + + $parent->appendChild($node); + } + } +} \ No newline at end of file diff --git a/src/Ion/functions.php b/src/Ion/functions.php new file mode 100644 index 00000000..151c9772 --- /dev/null +++ b/src/Ion/functions.php @@ -0,0 +1,29 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion; + +/** + * Joins paths together. Variadic to take an + * arbitrary number of arguments + * + * @param string ...$args + * @return string + */ +function _dir(string ...$args): string +{ + return implode(DIRECTORY_SEPARATOR, $args); +} \ No newline at end of file diff --git a/tests/Ion/BaseModelTest.php b/tests/Ion/BaseModelTest.php new file mode 100644 index 00000000..e5e6ea61 --- /dev/null +++ b/tests/Ion/BaseModelTest.php @@ -0,0 +1,28 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\Model as BaseModel; + +class BaseModelTest extends Ion_TestCase { + + public function testBaseModelSanity() + { + $baseModel = new BaseModel(); + $this->assertTrue(is_object($baseModel)); + } +} \ No newline at end of file diff --git a/tests/Ion/ConfigTest.php b/tests/Ion/ConfigTest.php new file mode 100644 index 00000000..f009c575 --- /dev/null +++ b/tests/Ion/ConfigTest.php @@ -0,0 +1,151 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\Config; + +class ConfigTest extends Ion_TestCase { + + protected $config; + + public function setUp(): void + { + $this->config = new Config([ + 'foo' => 'bar', + 'asset_path' => '/assets', + 'bar' => 'baz', + 'a' => [ + 'b' => [ + 'c' => TRUE, + ], + ], + ]); + } + + public function testConfigHas(): void + { + $this->assertTrue($this->config->has('foo')); + $this->assertTrue($this->config->has(['a', 'b', 'c'])); + + $this->assertFalse($this->config->has('baz')); + $this->assertFalse($this->config->has(['c', 'b', 'a'])); + } + + public function testConfigGet(): void + { + $this->assertEquals('bar', $this->config->get('foo')); + $this->assertEquals('baz', $this->config->get('bar')); + $this->assertNull($this->config->get('baz')); + $this->assertNull($this->config->get(['apple', 'sauce', 'is'])); + } + + public function testConfigSet(): void + { + $ret = $this->config->set('foo', 'foobar'); + $this->assertInstanceOf(Config::class, $ret); + $this->assertEquals('foobar', $this->config->get('foo')); + + $this->config->set(['apple', 'sauce', 'is'], 'great'); + $apple = $this->config->get('apple'); + $this->assertEquals('great', $apple['sauce']['is'], 'Config value not set correctly'); + + $this->assertEquals('great', $this->config->get(['apple', 'sauce', 'is']), "Array argument get for config failed."); + } + + public function testConfigBadSet(): void + { + $this->expectException('InvalidArgumentException'); + $this->config->set(NULL, FALSE); + } + + public function dataConfigDelete(): array + { + return [ + 'top level delete' => [ + 'key' => 'apple', + 'assertKeys' => [ + [ + 'path' => ['apple', 'sauce', 'is'], + 'expected' => NULL + ], + [ + 'path' => ['apple', 'sauce'], + 'expected' => NULL + ], + [ + 'path' => 'apple', + 'expected' => NULL + ] + ] + ], + 'mid level delete' => [ + 'key' => ['apple', 'sauce'], + 'assertKeys' => [ + [ + 'path' => ['apple', 'sauce', 'is'], + 'expected' => NULL + ], + [ + 'path' => ['apple', 'sauce'], + 'expected' => NULL + ], + [ + 'path' => 'apple', + 'expected' => [ + 'sauce' => NULL + ] + ] + ] + ], + 'deep delete' => [ + 'key' => ['apple', 'sauce', 'is'], + 'assertKeys' => [ + [ + 'path' => ['apple', 'sauce', 'is'], + 'expected' => NULL + ], + [ + 'path' => ['apple', 'sauce'], + 'expected' => [ + 'is' => NULL + ] + ] + ] + ] + ]; + } + + /** + * @dataProvider dataConfigDelete + */ + public function testConfigDelete($key, array $assertKeys): void + { + $config = new Config([]); + $config->set(['apple', 'sauce', 'is'], 'great'); + $config->delete($key); + + foreach($assertKeys as $pair) + { + $this->assertEquals($pair['expected'], $config->get($pair['path'])); + } + } + + public function testGetNonExistentConfigItem(): void + { + $this->assertNull($this->config->get('foobar')); + } +} \ No newline at end of file diff --git a/tests/Ion/Di/ContainerAwareTest.php b/tests/Ion/Di/ContainerAwareTest.php new file mode 100644 index 00000000..89236807 --- /dev/null +++ b/tests/Ion/Di/ContainerAwareTest.php @@ -0,0 +1,55 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Di; + +use Aviat\Ion\Di\{Container, ContainerAware, ContainerInterface}; +use Aviat\Ion\Tests\Ion_TestCase; + +class Aware { + use ContainerAware; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } +} + + +class ContainerAwareTest extends Ion_TestCase { + + protected $aware; + + public function setUp(): void + { + $this->container = new Container(); + $this->aware = new Aware($this->container); + } + + public function testContainerAwareTrait(): void + { + // The container was set in setup + // check that the get method returns the same + $this->assertSame($this->container, $this->aware->getContainer()); + + $container2 = new Container([ + 'foo' => 'bar', + 'baz' => 'foobar' + ]); + $this->aware->setContainer($container2); + $this->assertSame($container2, $this->aware->getContainer()); + } +} \ No newline at end of file diff --git a/tests/Ion/Di/ContainerTest.php b/tests/Ion/Di/ContainerTest.php new file mode 100644 index 00000000..ac73416d --- /dev/null +++ b/tests/Ion/Di/ContainerTest.php @@ -0,0 +1,205 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Di; + +use Aviat\Ion\Di\{Container, ContainerAware}; +use Aviat\Ion\Di\Exception\ContainerException; +use Aviat\Ion\Tests\Ion_TestCase; +use Monolog\Logger; +use Monolog\Handler\{TestHandler, NullHandler}; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Di\Exception\NotFoundException; + +class FooTest { + + public $item; + + public function __construct($item) { + $this->item = $item; + } +} + +class FooTest2 { + use ContainerAware; +} + +class ContainerTest extends Ion_TestCase { + + public function setUp(): void + { + $this->container = new Container(); + } + + public function dataGetWithException(): array + { + return [ + 'Bad index type: number' => [ + 'id' => 42, + 'exception' => ContainerException::class, + 'message' => 'Id must be a string' + ], + 'Bad index type: array' => [ + 'id' => [], + 'exception' => ContainerException::class, + 'message' => 'Id must be a string' + ], + 'Non-existent id' => [ + 'id' => 'foo', + 'exception' => NotFoundException::class, + 'message' => "Item 'foo' does not exist in container." + ] + ]; + } + + /** + * @dataProvider dataGetWithException + */ + public function testGetWithException($id, $exception, $message): void + { + try + { + $this->container->get($id); + } + catch(ContainerException $e) + { + $this->assertInstanceOf($exception, $e); + $this->assertEquals($message, $e->getMessage()); + } + } + + /** + * @dataProvider dataGetWithException + */ + public function testGetNewWithException($id, $exception, $message): void + { + $this->expectException($exception); + $this->expectExceptionMessage($message); + $this->container->getNew($id); + } + + public function dataSetInstanceWithException(): array + { + return [ + 'Non-existent id' => [ + 'id' => 'foo', + 'exception' => NotFoundException::class, + 'message' => "Factory 'foo' does not exist in container. Set that first.", + ], + 'Non-existent id 2' => [ + 'id' => 'foobarbaz', + 'exception' => NotFoundException::class, + 'message' => "Factory 'foobarbaz' does not exist in container. Set that first.", + ], + ]; + } + + /** + * @dataProvider dataSetInstanceWithException + */ + public function testSetInstanceWithException($id, $exception, $message): void + { + try + { + $this->container->setInstance($id, NULL); + } + catch(ContainerException $e) + { + $this->assertInstanceOf($exception, $e); + $this->assertEquals($message, $e->getMessage()); + } + } + + public function testGetNew(): void + { + $this->container->set('footest', static function($item) { + return new FooTest($item); + }); + + // Check that the item is the container, if called without arguments + $footest1 = $this->container->getNew('footest'); + $this->assertInstanceOf(ContainerInterface::class, $footest1->item); + + $footest2 = $this->container->getNew('footest', ['Test String']); + $this->assertEquals('Test String', $footest2->item); + } + + public function testSetContainerInInstance(): void + { + $this->container->set('footest2', function() { + return new FooTest2(); + }); + + $footest2 = $this->container->get('footest2'); + $this->assertEquals($this->container, $footest2->getContainer()); + } + + public function testGetNewReturnCallable(): void + { + $this->container->set('footest', static function($item) { + return static function() use ($item) { + return $item; + }; + }); + + // Check that the item is the container, if called without arguments + $footest1 = $this->container->getNew('footest'); + $this->assertInstanceOf(ContainerInterface::class, $footest1()); + + $footest2 = $this->container->getNew('footest', ['Test String']); + $this->assertEquals('Test String', $footest2()); + } + + public function testGetSet(): void + { + $container = $this->container->set('foo', static function() { + return static function() {}; + }); + + $this->assertInstanceOf(Container::class, $container); + $this->assertInstanceOf(ContainerInterface::class, $container); + + // The factory returns a callable + $this->assertTrue(is_callable($container->get('foo'))); + } + + public function testLoggerMethods(): void + { + // Does the container have the default logger? + $this->assertFalse($this->container->hasLogger()); + $this->assertFalse($this->container->hasLogger('default')); + + $logger1 = new Logger('default'); + $logger2 = new Logger('testing'); + $logger1->pushHandler(new NullHandler()); + $logger2->pushHandler(new TestHandler()); + + // Set the logger channels + $container = $this->container->setLogger($logger1); + $container2 = $this->container->setLogger($logger2, 'test'); + + $this->assertInstanceOf(ContainerInterface::class, $container); + $this->assertInstanceOf(Container::class, $container2); + + $this->assertEquals($logger1, $this->container->getLogger('default')); + $this->assertEquals($logger2, $this->container->getLogger('test')); + $this->assertNull($this->container->getLogger('foo')); + + $this->assertTrue($this->container->hasLogger()); + $this->assertTrue($this->container->hasLogger('default')); + $this->assertTrue($this->container->hasLogger('test')); + } +} \ No newline at end of file diff --git a/tests/Ion/EnumTest.php b/tests/Ion/EnumTest.php new file mode 100644 index 00000000..6254e938 --- /dev/null +++ b/tests/Ion/EnumTest.php @@ -0,0 +1,83 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\Enum; + +class EnumTest extends Ion_TestCase { + + protected $expectedConstList = [ + 'FOO' => 'bar', + 'BAR' => 'foo', + 'FOOBAR' => 'baz' + ]; + + public function setUp(): void { + parent::setUp(); + $this->enum = new TestEnum(); + } + + public function testStaticGetConstList() + { + $actual = TestEnum::getConstList(); + $this->assertEquals($this->expectedConstList, $actual); + } + + public function testGetConstList() + { + $actual = $this->enum->getConstList(); + $this->assertEquals($this->expectedConstList, $actual); + } + + public function dataIsValid() + { + return [ + 'Valid' => [ + 'value' => 'baz', + 'expected' => TRUE, + 'static' => FALSE + ], + 'ValidStatic' => [ + 'value' => 'baz', + 'expected' => TRUE, + 'static' => TRUE + ], + 'Invalid' => [ + 'value' => 'foobar', + 'expected' => FALSE, + 'static' => FALSE + ], + 'InvalidStatic' => [ + 'value' => 'foobar', + 'expected' => FALSE, + 'static' => TRUE + ] + ]; + } + + /** + * @dataProvider dataIsValid + */ + public function testIsValid($value, $expected, $static) + { + $actual = ($static) + ? TestEnum::isValid($value) + : $this->enum->isValid($value); + + $this->assertEquals($expected, $actual); + } +} \ No newline at end of file diff --git a/tests/Ion/Exception/DoubleRenderExceptionTest.php b/tests/Ion/Exception/DoubleRenderExceptionTest.php new file mode 100644 index 00000000..4be0ba5d --- /dev/null +++ b/tests/Ion/Exception/DoubleRenderExceptionTest.php @@ -0,0 +1,31 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Exception; + +use Aviat\Ion\Exception\DoubleRenderException; +use Aviat\Ion\Tests\Ion_TestCase; + +class DoubleRenderExceptionTest extends Ion_TestCase { + + public function testDefaultMessage() + { + $this->expectException(DoubleRenderException::class); + $this->expectExceptionMessage('A view can only be rendered once, because headers can only be sent once.'); + + throw new DoubleRenderException(); + } +} \ No newline at end of file diff --git a/tests/Ion/FriendTest.php b/tests/Ion/FriendTest.php new file mode 100644 index 00000000..861f175a --- /dev/null +++ b/tests/Ion/FriendTest.php @@ -0,0 +1,75 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\Friend; +use Aviat\Ion\Tests\FriendTestClass; + +class FriendTest extends Ion_TestCase { + + public function setUp(): void { + parent::setUp(); + $obj = new FriendTestClass(); + $this->friend = new Friend($obj); + } + + public function testPrivateMethod() + { + $actual = $this->friend->getPrivate(); + $this->assertEquals(23, $actual); + } + + public function testProtectedMethod() + { + $actual = $this->friend->getProtected(); + $this->assertEquals(4, $actual); + } + + public function testGet() + { + $this->assertEquals(356, $this->friend->protected); + $this->assertNull($this->friend->foo); // Return NULL for non-existent properties + $this->assertEquals(47, $this->friend->parentProtected); + $this->assertEquals(84, $this->friend->grandParentProtected); + $this->assertNull($this->friend->parentPrivate); // Can't get a parent's privates + } + + public function testSet() + { + $this->friend->private = 123; + $this->assertEquals(123, $this->friend->private); + + $this->friend->foo = 32; + $this->assertNull($this->friend->foo); + } + + public function testBadInvokation() + { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Friend must be an object'); + + $friend = new Friend('foo'); + } + + public function testBadMethod() + { + $this->expectException('BadMethodCallException'); + $this->expectExceptionMessage("Method 'foo' does not exist"); + + $this->friend->foo(); + } +} \ No newline at end of file diff --git a/tests/Ion/Ion_TestCase.php b/tests/Ion/Ion_TestCase.php new file mode 100644 index 00000000..7b80da12 --- /dev/null +++ b/tests/Ion/Ion_TestCase.php @@ -0,0 +1,133 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use function Aviat\Ion\_dir; + +use PHPUnit\Framework\TestCase; +use Zend\Diactoros\ServerRequestFactory; + +define('ROOT_DIR', realpath(__DIR__ . '/../') . '/'); +define('SRC_DIR', ROOT_DIR . 'src/'); +define('TEST_DATA_DIR', __DIR__ . '/test_data'); +define('TEST_VIEW_DIR', __DIR__ . '/test_views'); + +/** + * Base class for TestCases + */ +class Ion_TestCase extends TestCase { + // Test directory constants + public const ROOT_DIR = ROOT_DIR; + public const SRC_DIR = SRC_DIR; + public const TEST_DATA_DIR = TEST_DATA_DIR; + public const TEST_VIEW_DIR = TEST_VIEW_DIR; + + protected $container; + protected static $staticContainer; + protected static $session_handler; + + /*public static function setUpBeforeClass() + { + // Use mock session handler + $session_handler = new TestSessionHandler(); + session_set_save_handler($session_handler, TRUE); + self::$session_handler = $session_handler; + }*/ + + public function setUp(): void + { + parent::setUp(); + + $ROOT_DIR = realpath(_dir(__DIR__, '/../')); + $APP_DIR = _dir($ROOT_DIR, 'app'); + + $config_array = [ + 'asset_path' => '//localhost/assets/', + 'img_cache_path' => _dir(ROOT_DIR, 'public/images'), + 'database' => [ + 'collection' => [ + 'type' => 'sqlite', + 'host' => '', + 'user' => '', + 'pass' => '', + 'port' => '', + 'name' => 'default', + 'database' => '', + 'file' => ':memory:', + ], + 'cache' => [ + 'type' => 'sqlite', + 'host' => '', + 'user' => '', + 'pass' => '', + 'port' => '', + 'name' => 'default', + 'database' => '', + 'file' => ':memory:', + ] + ], + 'routes' => [ + 'route_config' => [ + 'asset_path' => '/assets' + ], + 'routes' => [ + + ] + ], + 'redis' => [ + 'host' => (array_key_exists('REDIS_HOST', $_ENV)) ? $_ENV['REDIS_HOST'] : 'localhost', + 'database' => 13 + ] + ]; + + // Set up DI container + $di = require('di.php'); + $container = $di($config_array); + $container->set('session-handler', static function() { + // Use mock session handler + $session_handler = new TestSessionHandler(); + session_set_save_handler($session_handler, TRUE); + return $session_handler; + }); + + $this->container = $container; + } + + /** + * Set arbitrary superglobal values for testing purposes + * + * @param array $supers + * @return void + */ + public function setSuperGlobals(array $supers = []): void + { + $default = [ + '_SERVER' => $_SERVER, + '_GET' => $_GET, + '_POST' => $_POST, + '_COOKIE' => $_COOKIE, + '_FILES' => $_FILES + ]; + + $request = call_user_func_array( + [ServerRequestFactory::class, 'fromGlobals'], + array_merge($default, $supers) + ); + $this->container->setInstance('request', $request); + } +} +// End of Ion_TestCase.php \ No newline at end of file diff --git a/tests/Ion/JsonTest.php b/tests/Ion/JsonTest.php new file mode 100644 index 00000000..cc45dcf7 --- /dev/null +++ b/tests/Ion/JsonTest.php @@ -0,0 +1,89 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use function Aviat\Ion\_dir; + +use Aviat\Ion\{Json, JsonException}; + +class JsonTest extends Ion_TestCase { + + public function testEncode() + { + $data = (object) [ + 'foo' => [1, 2, 3, 4] + ]; + $expected = '{"foo":[1,2,3,4]}'; + $this->assertEquals($expected, Json::encode($data)); + } + + public function dataEncodeDecode() + { + return [ + 'set1' => [ + 'data' => [ + 'apple' => [ + 'sauce' => ['foo','bar','baz'] + ] + ], + 'expected_size' => 39, + 'expected_json' => '{"apple":{"sauce":["foo","bar","baz"]}}' + ] + ]; + } + + /** + * @dataProvider dataEncodeDecode + */ + public function testEncodeDecodeFile($data, $expected_size, $expected_json) + { + $target_file = _dir(self::TEST_DATA_DIR, 'json_write.json'); + + $actual_size = Json::encodeFile($target_file, $data); + $actual_json = file_get_contents($target_file); + + $this->assertTrue(Json::isJson($actual_json)); + $this->assertEquals($expected_size, $actual_size); + $this->assertEquals($expected_json, $actual_json); + + $this->assertEquals($data, Json::decodeFile($target_file)); + + unlink($target_file); + } + + public function testDecode() + { + $json = '{"foo":[1,2,3,4]}'; + $expected = [ + 'foo' => [1, 2, 3, 4] + ]; + $this->assertEquals($expected, Json::decode($json)); + $this->assertEquals((object)$expected, Json::decode($json, false)); + + $badJson = '{foo:{1|2}}'; + $this->expectException('Aviat\Ion\JsonException'); + $this->expectExceptionMessage('JSON_ERROR_SYNTAX - Syntax error'); + $this->expectExceptionCode(JSON_ERROR_SYNTAX); + + Json::decode($badJson); + } + + public function testDecodeNull() + { + $this->assertNull(Json::decode(NULL)); + } +} \ No newline at end of file diff --git a/tests/Ion/Model/BaseDBModelTest.php b/tests/Ion/Model/BaseDBModelTest.php new file mode 100644 index 00000000..f72ede4b --- /dev/null +++ b/tests/Ion/Model/BaseDBModelTest.php @@ -0,0 +1,29 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Model; + +use Aviat\Ion\Model\DB as BaseDBModel; +use Aviat\Ion\Tests\Ion_TestCase; + +class BaseDBModelTest extends Ion_TestCase { + + public function testBaseDBModelSanity() + { + $baseDBModel = new BaseDBModel($this->container->get('config')); + $this->assertTrue(is_object($baseDBModel)); + } +} \ No newline at end of file diff --git a/tests/Ion/StringWrapperTest.php b/tests/Ion/StringWrapperTest.php new file mode 100644 index 00000000..d45ae590 --- /dev/null +++ b/tests/Ion/StringWrapperTest.php @@ -0,0 +1,39 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\StringWrapper; +use Aviat\Ion\Type\StringType; +use PHPUnit\Framework\TestCase; + +class StringWrapperTest extends TestCase { + + protected $wrapper; + + public function setUp(): void { + $this->wrapper = new class { + use StringWrapper; + }; + } + + public function testString() + { + $str = $this->wrapper->string('foo'); + $this->assertInstanceOf(StringType::class, $str); + } + +} \ No newline at end of file diff --git a/tests/Ion/TestSessionHandler.php b/tests/Ion/TestSessionHandler.php new file mode 100644 index 00000000..59acac99 --- /dev/null +++ b/tests/Ion/TestSessionHandler.php @@ -0,0 +1,71 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use SessionHandlerInterface; + +class TestSessionHandler implements SessionHandlerInterface { + + public $data = []; + public $save_path = './test_data/sessions'; + + public function close() + { + return TRUE; + } + + public function destroy($id) + { + $file = "$this->save_path/$id"; + if (file_exists($file)) + { + @unlink($file); + } + $this->data[$id] = []; + return TRUE; + } + + public function gc($maxLifetime) + { + return TRUE; + } + + public function open($save_path, $name) + { + /*if ( ! array_key_exists($save_path, $this->data)) + { + $this->save_path = $save_path; + $this->data = []; + }*/ + return TRUE; + } + + public function read($id) + { + return json_decode(@file_get_contents("$this->save_path/$id"), TRUE); + } + + public function write($id, $data) + { + $file = "$this->save_path/$id"; + file_put_contents($file, json_encode($data)); + + return TRUE; + } + +} +// End of TestSessionHandler.php \ No newline at end of file diff --git a/tests/Ion/Transformer/AbstractTransformerTest.php b/tests/Ion/Transformer/AbstractTransformerTest.php new file mode 100644 index 00000000..0e6adf97 --- /dev/null +++ b/tests/Ion/Transformer/AbstractTransformerTest.php @@ -0,0 +1,157 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Transformer; + +use Aviat\Ion\Tests\Ion_TestCase; +use Aviat\Ion\Tests\{TestTransformer, TestTransformerUntransform}; + +class AbstractTransformerTest extends Ion_TestCase { + + protected $transformer; + protected $untransformer; + + + public function setUp(): void { + $this->transformer = new TestTransformer(); + $this->untransformer = new TestTransformerUntransform(); + } + + public function dataTransformCollection() + { + return [ + 'object' => [ + 'original' => [ + (object)[ + ['name' => 'Comedy'], + ['name' => 'Romance'], + ['name' => 'School'], + ['name' => 'Harem'] + ], + (object)[ + ['name' => 'Action'], + ['name' => 'Comedy'], + ['name' => 'Magic'], + ['name' => 'Fantasy'], + ['name' => 'Mahou Shoujo'] + ], + (object)[ + ['name' => 'Comedy'], + ['name' => 'Sci-Fi'] + ] + ], + 'expected' => [ + ['Comedy', 'Romance', 'School', 'Harem'], + ['Action', 'Comedy', 'Magic', 'Fantasy', 'Mahou Shoujo'], + ['Comedy', 'Sci-Fi'] + ] + ], + 'array' => [ + 'original' => [ + [ + ['name' => 'Comedy'], + ['name' => 'Romance'], + ['name' => 'School'], + ['name' => 'Harem'] + ], + [ + ['name' => 'Action'], + ['name' => 'Comedy'], + ['name' => 'Magic'], + ['name' => 'Fantasy'], + ['name' => 'Mahou Shoujo'] + ], + [ + ['name' => 'Comedy'], + ['name' => 'Sci-Fi'] + ] + ], + 'expected' => [ + ['Comedy', 'Romance', 'School', 'Harem'], + ['Action', 'Comedy', 'Magic', 'Fantasy', 'Mahou Shoujo'], + ['Comedy', 'Sci-Fi'] + ] + ], + ]; + } + + public function dataUnTransformCollection() + { + return [ + 'object' => [ + 'original' => [ + (object)['Comedy', 'Romance', 'School', 'Harem'], + (object)['Action', 'Comedy', 'Magic', 'Fantasy', 'Mahou Shoujo'], + (object)['Comedy', 'Sci-Fi'] + ], + 'expected' => [ + ['Comedy', 'Romance', 'School', 'Harem'], + ['Action', 'Comedy', 'Magic', 'Fantasy', 'Mahou Shoujo'], + ['Comedy', 'Sci-Fi'] + ] + ], + 'array' => [ + 'original' => [ + ['Comedy', 'Romance', 'School', 'Harem'], + ['Action', 'Comedy', 'Magic', 'Fantasy', 'Mahou Shoujo'], + ['Comedy', 'Sci-Fi'] + ], + 'expected' => [ + ['Comedy', 'Romance', 'School', 'Harem'], + ['Action', 'Comedy', 'Magic', 'Fantasy', 'Mahou Shoujo'], + ['Comedy', 'Sci-Fi'] + ] + ] + ]; + } + + public function testTransform() + { + $data = $this->dataTransformCollection(); + $original = $data['object']['original'][0]; + $expected = $data['object']['expected'][0]; + + $actual = $this->transformer->transform($original); + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider dataTransformCollection + */ + public function testTransformCollection($original, $expected) + { + $actual = $this->transformer->transformCollection($original); + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider dataUnTransformCollection + */ + public function testUntransformCollection($original, $expected) + { + $actual = $this->untransformer->untransformCollection($original); + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider dataUnTransformCollection + */ + public function testUntransformCollectionWithException($original, $expected) + { + $this->expectException(\BadMethodCallException::class); + $this->transformer->untransformCollection($original); + } +} \ No newline at end of file diff --git a/tests/Ion/Type/ArrayTypeTest.php b/tests/Ion/Type/ArrayTypeTest.php new file mode 100644 index 00000000..fbbc96da --- /dev/null +++ b/tests/Ion/Type/ArrayTypeTest.php @@ -0,0 +1,215 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Type; + +use Aviat\Ion\ArrayWrapper; +use Aviat\Ion\Tests\Ion_TestCase; + +class ArrayTypeTest extends Ion_TestCase { + use ArrayWrapper; + + public function setUp(): void { + parent::setUp(); + } + + public function dataCall() + { + $method_map = [ + 'chunk' => 'array_chunk', + 'pluck' => 'array_column', + 'assoc_diff' => 'array_diff_assoc', + 'key_diff' => 'array_diff_key', + 'diff' => 'array_diff', + 'filter' => 'array_filter', + 'flip' => 'array_flip', + 'intersect' => 'array_intersect', + 'keys' => 'array_keys', + 'merge' => 'array_merge', + 'pad' => 'array_pad', + 'random' => 'array_rand', + 'reduce' => 'array_reduce', + ]; + + return [ + 'array_merge' => [ + 'method' => 'merge', + 'array' => [1, 3, 5, 7], + 'args' => [[2, 4, 6, 8]], + 'expected' => [1, 3, 5, 7, 2, 4, 6, 8] + ], + 'array_product' => [ + 'method' => 'product', + 'array' => [1, 2, 3], + 'args' => [], + 'expected' => 6 + ], + 'array_reverse' => [ + 'method' => 'reverse', + 'array' => [1, 2, 3, 4, 5], + 'args' => [], + 'expected' => [5, 4, 3, 2, 1] + ], + 'array_sum' => [ + 'method' => 'sum', + 'array' => [1, 2, 3, 4, 5, 6], + 'args' => [], + 'expected' => 21 + ], + 'array_unique' => [ + 'method' => 'unique', + 'array' => [1, 1, 3, 2, 2, 2, 3, 3, 5], + 'args' => [SORT_REGULAR], + 'expected' => [0 => 1, 2 => 3, 3 => 2, 8 => 5] + ], + 'array_values' => [ + 'method' => 'values', + 'array' => ['foo' => 'bar', 'baz' => 'foobar'], + 'args' => [], + 'expected' => ['bar', 'foobar'] + ] + ]; + } + + /** + * Test the array methods defined for the __Call method + * + * @dataProvider dataCall + */ + public function testCall($method, $array, $args, $expected) + { + $obj = $this->arr($array); + $actual = $obj->__call($method, $args); + $this->assertEquals($expected, $actual); + } + + public function testSet() + { + $obj = $this->arr([]); + $arraytype = $obj->set('foo', 'bar'); + + $this->assertInstanceOf('Aviat\Ion\Type\ArrayType', $arraytype); + $this->assertEquals('bar', $obj->get('foo')); + } + + public function testGet() + { + $array = [1, 2, 3, 4, 5]; + $obj = $this->arr($array); + $this->assertEquals($array, $obj->get()); + $this->assertEquals(1, $obj->get(0)); + $this->assertEquals(5, $obj->get(4)); + } + + public function testGetDeepKey() + { + $arr = [ + 'foo' => 'bar', + 'baz' => [ + 'bar' => 'foobar' + ] + ]; + $obj = $this->arr($arr); + $this->assertEquals('foobar', $obj->getDeepKey(['baz', 'bar'])); + $this->assertNull($obj->getDeepKey(['foo', 'bar', 'baz'])); + } + + public function testMap() + { + $obj = $this->arr([1, 2, 3]); + $actual = $obj->map(function($item) { + return $item * 2; + }); + + $this->assertEquals([2, 4, 6], $actual); + } + + public function testBadCall() + { + $obj = $this->arr([]); + + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage("Method 'foo' does not exist"); + + $obj->foo(); + } + + public function testShuffle() + { + $original = [1, 2, 3, 4]; + $test = [1, 2, 3, 4]; + $obj = $this->arr($test); + $actual = $obj->shuffle(); + + //$this->assertNotEquals($actual, $original); + $this->assertTrue(is_array($actual)); + } + + public function testHasKey() + { + $obj = $this->arr([ + 'a' => 'b', + 'z' => 'y' + ]); + $this->assertTrue($obj->hasKey('a')); + $this->assertFalse($obj->hasKey('b')); + } + + public function testHasKeyArray() + { + $obj = $this->arr([ + 'foo' => [ + 'bar' => [ + 'baz' => [ + 'foobar' => NULL, + 'one' => 1, + ], + ], + ], + ]); + + $this->assertTrue($obj->hasKey(['foo'])); + $this->assertTrue($obj->hasKey(['foo', 'bar'])); + $this->assertTrue($obj->hasKey(['foo', 'bar', 'baz'])); + $this->assertTrue($obj->hasKey(['foo', 'bar', 'baz', 'one'])); + $this->assertTrue($obj->hasKey(['foo', 'bar', 'baz', 'foobar'])); + + $this->assertFalse($obj->hasKey(['foo', 'baz'])); + $this->assertFalse($obj->hasKey(['bar', 'baz'])); + } + + public function testHas() + { + $obj = $this->arr([1, 2, 6, 8, 11]); + $this->assertTrue($obj->has(8)); + $this->assertFalse($obj->has(8745)); + } + + public function testSearch() + { + $obj = $this->arr([1, 2, 5, 7, 47]); + $actual = $obj->search(47); + $this->assertEquals(4, $actual); + } + + public function testFill() + { + $obj = $this->arr([]); + $expected = ['?', '?', '?']; + $actual = $obj->fill(0, 3, '?'); + $this->assertEquals($actual, $expected); + } +} \ No newline at end of file diff --git a/tests/Ion/Type/StringTypeTest.php b/tests/Ion/Type/StringTypeTest.php new file mode 100644 index 00000000..4872108b --- /dev/null +++ b/tests/Ion/Type/StringTypeTest.php @@ -0,0 +1,66 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\Type; + +use Aviat\Ion\StringWrapper; +use Aviat\Ion\Tests\Ion_TestCase; + +class StringTypeTest extends Ion_TestCase { + use StringWrapper; + + + public function dataFuzzyCaseMatch() + { + return [ + 'space separated' => [ + 'str1' => 'foo bar baz', + 'str2' => 'foo-bar-baz', + 'expected' => true + ], + 'camelCase' => [ + 'str1' => 'fooBarBaz', + 'str2' => 'foo-bar-baz', + 'expected' => true + ], + 'PascalCase' => [ + 'str1' => 'FooBarBaz', + 'str2' => 'foo-bar-baz', + 'expected' => true + ], + 'snake_case' => [ + 'str1' => 'foo_bar_baz', + 'str2' => 'foo-bar-baz', + 'expected' => true + ], + 'mEsSYcAse' => [ + 'str1' => 'fOObArBAZ', + 'str2' => 'foo-bar-baz', + 'expected' => false + ], + ]; + } + + /** + * @dataProvider dataFuzzyCaseMatch + */ + public function testFuzzyCaseMatch($str1, $str2, $expected) + { + $actual = $this->string($str1)->fuzzyCaseMatch($str2); + $this->assertEquals($expected, $actual); + } + +} \ No newline at end of file diff --git a/tests/Ion/View/HtmlViewTest.php b/tests/Ion/View/HtmlViewTest.php new file mode 100644 index 00000000..d31a3037 --- /dev/null +++ b/tests/Ion/View/HtmlViewTest.php @@ -0,0 +1,42 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\View; + +use function Aviat\Ion\_dir; + +use Aviat\Ion\Tests\TestHtmlView; + +class HtmlViewTest extends HttpViewTest { + + protected $template_path; + + public function setUp(): void { + parent::setUp(); + $this->view = new TestHtmlView($this->container); + } + + public function testRenderTemplate() + { + $path = _dir(self::TEST_VIEW_DIR, 'test_view.php'); + $expected = 'foo'; + $actual = $this->view->renderTemplate($path, [ + 'var' => 'foo' + ]); + $this->assertEquals($expected, $actual); + } + +} \ No newline at end of file diff --git a/tests/Ion/View/HttpViewTest.php b/tests/Ion/View/HttpViewTest.php new file mode 100644 index 00000000..8305e9b2 --- /dev/null +++ b/tests/Ion/View/HttpViewTest.php @@ -0,0 +1,95 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\View; + +use Aviat\Ion\Friend; +use Aviat\Ion\Exception\DoubleRenderException; +use Aviat\Ion\Tests\Ion_TestCase; +use Aviat\Ion\Tests\TestHttpView; + +class HttpViewTest extends Ion_TestCase { + + protected $view; + protected $friend; + + public function setUp(): void { + parent::setUp(); + $this->view = new TestHttpView($this->container); + $this->friend = new Friend($this->view); + } + + public function testGetOutput() + { + $this->friend->setOutput('foo'); + $this->assertEquals('foo', $this->friend->getOutput()); + $this->assertFalse($this->friend->hasRendered); + + $this->assertEquals($this->friend->getOutput(), $this->friend->__toString()); + $this->assertTrue($this->friend->hasRendered); + } + + public function testSetOutput() + { + $same = $this->view->setOutput('

'); + $this->assertEquals($same, $this->view); + $this->assertEquals('

', $this->view->getOutput()); + } + + public function testAppendOutput() + { + $this->view->setOutput('

'); + $this->view->appendOutput('

'); + $this->assertEquals('

', $this->view->getOutput()); + } + + public function testSetStatusCode() + { + $view = $this->view->setStatusCode(404); + $this->assertEquals(404, $view->response->getStatusCode()); + } + + public function testAddHeader() + { + $view = $this->view->addHeader('foo', 'bar'); + $this->assertTrue($view->response->hasHeader('foo')); + $this->assertEquals(['bar'], $view->response->getHeader('foo')); + } + + public function testSendDoubleRenderException() + { + $this->expectException(DoubleRenderException::class); + $this->expectExceptionMessage('A view can only be rendered once, because headers can only be sent once.'); + + // First render + $this->view->__toString(); + + // Second render + $this->view->send(); + } + + public function test__toStringDoubleRenderException() + { + $this->expectException(DoubleRenderException::class); + $this->expectExceptionMessage('A view can only be rendered once, because headers can only be sent once.'); + + // First render + $this->view->send(); + + // Second render + $this->view->__toString(); + } +} \ No newline at end of file diff --git a/tests/Ion/View/JsonViewTest.php b/tests/Ion/View/JsonViewTest.php new file mode 100644 index 00000000..2afd8a84 --- /dev/null +++ b/tests/Ion/View/JsonViewTest.php @@ -0,0 +1,57 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests\View; + +use Aviat\Ion\Friend; +use Aviat\Ion\Tests\TestJsonView; + +class JsonViewTest extends HttpViewTest { + + public function setUp(): void { + parent::setUp(); + + $this->view = new TestJsonView($this->container); + $this->friend = new Friend($this->view); + } + + public function testSetOutputJSON() + { + // Extend view class to remove destructor which does output + $view = new TestJsonView($this->container); + + // Json encode non-string + $content = ['foo' => 'bar']; + $expected = json_encode($content); + $view->setOutput($content); + $this->assertEquals($expected, $this->view->getOutput()); + } + + public function testSetOutput() + { + // Directly set string + $view = new TestJsonView($this->container); + $content = '{}'; + $expected = '{}'; + $view->setOutput($content); + $this->assertEquals($expected, $view->getOutput()); + } + + public function testOutput() + { + $this->assertEquals('application/json', $this->friend->contentType); + } +} \ No newline at end of file diff --git a/tests/Ion/XMLTest.php b/tests/Ion/XMLTest.php new file mode 100644 index 00000000..a4e6774a --- /dev/null +++ b/tests/Ion/XMLTest.php @@ -0,0 +1,88 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\XML; +use PHPUnit\Framework\TestCase; + +class XMLTest extends TestCase { + + protected $xml; + protected $expectedXml; + protected $object; + protected $array; + + public function setUp(): void { + $this->xml = file_get_contents(__DIR__ . '/test_data/XML/xmlTestFile.xml'); + $this->expectedXml = file_get_contents(__DIR__ . '/test_data/XML/minifiedXmlTestFile.xml'); + + $this->array = [ + 'entry' => [ + 'foo' => [ + 'bar' => [ + 'baz' => 42 + ] + ], + 'episode' => '11', + 'status' => 'watching', + 'score' => '7', + 'storage_type' => '1', + 'storage_value' => '2.5', + 'times_rewatched' => '1', + 'rewatch_value' => '3', + 'date_start' => '01152015', + 'date_finish' => '10232016', + 'priority' => '2', + 'enable_discussion' => '0', + 'enable_rewatching' => '1', + 'comments' => 'Should you say something?', + 'tags' => 'test tag, 2nd tag' + ] + ]; + + $this->object = new XML(); + } + + public function testToArray() + { + $this->assertEquals($this->array, XML::toArray($this->xml)); + } + + public function testParse() + { + $this->object->setXML($this->xml); + $this->assertEquals($this->array, $this->object->parse()); + } + + public function testToXML() + { + $this->assertEquals($this->expectedXml, XML::toXML($this->array)); + } + + public function testCreateXML() + { + $this->object->setData($this->array); + $this->assertEquals($this->expectedXml, $this->object->createXML()); + } + + public function testToString() + { + $this->object->setData($this->array); + $this->assertEquals($this->expectedXml, $this->object->__toString()); + $this->assertEquals($this->expectedXml, (string)$this->object); + } +} \ No newline at end of file diff --git a/tests/Ion/bootstrap.php b/tests/Ion/bootstrap.php new file mode 100644 index 00000000..5cd54140 --- /dev/null +++ b/tests/Ion/bootstrap.php @@ -0,0 +1,41 @@ +set('config', static function() { + return new Config([]); + }); + + $container->setInstance('config', new Config($config_array)); + + $container->set('request', static function() { + return ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES + ); + }); + + $container->set('response', static function() { + return new Response(); + }); + + // Create session Object + $container->set('session', static function() { + return (new SessionFactory())->newInstance($_COOKIE); + }); + + // Create Html helper Object + $container->set('html-helper', static function() { + return (new HelperLocatorFactory)->newInstance(); + }); + + return $container; +}; \ No newline at end of file diff --git a/tests/Ion/functionsTest.php b/tests/Ion/functionsTest.php new file mode 100644 index 00000000..c567fd15 --- /dev/null +++ b/tests/Ion/functionsTest.php @@ -0,0 +1,33 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use function Aviat\Ion\_dir; + +use PHPUnit\Framework\TestCase; + +class functionsTest extends TestCase { + + + public function test_dir() + { + $args = ['foo', 'bar', 'baz']; + $expected = implode(\DIRECTORY_SEPARATOR, $args); + + $this->assertEquals(_dir(...$args), $expected); + } +} \ No newline at end of file diff --git a/tests/Ion/mocks.php b/tests/Ion/mocks.php new file mode 100644 index 00000000..83864d5c --- /dev/null +++ b/tests/Ion/mocks.php @@ -0,0 +1,191 @@ + + * @copyright 2015 - 2019 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 3.0.0 + * @link https://git.timshomepage.net/aviat/ion + */ + +namespace Aviat\Ion\Tests; + +use Aviat\Ion\Enum; +use Aviat\Ion\Exception\DoubleRenderException; +use Aviat\Ion\Friend; +use Aviat\Ion\Transformer\AbstractTransformer; +use Aviat\Ion\View; +use Aviat\Ion\View\{HtmlView, HttpView, JsonView}; + +// ----------------------------------------------------------------------------- +// Mock the default error handler +// ----------------------------------------------------------------------------- + +class MockErrorHandler { + public function addDataTable($name, array $values=[]) {} +} + +// ----------------------------------------------------------------------------- +// Ion Mocks +// ----------------------------------------------------------------------------- + +class TestEnum extends Enum { + const FOO = 'bar'; + const BAR = 'foo'; + const FOOBAR = 'baz'; +} + +class FriendGrandParentTestClass { + protected $grandParentProtected = 84; +} + +class FriendParentTestClass extends FriendGrandParentTestClass { + protected $parentProtected = 47; + private $parentPrivate = 654; +} + +class FriendTestClass extends FriendParentTestClass { + protected $protected = 356; + private $private = 486; + + protected function getProtected() + { + return 4; + } + + private function getPrivate() + { + return 23; + } +} + +class TestTransformer extends AbstractTransformer { + + public function transform($item) + { + $out = []; + $genre_list = (array) $item; + + foreach($genre_list as $genre) + { + $out[] = $genre['name']; + } + + return $out; + } +} + +class TestTransformerUntransform extends TestTransformer { + public function untransform($item) + { + return (array)$item; + } +} + +trait MockViewOutputTrait { + /*protected function output() { + $reflect = new ReflectionClass($this); + $properties = $reflect->getProperties(); + $props = []; + + foreach($properties as $reflectProp) + { + $reflectProp->setAccessible(TRUE); + $props[$reflectProp->getName()] = $reflectProp->getValue($this); + } + + $view = new TestView($this->container); + $friend = new Friend($view); + foreach($props as $name => $val) + { + $friend->__set($name, $val); + } + + //$friend->output(); + }*/ + + public function send(): void + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + + $this->hasRendered = TRUE; + } +} + +class TestView extends View { + public function send(): void + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + + $this->hasRendered = TRUE; + } + public function output() {} +} + +class TestHtmlView extends HtmlView { + protected function output(): void + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + + $this->hasRendered = TRUE; + } +} + +class TestHttpView extends HttpView { + protected function output(): void + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + + $this->hasRendered = TRUE; + } +} + +class TestJsonView extends JsonView { + public function __destruct() {} + + protected function output(): void + { + if ($this->hasRendered) + { + throw new DoubleRenderException(); + } + + $this->hasRendered = TRUE; + } +} + +// ----------------------------------------------------------------------------- +// AnimeClient Mocks +// ----------------------------------------------------------------------------- + +trait MockInjectionTrait { + public function __get($key) + { + return $this->$key; + } + + public function __set($key, $value) + { + $this->$key = $value; + return $this; + } +} +// End of mocks.php \ No newline at end of file diff --git a/tests/Ion/test_data/XML/minifiedXmlTestFile.xml b/tests/Ion/test_data/XML/minifiedXmlTestFile.xml new file mode 100644 index 00000000..c0490c7c --- /dev/null +++ b/tests/Ion/test_data/XML/minifiedXmlTestFile.xml @@ -0,0 +1,2 @@ + +4211watching712.5130115201510232016201Should you say something?test tag, 2nd tag diff --git a/tests/Ion/test_data/XML/xmlTestFile.xml b/tests/Ion/test_data/XML/xmlTestFile.xml new file mode 100644 index 00000000..b37cf964 --- /dev/null +++ b/tests/Ion/test_data/XML/xmlTestFile.xml @@ -0,0 +1,22 @@ + + + + + 42 + + + 11 + watching + 7 + 1 + 2.5 + 1 + 3 + 01152015 + 10232016 + 2 + 0 + 1 + Should you say something? + test tag, 2nd tag + \ No newline at end of file diff --git a/tests/Ion/test_data/invalid_json.json b/tests/Ion/test_data/invalid_json.json new file mode 100644 index 00000000..8a3cefa6 --- /dev/null +++ b/tests/Ion/test_data/invalid_json.json @@ -0,0 +1 @@ +[}] \ No newline at end of file diff --git a/tests/Ion/test_data/valid_json.json b/tests/Ion/test_data/valid_json.json new file mode 100644 index 00000000..56b0789b --- /dev/null +++ b/tests/Ion/test_data/valid_json.json @@ -0,0 +1,5 @@ +[{ + "foo": { + "bar": [1,2,3] + } +}] \ No newline at end of file diff --git a/tests/Ion/test_views/test_view.php b/tests/Ion/test_views/test_view.php new file mode 100644 index 00000000..3b7e2f71 --- /dev/null +++ b/tests/Ion/test_views/test_view.php @@ -0,0 +1 @@ + \ No newline at end of file