<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles;
/**
 * Represents the root path within a project.
 *
 * The root path is defined as the path in which the whole application resides.
 * In case the application is inside a phar, it's the directory where the phar
 * is stored.
 *
 * @since  4.0.0
 * @Singleton
 */
class Rootpath
{
    /**
     * root path of application
     *
     * @type  string
     */
    private $rootpath;

    /**
     * constructor
     *
     * If no root path is given it tries to detect it automatically. Detection
     * works the following way:
     * - In case the application is inside a phar, it's the directory where the
     *   phar is stored.
     * - Try to locate the vendor/autoload.php file generated by Composer, and
     *   go up one above to vendor/..
     *
     * @param   string  $rootpath  optional  path to root
     * @throws  \InvalidArgumentException  in case a root path is given but does not exist
     */
    public function __construct($rootpath = null)
    {
        if (null !== $rootpath && !file_exists($rootpath)) {
            throw new \InvalidArgumentException(
                    'Given rootpath "' . $rootpath . '" does not exist'
            );
        }

        $this->rootpath = (null === $rootpath) ? ($this->detectRootPath()) : ($this->realpath($rootpath));
    }

    /**
     * creates realpath from given path
     *
     * The native function is not directly used as it doesn't work with
     * vfsStream URIs and would prevent mocking the root path in tests.
     *
     * @param   string  $path
     * @return  string
     */
    private function realpath($path)
    {
        if (substr($path, 0, 6) === 'vfs://') {
            return $path;
        }

        return realpath($path);
    }

    /**
     * casts given value to an instance of Rootpath
     *
     * @param   string|\stubbles\lang\Rootpath  $rootpath
     * @return  \stubbles\lang\Rootpath
     */
    public static function castFrom($rootpath)
    {
        if ($rootpath instanceof self) {
            return $rootpath;
        }

        return new self($rootpath);
    }

    /**
     * returns root path
     *
     * @return  string
     */
    public function __toString()
    {
        return $this->rootpath;
    }

