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+', '$1><', $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 = '