    /**
     * returns absolute path to given local path
     *
     * Supports arbitrary lists of arguments, e.g.
     * <code>$rootpath->to('src', 'main', 'php', 'Example.php')</code>
     * will return "path/to/root/src/main/php/Example.php".
     *
     * @param   string...  $pathParts
     * @return  string
     */
    public function to(...$pathParts)
    {
        return $this->rootpath . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $pathParts);
    }

    /**
     * checks if given path is located within root path
     *
     * @param   string  $path
     * @return  bool
     */
    public function contains($path)
    {
        $realpath = realpath($path);
        if (false === $realpath) {
            return false;
        }

        return substr($realpath, 0, strlen($this->rootpath)) === $this->rootpath;
    }

    /**
     * returns list of source pathes defined for autoloader
     *
     * Relies on autoloader generated by Composer. If no such autoloader is
     * present the list of source pathes will be empty.
     *
     * @return  string[]
     */
    public function sourcePathes()
    {
        $vendorPathes = [];
        foreach (array_merge($this->loadPsr0Pathes(), $this->loadPsr4Pathes()) as $pathes) {
            if (is_array($pathes)) {
                $vendorPathes = array_merge($vendorPathes, $pathes);
            } else {
                $vendorPathes[] = $pathes;
            }
        }

        return $vendorPathes;
    }

    /**
     * loads list of pathes defined via PSR-0
     *
     * @return  string[]
     */
    private function loadPsr0Pathes()
    {
        if (file_exists($this->rootpath . '/vendor/composer/autoload_namespaces.php')) {
            return require $this->rootpath . '/vendor/composer/autoload_namespaces.php';
        }

        return [];
    }

    /**
     * loads list of pathes defined via PSR-4
     *
     * @return  string[]
     */
    private function loadPsr4Pathes()
    {
        if (file_exists($this->rootpath . '/vendor/composer/autoload_psr4.php')) {
            return require $this->rootpath . '/vendor/composer/autoload_psr4.php';
        }

        return [];
    }

    /**
     * detects root path
     *
     * @return  string
     */
    private function detectRootPath()
    {
        static $rootpath = null;
        if (null === $rootpath) {
            if (\Phar::running() !== '') {
                $rootpath = dirname(\Phar::running(false));
            } elseif (file_exists(__DIR__ . '/../../../../../autoload.php')) {
                // stubbles/core is inside the vendor dir of the application
                // it is a dependency of
                $rootpath = realpath(__DIR__ . '/../../../../../../');
            } else {
                // local checkout while development
                $rootpath = realpath(__DIR__ . '/../../../');
            }
        }

        return $rootpath;
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\ioc\binding;
use stubbles\ioc\Injector;
use stubbles\values\Properties;
/**
 * Provides properties, partially based on current runtime mode.
 *
 * @since  3.4.0
 */
class PropertyBinding implements Binding
{
    /**
     * This string is used when generating the key for a constant binding.
     */
    const TYPE             = '__PROPERTY__';
    /**
     * actual properties
     *
     * @type  \stubbles\values\Properties
     */
    private $properties;
    /**
     * current environment
     *
     * @type  string
     */
    private $environment;

    /**
     * constructor
     *
     * @param  \stubbles\values\Properties  $properties
     * @param  string                       $environment  current environment
     */
    public function __construct(Properties $properties, $environment)
    {
        $this->properties  = $properties;
        $this->environment = $environment;
    }

    /**
     * checks if property with given name exists
     *
     * @param   string  $name
     * @return  bool
     */
    public function hasProperty($name)
    {
        if ($this->properties->containValue($this->environment, $name)) {
            return true;
        }

        return $this->properties->containValue('config', $name);
    }

    /**
     * returns the created instance
     *
     * @param   \stubbles\ioc\Injector  $injector
     * @param   string                  $name
     * @return  mixed
     * @throws  \stubbles\ioc\binding\BindingException
     */
    public function getInstance(Injector $injector, $name)
    {
        if ($this->properties->containValue($this->environment, $name)) {
            return $this->properties->parseValue($this->environment, $name);
        }

        if ($this->properties->containValue('config', $name)) {
            return $this->properties->parseValue('config', $name);
        }

        throw new BindingException('Missing property ' . $name . ' for environment ' . $this->environment);
    }

    /**
     * creates a unique key for this binding
     *
     * @return  string
     */
    public function getKey()
    {
        return self::TYPE;
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\ioc\binding;
use stubbles\ioc\InjectionProvider;
use stubbles\ioc\binding\BindingScope;
/**
 * Interface for session storages.
 *
 * @since  5.4.0
 */
class SessionBindingScope implements BindingScope
{
    /**
     * session prefix key
     */
    const SESSION_KEY  = 'stubbles.ioc.session.scope#';
    /**
     * session instance to store instances in
     *
     * @type  \stubbles\ioc\binding\Session
     */
    private $session;

    /**
     * sets actual session
     *
     * @param   \stubbles\ioc\binding\Session  $session
     * @return  \stubbles\ioc\binding\SessionBindingScope
     */
    public function setSession(Session $session)
    {
        $this->session = $session;
        return $this;
    }

    /**
     * returns the requested instance from the scope
     *
     * @param   \ReflectionClass                 $impl      concrete implementation
     * @param   \stubbles\ioc\InjectionProvider  $provider
     * @return  object
     * @throws  \RuntimeException
     */
    public function getInstance(\ReflectionClass $impl, InjectionProvider $provider)
    {
        if (null === $this->session) {
            throw new \RuntimeException('Can not create session-scoped instance for ' . $impl->getName() . ', no session set in session scope');
        }

        $key = self::SESSION_KEY . $impl->getName();
        if ($this->session->hasValue($key)) {
            return $this->session->value($key);
        }

        $instance = $provider->get();
        $this->session->putValue($key, $instance);
        return $instance;
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\ioc\binding;
use stubbles\ioc\Injector;
use stubbles\ioc\InjectionProvider;
/**
 * Base class for multi bindings.
 *
 * @since  2.0.0
 */
abstract class MultiBinding implements Binding
{
    /**
     * name of the list or map
     *
     * @type  string
     */
    private $name;

    /**
     * created multi binding
     *
     * @type  array
     */
    private $array = null;

    /**
     * constructor
     *
     * @param  string  $name  name of the list or map
     */
    public function __construct($name)
    {
        $this->name = $name;
    }

    /**
     * creates a closure which returns the given value
     *
     * @param   mixed $value
     * @return  \Closure
     */
    protected function getValueCreator($value)
    {
        if (is_string($value) && class_exists($value)) {
            return function($injector) use($value) { return $injector->getInstance($value); };
        }

        return function() use($value) { return $value; };
    }

    /**
     * creates a closure which uses the given provider to create the value
     *
     * @param   string|\stubbles\ioc\InjectionProvider  $provider
     * @return  \Closure
     * @throws  \InvalidArgumentException
     */
    protected function getProviderCreator($provider)
    {
        if (is_string($provider)) {
            return function($injector, $name, $key) use($provider)
                   {
                       $providerInstance = $injector->getInstance($provider);
                       if (!($providerInstance instanceof InjectionProvider)) {
                           throw new BindingException('Configured provider class ' . $provider . ' for ' . $name . '[' . $key . '] is not an instance of stubbles\ioc\InjectionProvider.');
                       }

                       return $providerInstance->get();

                   };
        } elseif ($provider instanceof InjectionProvider) {
            return function() use($provider) { return $provider->get(); };
        }

        throw new \InvalidArgumentException(
                'Given provider must either be a instance of'
                . ' stubbles\ioc\InjectionProvider or a class name representing'
                . ' such a provider instance.'
        );
    }

    /**
     * returns the created instance
     *
     * @param   \stubbles\ioc\Injector  $injector
     * @param   string                  $name
     * @return  mixed
     */
    public function getInstance(Injector $injector, $name)
    {
        if (null === $this->array) {
            $this->array = $this->resolve($injector, $name);
        }

        return $this->array;
    }

    /**
     * creates the instance
     *
     * @param   \stubbles\ioc\Injector  $injector
     * @param   string    $type
     * @return  array
     * @throws  \stubbles\ioc\binding\BindingException
     */
    private function resolve(Injector $injector, $type)
    {
        $resolved = [];
        foreach ($this->getBindings() as $key => $bindingValue) {
            $value = $bindingValue($injector, $this->name, $key);
            if ($this->isTypeMismatch($type, $value)) {
                $valueType = ((is_object($value)) ? (get_class($value)) : (gettype($value)));
                throw new BindingException('Value of type ' . $valueType . ' for ' . ((is_int($key)) ? ('list') : ('map')) . ' named ' . $this->name . ' at position ' . $key . ' is not of type ' . $type->getName());
            }

            $resolved[$key] = $value;
        }

        return $resolved;
    }

    /**
     * checks if given type and type of value are a mismatch
     *
     * A type mismatch is defined as follows: $value is an object and it's
     * an instance of the class defined with $type. In any other case there's no
     * type mismatch
     *
     * @param   string|\ReflectionClass  $type
     * @param   mixed                    $value
     * @return  bool
     */
    private function isTypeMismatch($type, $value)
    {
        if (!($type instanceof \ReflectionClass)) {
            return false;
        }

        if (!is_object($value)) {
            return true;
        }

        return !$type->isInstance($value);
    }

    /**
     * returns list of bindings for the array to create
     *
     * @return  array
     */
    protected abstract function getBindings();

    /**
     * creates a unique key for this binding
     *
     * @return  string
     */
    public function getKey()
    {
        return $this->getTypeKey() . '#' . $this->name;
    }

    /**
     * returns type key for for this binding
     *
     * @return  string
     */
    protected abstract function getTypeKey();
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\ioc;
use stubbles\ioc\binding\Binding;
use stubbles\ioc\binding\BindingScopes;
use stubbles\ioc\binding\ClassBinding;
use stubbles\ioc\binding\ConstantBinding;
use stubbles\ioc\binding\ListBinding;
use stubbles\ioc\binding\MapBinding;
use stubbles\ioc\binding\PropertyBinding;
use stubbles\values\Properties;
/**
 * Binder for the IoC functionality.
 *
 * @api
 */
class Binder
{
    /**
     * @type  string
     */
    private $environment;
    /**
     * list of available binding scopes
     *
     * @type  \stubbles\ioc\binding\BindingScopes
     */
    private $scopes;
    /**
     * added bindings that are in the index not yet
     *
     * @type  \stubbles\ioc\binding\Binding[]
     */
    private $bindings = [];
    /**
     * map of list bindings
     *
     * @type  \stubbles\ioc\binding\ListBinding[]
     */
    private $listBindings = [];
    /**
     * map of map bindings
     *
     * @type  \stubbles\ioc\binding\MapBinding[]
     */
    private $mapBindings  = [];

    /**
     * constructor
     *
     * @param  \stubbles\ioc\binding\BindingScopes  $scopes  optional
     */
    public function __construct(BindingScopes $scopes = null)
    {
        $this->scopes = ((null === $scopes) ? (new BindingScopes()) : ($scopes));
    }

    /**
     * adds a new binding to the injector
     *
     * @param   \stubbles\ioc\binding\Binding  $binding
     * @return  \stubbles\ioc\binding\Binding
     */
    public function addBinding(Binding $binding)
    {
         $this->bindings[] = $binding;
         return $binding;
    }

    /**
     * Bind a new interface to a class
     *
     * @param   string  $interface
     * @return  \stubbles\ioc\binding\ClassBinding
     */
    public function bind($interface)
    {
        return $this->addBinding(new ClassBinding($interface, $this->scopes));
    }

    /**
     * set environment for bindings
     *
     * @param   string  $environment
     * @return  \stubbles\ioc\Binder
     * @since   6.0.0
     */
    public function setEnvironment($environment)
    {
        $this->environment = $environment;
        return $this;
    }

    /**
     * binds properties from given properties file
     *
     * @param   string   $propertiesFile  file where properties are stored
     * @param   string   $environment     name of current environment
     * @return  \stubbles\values\Properties
     * @since   4.0.0
     */
    public function bindPropertiesFromFile($propertiesFile, $environment)
    {
        return $this->bindProperties(
                Properties::fromFile($propertiesFile),
                $environment
        );
    }

    /**
     * binds properties
     *
     * @param   \stubbles\values\Properties  $properties
     * @param   string                $environment  name of current environment
     * @return  \stubbles\values\Properties
     * @since   3.4.0
     */
    public function bindProperties(Properties $properties, $environment)
    {
        $this->addBinding(new PropertyBinding($properties, $environment));
        $this->bind(Properties::class)
             ->named('config.ini')
             ->toInstance($properties);
        return $properties;
    }

    /**
     * bind a constant
     *
     * @param   string  $name  name of constant to bind
     * @return  \stubbles\ioc\binding\ConstantBinding
     */
    public function bindConstant($name)
    {
        return $this->addBinding(new ConstantBinding($name));
    }

    /**
     * bind to a list
     *
     * If a list with given name already exists it will return exactly this list
     * to add more values to it.
     *
     * @param   string  $name
     * @return  \stubbles\ioc\binding\ListBinding
     * @since   2.0.0
     */
    public function bindList($name)
    {
        if (!isset($this->listBindings[$name])) {
            $this->listBindings[$name] = $this->addBinding(new ListBinding($name));
        }

        return $this->listBindings[$name];
    }

    /**
     * bind to a map
     *
     * If a map with given name already exists it will return exactly this map
     * to add more key-value pairs to it.
     *
     * @param   string  $name
     * @return  \stubbles\ioc\binding\MapBinding
     * @since   2.0.0
     */
    public function bindMap($name)
    {
        if (!isset($this->mapBindings[$name])) {
            $this->mapBindings[$name] = $this->addBinding(new MapBinding($name));
        }

        return $this->mapBindings[$name];
    }

    /**
     * Get an injector for this binder
     *
     * @return  \stubbles\ioc\Injector
     */
    public function getInjector()
    {
        return new Injector($this->environment, $this->bindings, $this->scopes);
    }

    /**
     * creates injector instance with bindings
     *
     * @param   callable  ...$applyBindings  optional  callables which accept instances of stubbles\ioc\Binder
     * @return  \stubbles\ioc\Injector
     * @since   7.0.0
     */
    public static function createInjector(callable ...$applyBindings)
    {
        $self = new self();
        foreach ($applyBindings as $applyBinding) {
            $applyBinding($self);
        }

        return $self->getInjector();
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\lang;
/**
 * Base class for enums.
 *
 * @deprecated  since 7.0.0, will be removed with 8.0.0
 */
abstract class Enum
{
    /**
     * name of the enum
     *
     * @type  string
     */
    protected $name;
    /**
     * value of enum
     *
     * @type  int
     */
    protected $value;

    /**
     * constructor
     *
     * @param  string  $name   enum name
     * @param  mixed   $value  enum value
     */
    protected function __construct($name, $value = null)
    {
        $this->name  = $name;
        $this->value = ((null !== $value) ? ($value) : ($name));
    }

    /**
     * forbid cloning of enums
     *
     * @throws  \RuntimeException
     */
    public final function __clone()
    {
        throw new \RuntimeException('Cloning of enums is not allowed.');
    }

    /**
     * returns the name of the enum
     *
     * @api
     * @return  string
     */
    public function name()
    {
        return $this->name;
    }

    /**
     * returns the value of the enum
     *
     * @api
     * @return  mixed
     */
    public function value()
    {
        return $this->value;
    }

    /**
     * returns the enum instance of given class identified by its name
     *
     * @api
     * @param   string  $name
     * @return  \stubbles\lang\Enum
     * @throws  \InvalidArgumentException
     */
    public static function forName($name)
    {
        $enum = new \ReflectionClass(get_called_class());
        try {
            return $enum->getStaticPropertyValue($name);
        } catch (\ReflectionException $re) {
            throw new \InvalidArgumentException($re->getMessage());
        }
    }

    /**
     * returns the enum instance of given class identified by its value
     *
     * @api
     * @param   string  $value
     * @return  \stubbles\lang\Enum
     * @throws  \InvalidArgumentException
     */
    public static function forValue($value)
    {
        $enumClass = new \ReflectionClass(get_called_class());
        foreach ($enumClass->getStaticProperties() as $instance) {
            if ($instance->value() === $value) {
                return $instance;
            }
        }

        throw new \InvalidArgumentException(
                'Enum ' . $enumClass->getName() . ' for value ' . $value
                . ' does not exist.'
        );
    }

    /**
     * returns a list of all instances for given enum
     *
     * @api
     * @return  \stubbles\lang\Enum[]
     */
    public static function instances()
    {
        $enum = new \ReflectionClass(get_called_class());
        return array_values($enum->getStaticProperties());
    }

    /**
     * returns a list of enum names for given enum
     *
     * @api
     * @return  string[]
     */
    public static function namesOf()
    {
        $enum = new \ReflectionClass(get_called_class());
        return array_keys($enum->getStaticProperties());
    }

    /**
     * returns a list of values for given enum
     *
     * @api
     * @return  mixed[]
     */
    public static function valuesOf()
    {
        $enum   = new \ReflectionClass(get_called_class());
        $values = [];
        foreach ($enum->getStaticProperties() as $name => $instance) {
            $values[$name] = $instance->value;
        }

        return $values;
    }

    /**
     * checks whether a value is equal to the class
     *
     * @XmlIgnore
     * @param   mixed  $compare
     * @return  bool
     */
    public function equals($compare)
    {
        if ($compare instanceof self) {
            return (get_class($compare) === get_class($this) && $compare->name() === $this->name);
        }

        return false;
    }

    /**
     * returns a string representation of the class
     *
     * @XmlIgnore
     * @return  string
     */
    public function __toString()
    {
        $string  = get_class($this) . " {\n";
        $string .= '    ' . $this->name . "\n";
        $string .= '    ' . $this->value . "\n";
        $string .= "}\n";
        return $string;
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\environments\errorhandler;
/**
 * Error handler for illegal arguments.
 *
 * This error handler is responsible for errors of type E_RECOVERABLE_ERROR which denote that
 * a type hint has been infringed with an argument of another type. If such an error is detected
 * an stubIllegalArgumentException will be thrown.
 *
 * @internal
 */
class InvalidArgument implements ErrorHandler
{
    /**
     * checks whether this error handler is responsible for the given error
     *
     * @param   int     $level    level of the raised error
     * @param   string  $message  error message
     * @param   string  $file     filename that the error was raised in
     * @param   int     $line     line number the error was raised at
     * @param   array   $context  array of every variable that existed in the scope the error was triggered in
     * @return  bool    true if error handler is responsible, else false
     */
    public function isResponsible($level, $message, $file = null, $line = null, array $context = [])
    {
        if (E_RECOVERABLE_ERROR != $level) {
            return false;
        }

        return (bool) preg_match('/Argument [0-9]+ passed to [a-zA-Z0-9_\\\\]+::[a-zA-Z0-9_]+\(\) must be an instance of [a-zA-Z0-9_\\\\]+, [a-zA-Z0-9_\\\\]+ given/', $message);
    }

    /**
     * checks whether this error is supressable
     *
     * This method is called in case the level is 0. A type hint infringement
     * is never supressable.
     *
     * @param   int     $level    level of the raised error
     * @param   string  $message  error message
     * @param   string  $file     filename that the error was raised in
     * @param   int     $line     line number the error was raised at
     * @param   array   $context  array of every variable that existed in the scope the error was triggered in
     * @return  bool    true if error is supressable, else false
     */
    public function isSupressable($level, $message, $file = null, $line = null, array $context = [])
    {
        return false;
    }

    /**
     * handles the given error
     *
     * @param   int     $level    level of the raised error
     * @param   string  $message  error message
     * @param   string  $file     filename that the error was raised in
     * @param   int     $line     line number the error was raised at
     * @param   array   $context  array of every variable that existed in the scope the error was triggered in
     * @return  bool    true if error message should populate $php_errormsg, else false
     * @throws  \InvalidArgumentException
     */
    public function handle($level, $message, $file = null, $line = null, array $context = [])
    {
        throw new \InvalidArgumentException(
                $message . ' @ ' . $file . ' on line ' . $line
        );
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\environments\exceptionhandler;
use stubbles\environments\ExceptionLogger;
/**
 * Abstract base implementation for exception handlers, containing logging of exceptions.
 *
 * @internal
 */
abstract class AbstractExceptionHandler implements ExceptionHandler
{
    /**
     * path to project
     *
     * @type  string
     */
    protected $projectPath;
    /**
     * current php sapi
     *
     * @type  string
     */
    private $sapi;
    /**
     * switch whether logging is enabled or not
     *
     * @type  bool
     */
    private $loggingEnabled = true;
    /**
     * logger for exceptions
     *
     * @type  \stubbles\environments\ExceptionLogger
     */
    private $exceptionLogger;

    /**
     * constructor
     *
     * @param  string  $projectPath  path to project
     * @param  string  $sapi         current php sapi
     */
    public function __construct($projectPath, $sapi = PHP_SAPI)
    {
        $this->projectPath     = $projectPath;
        $this->sapi            = $sapi;
        $this->exceptionLogger = new ExceptionLogger($projectPath);
    }

    /**
     * disables exception logging
     *
     * @return  \stubbles\environments\exceptionhandler\AbstractExceptionHandler
     */
    public function disableLogging()
    {
        $this->loggingEnabled = false;
        return $this;
    }

    /**
     * sets the mode for new log directories
     *
     * @param   int  $filemode
     * @return  \stubbles\environments\exceptionhandler\AbstractExceptionHandler
     */
    public function setFilemode($filemode)
    {
        $this->exceptionLogger->setFilemode($filemode);
        return $this;
    }

    /**
     * handles the exception
     *
     * @param  \Exception  $exception  the uncatched exception
     */
    public function handleException(\Exception $exception)
    {
        if ($this->loggingEnabled) {
            $this->exceptionLogger->log($exception);
        }

        if ('cgi' === $this->sapi) {
            $this->header('Status: 500 Internal Server Error');
        } else {
            $this->header('HTTP/1.1 500 Internal Server Error');
        }

        $this->writeBody($this->createResponseBody($exception));
    }

    /**
     * creates response body with useful data for display
     *
     * @param   \Exception  $exception  the uncatched exception
     * @return  string
     */
    protected abstract function createResponseBody(\Exception $exception);

    /**
     * helper method to send the header
     *
     * @param  string  $header
     */
    protected function header($header)
    {
        header($header);
    }

    /**
     * helper method to send the body
     *
     * @param  string  $body
     */
    protected function writeBody($body)
    {
        echo $body;
        flush();
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\ioc\binding;
/**
 * Class for map bindings.
 *
 * @since  2.0.0
 */
class MapBinding extends MultiBinding
{
    /**
     * This string is used when generating the key for a map binding.
     */
    const TYPE        = '__MAP__';
    /**
     * list of bindings for the map values
     *
     * @type  array
     */
    private $bindings = [];

    /**
     * adds an entry to the list
     *
     * @api
     * @param   string  $key
     * @param   mixed   $value
     * @return  \stubbles\ioc\binding\MapBinding
     */
    public function withEntry($key, $value)
    {
        $this->bindings[$key] = $this->getValueCreator($value);
        return $this;
    }

    /**
     * adds an entry to the map created by an injection provider
     *
     * @api
     * @param   string                    $key
     * @param   string|\stubbles\ioc\InjectionProvider  $provider
     * @return  \stubbles\ioc\binding\MapBinding
     */
    public function withEntryFromProvider($key, $provider)
    {
        $this->bindings[$key] = $this->getProviderCreator($provider);
        return $this;
    }

    /**
     * adds an entry which is created by given closure
     *
     * @api
     * @param   string    $key
     * @param   \Closure  $closure
     * @return  \stubbles\ioc\binding\MapBinding
     * @since   2.1.0
     */
    public function withEntryFromClosure($key, \Closure $closure)
    {
        $this->bindings[$key] = $closure;
        return $this;
    }

    /**
     * returns list of bindings for the map to create
     *
     * @return  array
     */
    protected function getBindings()
    {
        return $this->bindings;
    }

    /**
     * returns type key for for this binding
     *
     * @return  string
     */
    protected function getTypeKey()
    {
        return self::TYPE;
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\ioc;
use stubbles\ioc\binding\BindingException;
use stubbles\ioc\binding\ConstantBinding;
use stubbles\ioc\binding\ListBinding;
use stubbles\ioc\binding\MapBinding;
use stubbles\ioc\binding\PropertyBinding;

use function stubbles\reflect\annotationsOf;
/**
 * Default injection provider.
 *
 * Creates objects and injects all dependencies via the default injector.
 *
 * @internal
 */
class DefaultInjectionProvider implements InjectionProvider
{
    /**
     * injector to use for dependencies
     *
     * @type  \stubbles\ioc\Injector
     */
    private $injector;
    /**
     * concrete implementation to use
     *
     * @type  \ReflectionClass
     */
    private $class;

    /**
     * constructor
     *
     * @param  \stubbles\ioc\Injector  $injector
     * @param  \ReflectionClass        $impl
     */
    public function __construct(Injector $injector, \ReflectionClass $impl)
    {
        $this->injector = $injector;
        $this->class    = $impl;
    }

    /**
     * returns the value to provide
     *
     * @param   string  $name
     * @return  mixed
     */
    public function get($name = null)
    {
        $constructor = $this->class->getConstructor();
        if (null === $constructor || $this->class->isInternal()) {
            return $this->class->newInstance();
        }

        $params = $this->injectionValuesForMethod($constructor);
        if (count($params) === 0) {
            return $this->class->newInstance();
        }

        return $this->class->newInstanceArgs($params);
    }

    /**
     * returns a list of all injection values for given method
     *
     * @param   \ReflectionMethod  $method
     * @return  array
     * @throws  \stubbles\ioc\binding\BindingException
     */
    private function injectionValuesForMethod(\ReflectionMethod $method)
    {
        $paramValues = [];
        $defaultName = $this->methodBindingName($method);
        foreach ($method->getParameters() as $param) {
            $type  = $this->paramType($method, $param);
            $name  = $this->detectBindingName($param, $defaultName);
            $hasExplicitBinding = $this->injector->hasExplicitBinding($type, $name);
            if (!$hasExplicitBinding && $param->isDefaultValueAvailable()) {
                $paramValues[] = $param->getDefaultValue();
                continue;
            }

            if (!$this->injector->hasBinding($type, $name)) {
                $typeMsg = $this->createTypeMessage($type, $name);
                throw new BindingException(
                        'Can not inject into '
                        . $this->class->getName() . '::' . $method->getName()
                        . '(' . $this->createParamString($param, $type)
                        . '). No binding for type ' . $typeMsg
                        . ' specified. Injection stack: ' . "\n"
                        . join("\n", $this->injector->stack())
                );
            }

            $paramValues[] = $this->injector->getInstance($type, $name);
        }

        return $paramValues;
    }

    /**
     * returns default binding name for all parameters on given method
     *
     * @param   \ReflectionMethod  $method
     * @return  string
     */
    private function methodBindingName(\ReflectionMethod $method)
    {
        $annotations = annotationsOf($method);
        if ($annotations->contain('List')) {
            return $annotations->firstNamed('List')->getValue();
        }

        if ($annotations->contain('Map')) {
            return $annotations->firstNamed('Map')->getValue();
        }

        if ($annotations->contain('Named')) {
            return $annotations->firstNamed('Named')->getName();
        }

        if ($annotations->contain('Property')) {
            return $annotations->firstNamed('Property')->getValue();
        }

        return null;
    }

    /**
     * returns type of param
     *
     * @param   \ReflectionMethod     $method
     * @param   \ReflectionParameter  $param
     * @return  string
     */
    private function paramType(\ReflectionMethod $method, \ReflectionParameter $param)
    {
        $methodAnnotations = annotationsOf($method);
        $paramAnnotations  = annotationsOf($param);
        $paramClass        = $param->getClass();
        if (null !== $paramClass) {
            if ($methodAnnotations->contain('Property') || $paramAnnotations->contain('Property')) {
                return PropertyBinding::TYPE;
            }

            return $paramClass->getName();
        }

        if ($methodAnnotations->contain('List') || $paramAnnotations->contain('List')) {
            return ListBinding::TYPE;
        }

        if ($methodAnnotations->contain('Map') || $paramAnnotations->contain('Map')) {
            return MapBinding::TYPE;
        }

        if ($methodAnnotations->contain('Property') || $paramAnnotations->contain('Property')) {
            return PropertyBinding::TYPE;
        }

        return ConstantBinding::TYPE;
    }

    /**
     * detects name for binding
     *
     * @param   \ReflectionParameter  $param
     * @param   string                $default
     * @return  string|\ReflectionClass
     */
    private function detectBindingName(\ReflectionParameter $param, $default)
    {
        $annotations = annotationsOf($param);
        if ($annotations->contain('List')) {
            return $annotations->firstNamed('List')->getValue();
        }

        if ($annotations->contain('Map')) {
            return $annotations->firstNamed('Map')->getValue();
        }

        if ($annotations->contain('Named')) {
            return $annotations->firstNamed('Named')->getName();
        }

        if ($annotations->contain('Property')) {
            return $annotations->firstNamed('Property')->getValue();
        }

        return $default;
    }

    /**
     * creates the complete type message
     *
     * @param   string  $type  type to create message for
     * @param   string  $name  name of named parameter
     * @return  string
     */
    private function createTypeMessage($type, $name)
    {
        return ((null !== $name) ? ($type . ' (named "' . $name . '")') : ($type));
    }

    /**
     * creates the called method message
     *
     * @param   \ReflectionParameter  $parameter
     * @param   string                $type
     * @return  string
     */
    private function createParamString(\ReflectionParameter $parameter, $type)
    {
        $message = '';
        if (!in_array($type, [PropertyBinding::TYPE, ConstantBinding::TYPE, ListBinding::TYPE, MapBinding::TYPE])) {
            $message .= $type . ' ';
        } elseif ($parameter->isArray()) {
            $message .= 'array ';
        }

        return $message . '$' . $parameter->getName();
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\environments;
/**
 * Can be used to log exceptions.
 *
 * @since  3.3.0
 * @Singleton
 */
class ExceptionLogger
{
    /**
     * directory to log errors into
     *
     * @type  string
     */
    private $logDir;
    /**
     * mode for new directories
     *
     * @type  int
     */
    private $filemode = 0700;

    /**
     * constructor
     *
     * @param  string  $projectPath  path to project
     * @Named('stubbles.project.path')
     */
    public function __construct($projectPath)
    {
        $this->logDir = $projectPath
                . DIRECTORY_SEPARATOR
                . 'log'
                . DIRECTORY_SEPARATOR
                . 'errors'
                . DIRECTORY_SEPARATOR
                . '{Y}'
                . DIRECTORY_SEPARATOR
                . '{M}';
    }

    /**
     * sets the mode for new log directories
     *
     * @param   int  $filemode
     * @return  \stubbles\environments\exceptionhandler\ExceptionLogger
     */
    public function setFilemode($filemode)
    {
        $this->filemode = $filemode;
        return $this;
    }

    /**
     * logs the exception into a logfile
     *
     * @param  \Exception  $exception  exception to log
     */
    public function log(\Exception $exception)
    {
        $logData  = date('Y-m-d H:i:s');
        $logData .= $this->exceptionFields($exception);
        $logData .= $this->fieldsForPrevious($exception->getPrevious());
        error_log(
                $logData . "\n",
                3,
                $this->getLogDir() . DIRECTORY_SEPARATOR . 'exceptions-' . date('Y-m-d') . '.log'
        );
    }

    /**
     * returns fields for exception to log
     *
     * @param   \Exception  $exception
     * @return  string
     */
    private function exceptionFields(\Exception $exception)
    {
        return '|' . get_class($exception)
             . '|' . $exception->getMessage()
             . '|' . $exception->getFile()
             . '|' . $exception->getLine();
    }

    /**
     * returns fields for previous exception
     *
     * @param   \Exception  $exception
     * @return  string
     */
    private function fieldsForPrevious(\Exception $exception = null)
    {
        if (null === $exception) {
            return '||||';
        }

        return $this->exceptionFields($exception);
    }

    /**
     * returns directory where to write logfile to
     *
     * @return  string
     */
    private function getLogDir()
    {
        $logDir = str_replace(
                '{Y}',
                date('Y'),
                str_replace('{M}', date('m'), $this->logDir)
        );
        if (!file_exists($logDir)) {
            mkdir($logDir, $this->filemode, true);
        }

        return $logDir;
    }
}


<?php
/**
 * This file is part of stubbles.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @package  stubbles
 */
namespace stubbles\environments\errorhandler;
/**
 * Container for a collection of PHP error handlers.
 */
class ErrorHandlers implements ErrorHandler
{
    /**
     * list of registered error handlers
     *
     * @type  \stubbles\environments\ErrorHandler[]
     */
    private $errorHandlers = [];

    /**
     * adds an error handler to the collection
     *
     * @param  \stubbles\environments\ErrorHandler  $errorHandler
     */
    public function addErrorHandler(ErrorHandler $errorHandler)
    {
        $this->errorHandlers[] = $errorHandler;
    }

    /**
     * checks whether this error handler is responsible for the given error
     *
     * This method is called in case the level is 0. It decides whether the
     * error has to be handled or if it can be omitted.
     *
     * @param   int     $level    level of the raised error
     * @param   string  $message  error message
     * @param   string  $file     filename that the error was raised in
     * @param   int     $line     line number the error was raised at
     * @param   array   $context  array of every variable that existed in the scope the error was triggered in
     * @return  bool    true if error handler is responsible, else false
     */
    public function isResponsible($level, $message, $file = null, $line = null, array $context = [])
    {
        foreach ($this->errorHandlers as $errorHandler) {
            if ($errorHandler->isResponsible($level, $message, $file, $line, $context) == true) {
                return true;
            }
        }

        return false;
    }

    /**
     * checks whether this error is supressable
     *
     * @param   int     $level    level of the raised error
     * @param   string  $message  error message
     * @param   string  $file     filename that the error was raised in
     * @param   int     $line     line number the error was raised at
     * @param   array   $context  array of every variable that existed in the scope the error was triggered in
     * @return  bool    true if error is supressable, else false
     */
    public function isSupressable($level, $message, $file = null, $line = null, array $context = [])
    {
        foreach ($this->errorHandlers as $errorHandler) {
            if ($errorHandler->isSupressable($level, $message, $file, $line, $context) == false) {
                return false;
            }
        }

        return true;
    }

    /**
     * handles the given error
     *
     * @param   int     $level    level of the raised error
     * @param   string  $message  error message
     * @param   string  $file     filename that the error was raised in
     * @param   int     $line     line number the error was raised at
     * @param   array   $context  array of every variable that existed in the scope the error was triggered in
     * @return  bool    true if error message should populate $php_errormsg, else false
     */
    public function handle($level, $message, $file = null, $line = null, array $context = [])
    {
        $errorReporting = error_reporting();
        foreach ($this->errorHandlers as $errorHandler) {
            if ($errorHandler->isResponsible($level, $message, $file, $line, $context)) {
                // if function/method was called with prepended @ and error is supressable
                if (0 == $errorReporting && $errorHandler->isSupressable($level, $message, $file, $line, $context)) {
                    return ErrorHandler::STOP_ERROR_HANDLING;
                }

                return $errorHandler->handle($level, $message, $file, $line, $context);
            }
        }

        return ErrorHandler::CONTINUE_WITH_PHP_INTERNAL_HANDLING;
    }
}


<?php
declare(strict_types=1);

namespace TypistTech\Imposter;

class Config implements ConfigInterface
{
    /**
     * @var string
     */
    protected $packageDir;

    /**
     * @var array
     */
    private $config;

    public function __construct(string $packageDir, array $config)
    {
        $this->packageDir = StringUtil::addTrailingSlash($packageDir);
        $this->config = $config;
    }

    /**
     * @return string[]
     */
    public function getAutoloads(): array
    {
        return array_map(function (string $autoload) {
            return $this->packageDir . $autoload;
        }, array_unique($this->getAutoloadPaths()));
    }

    /**
     * @return string[]
     */
    private function getAutoloadPaths(): array
    {
        $autoload = $this->get('autoload');

        return ArrayUtil::flattenMap(function ($autoloadConfig) {
            return $this->normalizeAutoload($autoloadConfig);
        }, $autoload);
    }

    protected function get(string $key): array
    {
        return $this->config[$key] ?? [];
    }

    /**
     * @param $autoloadConfigs
     *
     * @return string[]
     */
    private function normalizeAutoload($autoloadConfigs): array
    {
        if (! is_array($autoloadConfigs)) {
            return [$autoloadConfigs];
        }

        return ArrayUtil::flattenMap(function ($autoloadConfig) {
            return $this->normalizeAutoload($autoloadConfig);
        }, $autoloadConfigs);
    }

    public function getPackageDir(): string
    {
        return $this->packageDir;
    }

    /**
     * @return string[]
     */
    public function getRequires(): array
    {
        return array_keys($this->get('require'));
    }
}


<?php
declare(strict_types=1);

namespace TypistTech\Imposter;

class ConfigCollectionFactory
{
    public static function forProject(
        ProjectConfigInterface $projectConfig,
        Filesystem $filesystem
    ): ConfigCollectionInterface {
        return self::addRequiredPackageConfigsRecursively(
            new ConfigCollection,
            $projectConfig,
            $projectConfig,
            $filesystem
        );
    }

    private static function addRequiredPackageConfigsRecursively(
        ConfigCollectionInterface $configCollection,
        ProjectConfigInterface $projectConfig,
        ConfigInterface $config,
        Filesystem $filesystem
    ): ConfigCollectionInterface {
        $filteredRequires = self::getFilteredPackages($projectConfig, $config);

        foreach ($filteredRequires as $package) {
            $packageConfig = ConfigFactory::build(
                $projectConfig->getVendorDir() . $package . '/composer.json',
                $filesystem
            );

            $configCollection->add($packageConfig);

            self::addRequiredPackageConfigsRecursively(
                $configCollection,
                $projectConfig,
                $packageConfig,
                $filesystem
            );
        }

        return $configCollection;
    }

    /**
     * @param ProjectConfigInterface $projectConfig
     * @param ConfigInterface        $config
     *
     * @return string[]
     */
    private static function getFilteredPackages(ProjectConfigInterface $projectConfig, ConfigInterface $config): array
    {
        $requiredPackages = array_filter($config->getRequires(), function (string $package) {
            return (false !== strpos($package, '/'));
        });

        $nonComposerPackages = array_filter($requiredPackages, function (string $package) {
            return (false === strpos($package, 'composer/'));
        });

        return array_filter($nonComposerPackages, function (string $package) use ($projectConfig) {
            return ! in_array($package, $projectConfig->getExcludes(), true);
        });
    }
}


<?php
declare(strict_types=1);

namespace TypistTech\Imposter;

class ImposterFactory
{
    /**
     * @param string   $projectPath
     * @param string[] $extraExcludes
     *
     * @return Imposter
     */
    public static function forProject(string $projectPath, array $extraExcludes = []): Imposter
    {
        $filesystem = new Filesystem;

        $projectConfig = ConfigFactory::buildProjectConfig($projectPath . '/composer.json', $filesystem);
        $projectConfig->setExtraExcludes($extraExcludes);

        $transformer = new Transformer($projectConfig->getImposterNamespace(), $filesystem);
        $configCollection = ConfigCollectionFactory::forProject(
            $projectConfig,
            $filesystem
        );

        return new Imposter($configCollection, $transformer);
    }
}


<?php
declare(strict_types=1);

namespace TypistTech\Imposter;

class Imposter implements ImposterInterface
{
    /**
     * @var string[]
     */
    private $autoloads;

    /**
     * @var ConfigCollectionInterface
     */
    private $configCollection;

    /**
     * @var TransformerInterface
     */
    private $transformer;

    /**
     * Imposter constructor.
     *
     * @param ConfigCollectionInterface $configCollection
     * @param TransformerInterface      $transformer
     */
    public function __construct(ConfigCollectionInterface $configCollection, TransformerInterface $transformer)
    {
        $this->configCollection = $configCollection;
        $this->transformer = $transformer;
    }

    /**
     * @return ConfigCollectionInterface
     */
    public function getConfigCollection(): ConfigCollectionInterface
    {
        return $this->configCollection;
    }

    /**
     * @return TransformerInterface
     */
    public function getTransformer(): TransformerInterface
    {
        return $this->transformer;
    }

    /**
     * Transform all autoload files.
     *
     * @return void
     */
    public function run()
    {
        $autoloads = $this->getAutoloads();
        array_walk($autoloads, [$this, 'transform']);
    }

    /**
     * Get all autoload paths.
     *
     * @return string[]
     */
    public function getAutoloads(): array
    {
        if (empty($this->autoloads)) {
            $this->autoloads = $this->configCollection->getAutoloads();
        }

        return $this->autoloads;
    }

    /**
     * Transform a file or directory recursively.
     *
     * @param string $target Path to the target file or directory.
     *
     * @return void
     */
    public function transform(string $target)
    {
        $this->transformer->transform($target);
    }
}


<?php
declare(strict_types=1);

namespace TypistTech\Imposter;

use SplFileInfo;

class Transformer implements TransformerInterface
{
    /**
     * @var FilesystemInterface
     */
    private $filesystem;

    /**
     * @var string
     */
    private $namespacePrefix;

    /**
     * Transformer constructor.
     *
     * @param string              $namespacePrefix
     * @param FilesystemInterface $filesystem
     */
    public function __construct(string $namespacePrefix, FilesystemInterface $filesystem)
    {
        $this->namespacePrefix = StringUtil::ensureDoubleBackwardSlash($namespacePrefix);
        $this->filesystem = $filesystem;
    }

    /**
     * Transform a file or directory recursively.
     *
     * @todo Skip non-php files.
     *
     * @param string $target Path to the target file or directory.
     *
     * @return void
     */
    public function transform(string $target)
    {
        if ($this->filesystem->isFile($target)) {
            $this->doTransform($target);

            return;
        }

        $files = $this->filesystem->allFiles($target);

        array_walk($files, function (SplFileInfo $file) {
            $this->doTransform($file->getRealPath());
        });
    }

    /**
     * @param string $targetFile
     *
     * @return void
     */
    private function doTransform(string $targetFile)
    {
        $this->prefixNamespace($targetFile);
        $this->prefixUseConst($targetFile);
        $this->prefixUseFunction($targetFile);
        $this->prefixUse($targetFile);
    }

    /**
     * Prefix namespace at the given path.
     *
     * @param string $targetFile
     *
     * @return void
     */
    private function prefixNamespace(string $targetFile)
    {
        $pattern = sprintf(
            '/%1$s\\s+(?!(%2$s)|(Composer(\\\\|;)))/',
            'namespace',
            $this->namespacePrefix
        );
        $replacement = sprintf('%1$s %2$s', 'namespace', $this->namespacePrefix);

        $this->replace($pattern, $replacement, $targetFile);
    }

    /**
     * Replace string in the given file.
     *
     * @param string $pattern
     * @param string $replacement
     * @param string $targetFile
     *
     * @return void
     */
    private function replace(string $pattern, string $replacement, string $targetFile)
    {
        $this->filesystem->put(
            $targetFile,
            preg_replace(
                $pattern,
                $replacement,
                $this->filesystem->get($targetFile)
            )
        );
    }

    /**
     * Prefix `use const` keywords at the given path.
     *
     * @param string $targetFile
     *
     * @return void
     */
    private function prefixUseConst(string $targetFile)
    {
        $pattern = sprintf(
            '/%1$s\\s+(?!(%2$s)|(\\\\(?!.*\\\\.*))|(Composer(\\\\|;)|(?!.*\\\\.*)))/',
            'use const',
            $this->namespacePrefix
        );
        $replacement = sprintf('%1$s %2$s', 'use const', $this->namespacePrefix);

        $this->replace($pattern, $replacement, $targetFile);
    }

    /**
     * Prefix `use function` keywords at the given path.
     *
     * @param string $targetFile
     *
     * @return void
     */
    private function prefixUseFunction(string $targetFile)
    {
        $pattern = sprintf(
            '/%1$s\\s+(?!(%2$s)|(\\\\(?!.*\\\\.*))|(Composer(\\\\|;)|(?!.*\\\\.*)))/',
            'use function',
            $this->namespacePrefix
        );
        $replacement = sprintf('%1$s %2$s', 'use function', $this->namespacePrefix);

        $this->replace($pattern, $replacement, $targetFile);
    }

    /**
     * Prefix `use` keywords at the given path.
     *
     * @param string $targetFile
     *
     * @return void
     */
    private function prefixUse(string $targetFile)
    {
        $pattern = sprintf(
            '/%1$s\\s+(?!(const)|(function)|(%2$s)|(\\\\(?!.*\\\\.*))|(Composer(\\\\|;)|(?!.*\\\\.*)))/',
            'use',
            $this->namespacePrefix
        );
        $replacement = sprintf('%1$s %2$s', 'use', $this->namespacePrefix);

        $this->replace($pattern, $replacement, $targetFile);
    }
}


<?php
declare(strict_types=1);

namespace TypistTech\Imposter;

use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;

class Filesystem implements FilesystemInterface
{
    /**
     * @param string $path
     *
     * @return \SplFileInfo[]
     * @throws \UnexpectedValueException
     */
    public function allFiles(string $path): array
    {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)
        );

        return iterator_to_array($iterator);
    }

    /**
     * Extract the parent directory from a file path.
     *
     * @param  string $path
     *
     * @return string
     */
    public function dirname(string $path): string
    {
        return pathinfo($path, PATHINFO_DIRNAME);
    }

    /**
     * Get the contents of a file.
     *
     * @param  string $path
     *
     * @return string
     * @throws \RuntimeException
     */
    public function get(string $path): string
    {
        if (! $this->isFile($path)) {
            throw new RuntimeException('File does not exist at path ' . $path);
        }

        return file_get_contents($path);
    }

    /**
     * Determine if the given path is a file.
     *
     * @param  string $file
     *
     * @return bool
     */
    public function isFile(string $file): bool
    {
        return is_file($file);
    }

    /**
     * Write the contents of a file.
     *
     * @param  string $path
     * @param  string $contents
     *
     * @return int|false
     */
    public function put(string $path, string $contents)
    {
        return file_put_contents($path, $contents);
    }
}


<?php

namespace Isswp101\Persimmon\Relationship;

use Isswp101\Persimmon\ElasticsearchModel;
use Isswp101\Persimmon\Exceptions\ParentModelNotFoundException;

class BelongsToRelationship
{
    /**
     * @var ElasticsearchModel
     */
    protected $child;

    /**
     * @var ElasticsearchModel
     */
    protected $parentClass;

    public function __construct(ElasticsearchModel $child, $parentClass)
    {
        $this->child = $child;
        $this->parentClass = $parentClass;
    }

    /**
     * Associate parent document.
     *
     * @param ElasticsearchModel $parent
     */
    public function associate(ElasticsearchModel $parent)
    {
        $this->child->setParent($parent);
    }

    /**
     * Return parent model.
     *
     * @return ElasticsearchModel|null
     */
    public function get()
    {
        $parent = $this->child->getParent();

        if ($parent) {
            return $parent;
        }

        $parentClass = $this->parentClass;

        $parentId = $this->child->getParentId();

        $innerHits = $this->child->getInnerHits();

        if ($innerHits) {
            $attributes = $innerHits->getParent($parentClass::getType());
            $parent = new $parentClass($attributes);
        } elseif ($parentId) {
            $parent = $parentClass::find($parentId);
        }

        if ($parent) {
            $this->child->setParent($parent);
        }

        return $parent;
    }

    /**
     * Return parent model.
     *
     * @throws ParentModelNotFoundException
     * @return ElasticsearchModel
     */
    public function getOrFail()
    {
        $model = $this->get();

        if (is_null($model)) {
            throw new ParentModelNotFoundException($this->parentClass, $this->child->getParentId());
        }

        return $model;
    }
}


<?php

namespace Isswp101\Persimmon\Relationship;

use Isswp101\Persimmon\Collection\ElasticsearchCollection;
use Isswp101\Persimmon\ElasticsearchModel;
use Isswp101\Persimmon\QueryBuilder\Filters\ParentFilter;
use Isswp101\Persimmon\QueryBuilder\QueryBuilder;

class HasManyRelationship
{
    /**
     * @var ElasticsearchModel
     */
    protected $parent;

    /**
     * @var ElasticsearchModel
     */
    protected $childClassName;

    public function __construct(ElasticsearchModel $parent, $childClassName)
    {
        $this->parent = $parent;
        $this->childClassName = $childClassName;
    }

    /**
     * Find all children.
     *
     * @return ElasticsearchCollection|ElasticsearchModel[]
     */
    public function get()
    {
        $child = $this->childClassName;
        $query = new QueryBuilder();
        $query->filter(new ParentFilter($this->parent->getId()));
        $collection = $child::search($query);
        $collection->each(function (ElasticsearchModel $model) {
            $model->setParent($this->parent);
        });
        return $collection;
    }

    /**
     * Find model by id.
     *
     * @param mixed $id
     * @return ElasticsearchModel|null
     */
    public function find($id)
    {
        $child = $this->childClassName;
        $model = $child::findWithParentId($id, $this->parent->getId());
        if ($model) {
            $model->setParent($this->parent);
        }
        return $model;
    }

    /**
     * Save children.
     *
     * @param ElasticsearchModel|ElasticsearchModel[] $child
     */
    public function save($child)
    {
        /** @var ElasticsearchModel[] $children */
        $children = !is_array($child) ? [$child] : $child;
        // @TODO: use bulk if count($children) > 1
        foreach ($children as $child) {
            $child->setParent($this->parent);
            $child->save();
        }
    }
}


<?php

namespace Isswp101\Persimmon;

use Elasticsearch\Common\Exceptions\Missing404Exception;
use Exception;
use Isswp101\Persimmon\Contracts\Arrayable;
use Isswp101\Persimmon\Contracts\Jsonable;
use Isswp101\Persimmon\Contracts\Stringable;
use Isswp101\Persimmon\DAL\IDAL;
use Isswp101\Persimmon\Exceptions\ModelNotFoundException;
use Isswp101\Persimmon\Traits\Cacheable;
use Isswp101\Persimmon\Traits\Eventable;
use Isswp101\Persimmon\Traits\Fillable;
use Isswp101\Persimmon\Traits\Idable;
use Isswp101\Persimmon\Traits\Mergeable;
use Isswp101\Persimmon\Traits\Presentable;
use Isswp101\Persimmon\Traits\Timestampable;
use Isswp101\Persimmon\Traits\Userable;
use JsonSerializable;
use LogicException;

abstract class Model implements Arrayable, Jsonable, Stringable, JsonSerializable
{
    use Presentable, Eventable, Mergeable;
    use Idable, Userable, Timestampable;
    use Fillable, Cacheable;

    /**
     * @var IDAL
     */
    public $_dal;

    /**
     * @var bool
     */
    public $_exist = false;

    /**
     * Create a new instance.
     *
     * @param IDAL $dal
     * @param array $attributes
     */
    public function __construct(IDAL $dal, array $attributes = [])
    {
        $this->_dal = $dal;

        $this->fill($attributes);
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        return array_where(get_object_vars($this), function ($value, $key) {
            return !starts_with($key, '_');
        });
    }

    /**
     * {@inheritdoc}
     */
    public function jsonSerialize()
    {
        return $this->toArray();
    }

    /**
     * Clone object.
     */
    public function __clone()
    {
        $this->_dal = clone $this->_dal;
        $this->_dal->setModel($this);
    }

    /**
     * Save the model.
     *
     * @param array $columns
     * @return bool
     */
    public function save($columns = ['*'])
    {
        $columns = $columns ? (array)$columns : ['*'];

        if ($this->saving() === false) {
            return false;
        }

        $this->fillTimestamp();

        $this->_dal->put($columns);

        $this->_exist = true;

        // self::cache()->put($id, $this);

        if ($this->saved() === false) {
            return false;
        }

        return true;
    }

    /**
     * Delete the model.
     *
     * @return bool
     */
    public function delete()
    {
        if ($this->deleting() === false) {
            return false;
        }

        $this->_dal->delete();

        $this->_exist = false;

        $cache = self::cache();
        $cache->forget($this->getId());

        if ($this->deleted() === false) {
            return false;
        }

        return true;
    }

    /**
     * Create a new instance.
     *
     * @return static
     */
    public static function createInstance()
    {
        throw new LogicException('Static method `createInstance()` must be overridden and return `new static(args)`');
    }

    /**
     * Find a model by its primary key.
     *
     * @param mixed $id
     * @param array $columns
     * @param array $options
     * @return static
     */
    public static function find($id, array $columns = ['*'], $options = [])
    {
        // Return a cached instance if one exists
        $cache = self::cache();
        if ($cache->containsAttributes($id, $columns)) {
            return $cache->get($id);
        }

        // Return attributes which are not cached
        $columns = $cache->getNotCachedAttributes($id, $columns);

        // Create a new model
        $model = static::createInstance();
        $model->setId($id);

        // Merge options
        $options = array_merge($options, ['columns' => $columns == ['*'] ? [] : $columns]);

        // Get by id
        try {
            $model->_dal->get($id, $options);
        } catch (Missing404Exception $e) {
            return null;
        }

        // Fill internal attributes
        $model->_exist = true;

        // Put model to cache
        $model = $cache->put($id, $model, $columns);

        return $model;
    }

    /**
     * Find a model by its primary key or return new model.
     *
     * @param mixed $id
     * @return static
     */
    public static function findOrNew($id)
    {
        $model = static::find($id);
        if (is_null($model)) {
            $model = static::createInstance();
            $model->setId($id);
        }
        return $model;
    }

    /**
     * Find a model by its primary key or throw an exception.
     *
     * @param mixed $id
     * @param array $columns
     * @param int $parent
     * @return static
     */
    public static function findOrFail($id, array $columns = ['*'], $parent = null)
    {
        $model = static::find($id, $columns, ['parent' => $parent]);
        if (is_null($model)) {
            throw new ModelNotFoundException(get_called_class(), $id);
        }
        return $model;
    }

    /**
     * Save a new model and return the instance.
     *
     * @param array $attributes
     * @throws Exception
     * @return static
     */
    public static function create(array $attributes = [])
    {
        $model = static::createInstance();

        if (array_key_exists('id', $attributes)) {
            $model->setId($attributes['id']);
        }

        $model->fill($attributes);

        $model->save();

        return $model;
    }

    /**
     * Destroy the models by the given id.
     *
     * @param mixed $id
     */
    public static function destroy($id)
    {
        $ids = is_array($id) ? $id : [$id];
        foreach ($ids as $id) {
            $model = static::find($id);
            if (!is_null($model)) {
                $model->delete();
            }
        }
    }
}


<?php

namespace Isswp101\Persimmon\Traits;

use Exception;
use Isswp101\Persimmon\ElasticsearchModel;
use Isswp101\Persimmon\QueryBuilder\QueryBuilder;

trait Paginationable
{
    /**
     * @var ElasticsearchModel
     */
    public $_previous = null;

    /**
     * @var ElasticsearchModel
     */
    public $_next = null;

    /**
     * Search documents and find previous and next documents.
     *
     * @param QueryBuilder $query Query
     * @throws Exception
     */
    public function makePagination(QueryBuilder $query = null)
    {
        if (is_null($this->_position)) {
            throw new Exception('To use Paginationable trait you must fill _position property in your model');
        }

        /** @var ElasticsearchModel $model */
        $model = static::createInstance();

        $query = $query ?: new QueryBuilder();

        $prevDoc = null;
        $nextDoc = null;

        $query->fields();
        $prevPos = $this->_position - 1;

        if ($prevPos >= 0) {
            $items = $model->search($query->from($prevPos)->size(3));
            $prevDoc = $items->first();
            $items = array_values($items->toArray());
            if (array_key_exists(2, $items)) {
                $nextDoc = $items[2];
            }
        } else {
            $items = $model->search($query->from($this->_position)->size(2));
            $total = $items->getTotal();
            $nextDoc = $items->last();

            $last = $total - 1;
            $items = $model->search($query->from($last)->size(1));
            $prevDoc = $items->first();
        }

        if (!$nextDoc) {
            $items = $model->search($query->from(0)->size(1));
            $nextDoc = $items->first();
        }

        $this->_previous = $prevDoc;
        $this->_next = $nextDoc;
    }

    /**
     * Return previous document.
     *
     * @return ElasticsearchModel
     */
    public function getPrevious()
    {
        return $this->_previous;
    }

    /**
     * Return next document.
     *
     * @return ElasticsearchModel
     */
    public function getNext()
    {
        return $this->_next;
    }
}


<?php

namespace Isswp101\Persimmon\Traits;

use Isswp101\Persimmon\ElasticsearchModel;

trait Relationshipable
{
    /**
     * @var ElasticsearchModel
     */
    protected $_parent;

    /**
     * @var mixed
     */
    protected $_parentId;

    /**
     * @return mixed
     */
    public function getParentId()
    {
        return $this->_parentId;
    }

    /**
     * @param mixed $id
     */
    public function setParentId($id)
    {
        $this->_parentId = $id;
    }

    /**
     * Return parent document.
     *
     * @return static
     */
    public function getParent()
    {
        return $this->_parent;
    }

    /**
     * Set parent document.
     *
     * @param ElasticsearchModel $parent
     */
    public function setParent(ElasticsearchModel $parent)
    {
        $this->_parent = $parent;
        $this->setParentId($parent->getId());
    }
}


<?php

namespace Isswp101\Persimmon;

use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Isswp101\Persimmon\Collection\ElasticsearchCollection;
use Isswp101\Persimmon\DAL\ElasticsearchDAL;
use Isswp101\Persimmon\DAL\IDAL;
use Isswp101\Persimmon\Exceptions\ModelNotFoundException;
use Isswp101\Persimmon\QueryBuilder\QueryBuilder;
use Isswp101\Persimmon\Relationship\BelongsToRelationship;
use Isswp101\Persimmon\Relationship\HasManyRelationship;
use Isswp101\Persimmon\Traits\Elasticsearchable;
use Isswp101\Persimmon\Traits\Paginationable;
use Isswp101\Persimmon\Traits\Relationshipable;

class ElasticsearchModel extends Model
{
    use Elasticsearchable, Paginationable, Relationshipable;

    /**
     * @var ElasticsearchDAL
     */
    public $_dal;

    public function __construct(IDAL $dal, array $attributes = [])
    {
        $this->validateModelEndpoint();

        parent::__construct($dal, $attributes);
    }

    public static function findWithParentId($id, $parent, array $columns = ['*'])
    {
        return static::find($id, $columns, ['parent' => $parent]);
    }

    /**
     * Execute the query and get the result.
     *
     * @param QueryBuilder|array $query
     * @return ElasticsearchCollection|static[]
     */
    public static function search($query = [])
    {
        if ($query instanceof QueryBuilder) {
            $query = $query->build();
        }
        $model = static::createInstance();
        return $model->_dal->search($query);
    }

    /**
     * Execute the query and get the first result.
     *
     * @param QueryBuilder|array $query
     * @return static
     */
    public static function first($query = [])
    {
        if ($query instanceof QueryBuilder) {
            $query = $query->build();
        }
        $query['from'] = 0;
        $query['size'] = 1;
        return static::search($query)->first();
    }

    /**
     * Execute the query and get the first result or throw an exception.
     *
     * @param QueryBuilder|array $query
     * @throws ModelNotFoundException
     * @return static
     */
    public static function firstOrFail($query = [])
    {
        $model = static::first($query);
        if (is_null($model)) {
            throw new ModelNotFoundException(get_called_class());
        }
        return $model;
    }

    /**
     * Apply the callback to the documents of the given query.
     *
     * @param QueryBuilder|array $query
     * @param callable $callback
     * @param int $limit
     * @return int hits.total
     */
    public static function map($query = [], callable $callback = null, $limit = -1)
    {
        if ($query instanceof QueryBuilder) {
            $query = $query->build();
        }

        $query['from'] = Arr::get($query, 'from', 0);
        $query['size'] = Arr::get($query, 'size', 50);

        $i = 0;
        $models = static::search($query);
        $total = $models->getTotal();
        while ($models) {
            foreach ($models as $model) {
                if ($callback) {
                    $callback($model);
                }
                $i++;
            }

            $query['from'] += $query['size'];

            if ($i >= $total || ($limit > 0 && $i >= $limit)) {
                break;
            }

            $models = static::search($query);
        }

        return $total;
    }

    /**
     * Execute the query and get all items.
     *
     * @param QueryBuilder|array $query
     * @return Collection|static[]
     */
    public static function all($query = [])
    {
        if ($query instanceof QueryBuilder) {
            $query = $query->build();
        }
        $collection = collect();
        static::map($query, function (ElasticsearchModel $document) use ($collection) {
            $collection->put($document->getId(), $document);
        });
        return $collection;
    }

    protected function belongsTo($class)
    {
        return new BelongsToRelationship($this, $class);
    }

    protected function hasMany($class)
    {
        return new HasManyRelationship($this, $class);
    }
}


<?php

namespace Isswp101\Persimmon\QueryBuilder\Filters;

abstract class Filter
{
    const MODE_INCLUDE = 'include';
    const MODE_EXCLUDE = 'exclude';
    const MODE_OFF = 'off';

    const MERGE_AND = 'AND';
    const MERGE_OR = 'OR';

    /**
     * Filter mode.
     *
     * @var string Can be as include, exclude or off.
     */
    protected $mode;

    /**
     * Filter values.
     *
     * @var mixed
     */
    protected $values;

    /**
     * This array contains linked instances of current filter.
     *
     * @var array
     */
    protected $linkedFilters = [];

    /**
     * Filters are merged using AND (must) by default.
     * But you can override it and merge them using OR (should).
     *
     * @var string
     */
    protected $mergeType = Filter::MERGE_AND;

    /**
     * Linked filters are merged using AND (must) by default.
     * But you can override it and merge them using OR (should).
     *
     * @var string
     */
    protected $logicalOperator = Filter::MERGE_OR;

    /**
     * Constructor.
     *
     * @param mixed $values Filter values.
     */
    public function __construct($values = null)
    {
        $this->values = $values;
    }

    /**
     * Set filter options.
     *
     * @param string $mode
     * @param string $logicalOperator
     * @param array $linkedFilters
     * @return $this
     */
    public function setOptions($mode = null, $logicalOperator = null, array $linkedFilters = [])
    {
        $this->mode = is_null($mode) ? Filter::MODE_INCLUDE : $mode;
        $this->logicalOperator = is_null($logicalOperator) ? Filter::MERGE_OR : $logicalOperator;
        $this->linkedFilters = $linkedFilters;
        return $this;
    }

    /**
     * Returns the actual elasticsearch query for one filter.
     * {
     *   "term": {
     *     "price": "0"
     *   }
     * }
     *
     * @param mixed $values
     * @return array
     */
    abstract public function query($values);

    /**
     * Returns wrapped elasticsearch filter query.
     * {
     *   "bool": {
     *     "should": [],
     *     "must_not": []
     *   }
     * }
     *
     * @return array
     */
    public function makeQuery()
    {
        $query = $this->query($this->getValues());

        $map = [
            Filter::MERGE_AND => 'must',
            Filter::MERGE_OR => 'should'
        ];

        if ($this->isInclude()) {
            if ($this->getLogicalOperator() == Filter::MERGE_AND) {
                $query = [
                    'bool' => [
                        'must' => [
                            $query
                        ]
                    ]
                ];
            } elseif ($this->getLogicalOperator() == Filter::MERGE_OR) {
                $query = [
                    'bool' => [
                        'should' => [
                            $query
                        ]
                    ]
                ];
            }
        } elseif ($this->isExclude()) {
            $mergeOperator = $map[$this->getLogicalOperator()];
            $query = [
                'bool' => [
                    $mergeOperator => [
                        [
                            'bool' => [
                                'must_not' => [
                                    $query
                                ]
                            ]
                        ]
                    ]
                ]
            ];
        } elseif ($this->isOff()) {
            $query = [];
        }

        foreach ($this->getLinkedFilters() as $filter) {
            $extraQuery = $filter->makeQuery();

//            if ($filter->isInclude()) {
            if ($this->getLogicalOperator() == Filter::MERGE_AND) {
                $query = $this->mergeBoolQuery($query, $extraQuery, 'must');
            } elseif ($this->getLogicalOperator() == Filter::MERGE_OR) {
                $query = $this->mergeBoolQuery($query, $extraQuery, 'should');
            }
//            }
//            elseif ($filter->isExclude()) {
//                $query = $this->mergeBoolQuery($query, $extraQuery, 'must_not');
//            }
        }

        return $query;
    }

    /**
     * Merges elastcisearch query with current filter query.
     *
     * @param array $query Elastcisearch query.
     * @return array Merged elasticsearch query.
     */
    public function mergeQuery(array $query)
    {
        $types = [
            Filter::MERGE_AND => 'must',
            Filter::MERGE_OR => 'should'
        ];

        $type = $this->getMergeType();

        $query['body']['filter']['bool'][$types[$type]][] = $this->makeQuery();

        return $query;
    }

    /**
     * Returns filter mode.
     *
     * @return string Can be as include, exclude or off.
     */
    public function getMode()
    {
        return $this->mode;
    }

    /**
     * Returns true if mode is include.
     *
     * @return bool
     */
    public function isInclude()
    {
        return $this->getMode() == Filter::MODE_INCLUDE;
    }

    /**
     * Returns true if mode is exclude.
     *
     * @return bool
     */
    public function isExclude()
    {
        return $this->getMode() == Filter::MODE_EXCLUDE;
    }

    /**
     * Returns true if mode is off.
     *
     * @return bool
     */
    public function isOff()
    {
        return $this->getMode() == Filter::MODE_OFF;
    }

    /**
     * Retuns filter values as array.
     *
     * @return mixed
     */
    public function getValues()
    {
        return $this->values;
    }

    /**
     * Returns linked instances of current filter.
     *
     * @return Filter[]
     */
    public function getLinkedFilters()
    {
        return $this->linkedFilters;
    }

    /**
     * Returns true if filter has linked filters.
     *
     * @return bool
     */
    public function hasLinkedFilters()
    {
        return !empty($this->linkedFilters);
    }

    /**
     * Returns filter merge type.
     *
     * @return string - "AND" || "OR"
     */
    public function getMergeType()
    {
        return $this->mergeType;
    }

    /**
     * Sets filter merge type.
     *
     * @param string $mergeType - "AND" || "OR"
     */
    public function setMergeType($mergeType)
    {
        $this->mergeType = $mergeType;
    }

    /**
     * Returns linked filters logical operator.
     *
     * @return string - "AND" || "OR"
     */
    public function getLogicalOperator()
    {
        return $this->logicalOperator;
    }

    /**
     * Updates linked filters logical operator.
     *
     * @param string $logicalOperator - "AND" || "OR"
     */
    public function setLogicalOperator($logicalOperator)
    {
        $this->logicalOperator = $logicalOperator;
    }

    /**
     * Returns true if merge type is valid.
     * @return bool
     */
    public function isMergeTypeValid()
    {
        return in_array($this->mergeType, [Filter::MERGE_AND, Filter::MERGE_OR]);
    }

    /**
     * Merges BOOL elasticsearch queries.
     *
     * @param array $query1
     * @param array $query2
     * @param string $type must, must_not, should
     * @return array
     */
    protected function mergeBoolQuery(array $query1, array $query2, $type)
    {
        if (empty($query2['bool'][$type])) {
            return $query1;
        } else {
            if (empty($query1['bool'][$type])) {
                $query1['bool'][$type] = [];
            }
        }

        $query1['bool'][$type] = array_merge($query1['bool'][$type], $query2['bool'][$type]);

        return $query1;
    }
}


<?php

namespace Isswp101\Persimmon\QueryBuilder;

use Isswp101\Persimmon\QueryBuilder\Aggregations\Aggregation;
use Isswp101\Persimmon\QueryBuilder\Filters\Filter;

class QueryBuilder
{
    /**
     * Query.
     *
     * @var array
     */
    protected $query = [];

    /**
     * Constructor.
     *
     * @param array $body
     */
    public function __construct(array $body = [])
    {
        $this->query['body'] = $body;
    }

    /**
     * @param int $from
     * @return $this
     */
    public function from($from)
    {
        $this->query['from'] = $from;
        return $this;
    }

    /**
     * @param int $size
     * @return $this
     */
    public function size($size)
    {
        $this->query['size'] = $size;
        return $this;
    }

    /**
     * @return bool
     */
    public function hasSort()
    {
        return !empty($this->query['body']['sort']);
    }

    /**
     * @param array|string $sort
     * @return $this
     */
    public function sort($sort)
    {
        $this->query['body']['sort'] = $sort;
        return $this;
    }

    /**
     * @param Filter|array $filter
     * @param string $mode
     * @return $this
     */
    public function filter($filter = [], $mode = Filter::MODE_INCLUDE)
    {
        if (!$filter) {
            unset($this->query['body']['filter']);
            return $this;
        }

        if ($filter instanceof Filter) {
            $filter = $filter->makeQuery();
        }

        $map = [
            Filter::MODE_INCLUDE => 'must',
            Filter::MODE_EXCLUDE => 'must_not',
            'should' => 'should'
        ];

        $mode = $map[$mode];

        $this->merge($filter, $mode);

        return $this;
    }

    /**
     * Set _source to search query.
     *
     * @param mixed $fields
     * @return $this
     */
    public function fields($fields = false)
    {
        if ($fields === false) {
            $this->query['body']['_source'] = false;
        } elseif ((array)$fields == ['*']) {
            unset($this->query['body']['_source']);
        } else {
            $this->query['body']['_source'] = $fields;
        }
        return $this;
    }

    /**
     * Return only _id.
     *
     * @return $this
     */
    public function getOnlyIds()
    {
        return $this->fields(false);
    }

    /**
     * Return query.
     *
     * @return array
     */
    protected function getQuery()
    {
        return $this->query;
    }

    /**
     * Build query.
     *
     * @return array
     */
    public function build()
    {
        return $this->getQuery();
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->toJson();
    }

    /**
     * @param int $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->getQuery(), $options);
    }

    protected function merge(array $query, $mode = 'must')
    {
        if (!array_key_exists('filter', $this->query['body'])) {
            $this->query['body']['filter']['bool'][$mode] = [];
        }

        $this->query['body']['filter']['bool'][$mode][] = $query;

        return $this;
    }

    public function aggregation(Aggregation $aggregation)
    {
        $this->query['body']['aggs'][$aggregation->getName()] = $aggregation->make();
        return $this;
    }

    public function where($field, $value)
    {
        $query = ['term' => [$field => $value]];
        $this->merge($query);
        return $this;
    }

    public function notWhere($field, $value)
    {
        $query = ['term' => [$field => $value]];
        $this->merge($query, 'must_not');
        return $this;
    }

    public function orWhere($field, $value)
    {
        $query = ['term' => [$field => $value]];
        $this->merge($query, 'should');
        return $this;
    }

    public function between($field, $start, $end)
    {
        $query = ['range' => [$field => ['gt' => $start, 'lt' => $end]]];
        $this->merge($query);
        return $this;
    }

    public function betweenOrEquals($field, $start, $end)
    {
        $query = ['range' => [$field => ['gte' => $start, 'lte' => $end]]];
        $this->merge($query);
        return $this;
    }

    public function range($field, $start, $end)
    {
        return $this->betweenOrEquals($field, $start, $end);
    }

    public function greaterThan($field, $start)
    {
        $query = ['range' => [$field => ['gt' => $start]]];
        $this->merge($query);
        return $this;
    }

    public function gt($field, $start)
    {
        return $this->greaterThan($field, $start);
    }

    public function greaterThanOrEquals($field, $start)
    {
        $query = ['range' => [$field => ['gte' => $start]]];
        $this->merge($query);
        return $this;
    }

    public function gte($field, $start)
    {
        return $this->greaterThanOrEquals($field, $start);
    }

    public function lessThan($field, $end)
    {
        $query = ['range' => [$field => ['lt' => $end]]];
        $this->merge($query);
        return $this;
    }

    public function lt($field, $start)
    {
        return $this->lessThan($field, $start);
    }

    public function lessThanOrEquals($field, $end)
    {
        $query = ['range' => [$field => ['lte' => $end]]];
        $this->merge($query);
        return $this;
    }

    public function lte($field, $start)
    {
        return $this->lessThanOrEquals($field, $start);
    }

    public function match($field, $value)
    {
        $query = ['query' => ['match' => [$field => $value]]];
        $this->merge($query);
        return $this;
    }

    public function notMatch($field, $value)
    {
        $query = ['query' => ['match' => [$field => $value]]];
        $this->merge($query, 'must_not');
        return $this;
    }

    public function orMatch($field, $value)
    {
        $query = ['query' => ['match' => [$field => $value]]];
        $this->merge($query, 'should');
        return $this;
    }
}


<?php

namespace Isswp101\Persimmon\Cache;

use Isswp101\Persimmon\Model;

class RuntimeCache
{
    /**
     * Cache.
     *
     * @var array [
     *   'instance' => Model,
     *   'attributes' => []
     * ]
     */
    private $cache = [];

    /**
     * Return true if cache contains this key.
     *
     * @param mixed $key
     * @return bool
     */
    public function has($key)
    {
        return array_key_exists($key, $this->cache);
    }

    /**
     * Return instance from cache.
     *
     * @param mixed $key
     * @return Model
     */
    public function get($key)
    {
        return $this->has($key) ? $this->cache[$key]['instance'] : null;
    }

    /**
     * Return all cache keys.
     *
     * @return array
     */
    public function keys()
    {
        return array_keys($this->cache);
    }

    /**
     * Put instance to cache.
     *
     * @param mixed $key
     * @param Model $instance
     * @param array $attributes
     * @return Model
     */
    public function put($key, Model $instance, array $attributes = ['*'])
    {
        if ($attributes != ['*'] && $this->has($key)) {
            $instance = Model::merge($this->cache[$key]['instance'], $instance, $attributes);
            $attributes = array_merge($this->cache[$key]['attributes'], $attributes);
        }

        $this->cache[$key] = [
            'instance' => $instance,
            'attributes' => $attributes
        ];

        return $instance;
    }

    /**
     * Return true if cache has already given attributes.
     *
     * @param mixed $key
     * @param array $attributes
     * @return bool
     */
    public function containsAttributes($key, array $attributes = ['*'])
    {
        return empty($this->getNotCachedAttributes($key, $attributes));
    }

    /**
     * Return the difference between given attributes and attributes which are already cached.
     *
     * @param mixed $key
     * @param array $attributes
     * @return array
     */
    public function getNotCachedAttributes($key, array $attributes = ['*'])
    {
        if (!$this->has($key)) {
            return $attributes;
        }
        $cachedAttributes = $this->cache[$key]['attributes'];
        return $cachedAttributes == ['*'] ? [] : array_diff($attributes, $cachedAttributes);
    }

    /**
     * Remove an item from the cache by key.
     *
     * @param mixed $key
     * @return $this
     */
    public function forget($key)
    {
        unset($this->cache[$key]);
        return $this;
    }
}


<?php

namespace Isswp101\Persimmon\Traits;

use Isswp101\Persimmon\Model;

trait Mergeable
{
    /**
     * Merge models.
     *
     * @param Model $model1
     * @param Model $model2
     * @param array $attributes
     * @return Model
     */
    public static function merge(Model $model1, Model $model2, array $attributes)
    {
        foreach ($attributes as $attribute) {
            $model1->$attribute = $model2->$attribute;
        }
        return $model1;
    }
}


<?php

namespace DetectLanguage;

class Client
{
    /**
     * Request engine.
     *
     * @var string Request engine (curl or stream).
     */
    public static $requestEngine = 'curl';

    /**
     * Request timeout.
     *
     * @var int
     */
    public static $requestTimeout = 60;

    /**
     * Connect timeout.
     *
     * @var int
     */
    public static $connectTimeout = 10;

    /**
     * Perform a request
     *
     * @param string $method Method name
     * @param array $params The parameters to use for the POST body
     *
     * @return object
     */
    public static function request($method, $params = null)
    {
        $url = self::getUrl($method);

        $request_method = self::getRequestMethodName();
        $response_body = self::$request_method($url, $params);
        $response = json_decode($response_body);

        if (!is_object($response))
            throw new Error("Invalid server response: $response_body");

        if (isset($response->error))
            throw new Error($response->error->message);

        return $response;
    }

    /**
     * Get request method name.
     *
     * @return string
     */
    protected static function getRequestMethodName()
    {
        $request_engine = self::$requestEngine;

        if ($request_engine == 'curl' && !function_exists('curl_init')) {
            trigger_error("DetectLanguage::Client - CURL not found, switching to stream");
            $request_engine = self::$requestEngine = 'stream';
        }

        switch ($request_engine) {
            case 'curl':
                return 'requestCurl';

            case 'stream':
                return 'requestStream';

            default:
                throw new Error("Invalid request engine: " . $request_engine);
        }
    }

    /**
     * Perform request using native PHP streams
     *
     * @param string $url Request URL
     * @param array $params The parameters to use for the POST body
     *
     * @return string Response body
     */
    protected static function requestStream($url, $params)
    {
        $opts = array('http' =>
            array(
                'method' => 'POST',
                'header' => implode("\n", self::getHeaders()),
                'content' => json_encode($params),
                'timeout' => self::$requestTimeout,
                'ignore_errors' => true,
            )
        );

        $context = stream_context_create($opts);

        return file_get_contents($url, false, $context);
    }

    /**
     * Perform request using CURL extension.
     *
     * @param string $url Request URL
     * @param array $params The parameters to use for the POST body
     *
     * @return string Response body
     */
    protected static function requestCurl($url, $params)
    {
        $ch = curl_init();

        $options = array(
            CURLOPT_URL => $url,
            CURLOPT_HTTPHEADER => self::getHeaders(),
            CURLOPT_POSTFIELDS => json_encode($params),
            CURLOPT_CONNECTTIMEOUT => self::$connectTimeout,
            CURLOPT_TIMEOUT => self::$requestTimeout,
            CURLOPT_USERAGENT => self::getUserAgent(),
            CURLOPT_RETURNTRANSFER => true
        );

        curl_setopt_array($ch, $options);

        $result = curl_exec($ch);

        if ($result === false) {
            $e = new Error(curl_error($ch));
            curl_close($ch);
            throw $e;
        }

        curl_close($ch);

        return $result;
    }

    /**
     * Build URL for given method
     *
     * @param string $method Method name
     * @return string
     */
    protected static function getUrl($method)
    {
        return self::getProtocol() . '://' . DetectLanguage::$host . '/' . DetectLanguage::$apiVersion . '/' . $method;
    }

    protected static function getProtocol()
    {
        return DetectLanguage::$secure ? 'https' : 'http';
    }

    /**
     * Build request headers.
     *
     * @return string
     */
    protected static function getHeaders()
    {
        return array(
            "Content-Type: application/json",
            "Accept-Encoding: gzip, deflate",
            "User-Agent: " . self::getUserAgent(),
            "Authorization: Bearer " . DetectLanguage::$apiKey
        );
    }

    protected static function getUserAgent()
    {
        return 'detectlanguage-php-' . DetectLanguage::VERSION;
    }
}


<?php

namespace DetectLanguage;

if (!function_exists('json_decode')) {
    throw new Error('DetectLanguage needs the JSON PHP extension.');
}

class DetectLanguage
{
    /**
     * The API key.
     *
     * @static
     * @var string
     */
    public static $apiKey;

    /**
     * API host.
     *
     * @static
     * @var string
     */
    public static $host = 'ws.detectlanguage.com';

    /**
     * API version.
     *
     * @static
     * @var string
     */
    public static $apiVersion = '0.2';

    /**
     * Enable secure mode (SSL).
     *
     * @static
     * @var boolean
     */
    public static $secure;

    /**
     * API Client Version.
     */
    const VERSION = '2.2.1';

    /**
     * Set API key
     *
     * @static
     * @var string
     */
    public static function setApiKey($apiKey)
    {
        self::$apiKey = $apiKey;
    }

    /**
     * Set secure mode
     *
     * @static
     * @var boolean
     */
    public static function setSecure($secure)
    {
        self::$secure = $secure;
    }

    /**
     * Detect text language.
     *
     * @static
     * @param string @text The text for language detection
     * @return array detected languages information
     */
    public static function detect($text)
    {
        $result = Client::request('detect', array('q' => $text));

        return $result->data->detections;
    }

    /**
     * Simple detection. If you need just the language code.
     *
     * @static
     * @param string @text The text for language detection
     * @return string detected language code
     */
    public static function simpleDetect($text)
    {
        $detections = self::detect($text);

        if (count($detections) > 0)
            return $detections[0]->language;
        else
            return null;
    }

    public static function getStatus()
    {
        return Client::request('user/status');
    }
}