<?php

namespace Osedea\LaravelRest\Commands;

use Osedea\LaravelRest\Exceptions\InvalidFilterException;
use Illuminate\Support\Facades\Request;

abstract class Command
{
    /*
     * List of url parameters that cannot be used for filtering
     */
    private $keywordParams = [
        'sort',
        'count',
        'embed',
        'page',
        'fields',
        'perPage'
    ];

    /**
     * This method can be overriden to return an array of commands to run before.
     * You need to specify the full command class (with namespace), so it's better to use the stdobject::class notation.
     * This also helps code naviguation in an IDE.
     * It is using the built-in Command Pipeline, more info here: http://laravel.com/docs/5.0/bus#command-pipeline
     *
     * Example: return [Lucy\Commands\ExampleCommand::class];
     *
     * @return array
     */
    public static function beforeCommands()
    {
        return [];
    }

    /**
     * Returns an integer for pagination based on a model's class.
     *
     * @param  string $modelClass A model's class.
     * @return int The number of items per page for the given model.
     */
    protected function getPerPageFromModelClass($modelClass) {
        // We need this empty model to grab perPage and perPageMax parameters
        $model = new $modelClass;

        // If Model::$perPage equals 0, then pagination is disabled
        if ($model->getPerPage() == 0) {
            return $modelClass::all();
        }

        // The number of items per page can be overriden as a query parameter
        $perPage = Request::input('perPage', $model->getPerPage());

        // We make sure the $perPage parameter is not higher than the maximum for this model
        if ($perPage > $model->getPerPageMax()) {
            $perPage = $model->getPerPageMax();
        }

        return $perPage;
    }

    /**
     * This method returns an array of fields parsed from the request. `[*]` is returned by default (all fields).
     * Fields can be written in snake_case or camelCase. They are converted to snake_case automatically.
     *
     * Example: `?fields=column1,column2` is returned as `['column1', 'column2']`
     *
     * @return array
     */
    protected function getFieldsFromRequest($prefix = null)
    {
        if (Request::has('fields')) {
            $prefix = $prefix ? $prefix . '.' : '';

            return array_map(function($field) use ($prefix) {
                return $prefix . snake_case($field);
            }, explode(',', Request::get('fields')));
        }

        return ['*'];
    }

    /**
     * Loop through Request params, creating `where` statements.
     *
     * @param $query
     * @return mixed
     */
    protected function addWhereStatements($query, $class)
    {
        $params = Request::all();

        foreach ($params as $key => $value) {
            if (!in_array($key, $this->keywordParams) && !in_array($key, $class::$filterable)) {
                throw new InvalidFilterException;
            }

            if (!in_array($key, $this->keywordParams)) {
                $query->where($key, '=', $value);
            }
        }

        return $query;
    }
}


<?php

namespace Osedea\LaravelRest\Commands\DefaultCommand;

use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Support\Facades\Request;
use Osedea\LaravelRest\Commands\Command;

class IndexCommand extends Command implements SelfHandling
{
    public $modelClass;

    /**
     * @param $modelClass
     */
    function __construct($modelClass)
    {
        $this->modelClass = $modelClass;
    }

    /**
     * Execute the command.
     *
     * @return \Illuminate\Database\Eloquent\Model
     */
    public function handle()
    {
        $class = $this->modelClass;
        $perPage = $this->getPerPageFromModelClass($class);
        $fields = $this->getFieldsFromRequest();

        $query = $class::withRequestSort()->withRequestEmbed();
        $query = $this->addWhereStatements($query, $class);

        // If `count` param is present, return early since we don't want to paginate
        if (Request::get('count')) {
            return [ 'count' => $query->count() ];
        }

        return $query->paginate($perPage, $fields);
    }
}


<?php

namespace Osedea\LaravelRest;

use Illuminate\Support\ServiceProvider;
use Illuminate\Routing\Router;

class LaravelRestServiceProvider extends ServiceProvider
{
    /**
     * Initialises the service provider.
     */
    public function boot()
    {
        $this->publishes([
            implode(DIRECTORY_SEPARATOR, [__DIR__, 'config', 'api.php']) => config_path('api.php')
        ]);

        $this->setupRoutes($this->app->router);
    }

    /**
     * Define the routes for the application.
     *
     * @param  \Illuminate\Routing\Router  $router
     * @return void
     */
    public function setupRoutes(Router $router)
    {
        $router->group(['namespace' => 'Osedea\LaravelRest\Http\Controllers'], function ($router)
        {
            require implode(DIRECTORY_SEPARATOR, [__DIR__, 'Http', 'routes.php']);
        });
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        // Nothing to see here!
    }
}


<?php

namespace Osedea\LaravelRest\Commands\DefaultCommand;

use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Support\Facades\Request;
use Osedea\LaravelRest\Commands\Command;

class RelationIndexCommand extends Command implements SelfHandling
{
    public $modelClass;
    public $id;
    public $relation;
    public $relationClass;

    /**
     * @param $modelClass
     * @param $id
     * @param $relation
     * @param $relationClass
     */
    function __construct($modelClass, $id, $relation, $relationClass)
    {
        $this->modelClass = $modelClass;
        $this->id = $id;
        $this->relation = $relation;
        $this->relationClass = $relationClass;
    }

    /**
     * Execute the command.
     *
     * @return \Illuminate\Database\Eloquent\Model
     */
    public function handle()
    {
        $class = $this->modelClass;
        $relation = $this->relation;
        $relationClass = $this->relationClass;

        $model = $class::findOrFail($this->id);

        $perPage = $this->getPerPageFromModelClass($relationClass);
        $fields = $this->getFieldsFromRequest($relation);

        $query = $model->$relation();
        $query = $this->addWhereStatements($query, $class);

        // If `count` param is present, return early since we don't want to sort or embed
        if (Request::get('count')) {
            return [ 'count' => $query->count() ];
        }

        return $query->withRequestSort()->withRequestEmbed()->paginate($perPage, $fields);
    }
}


<?php
namespace Lead\Router;

use Closure;

/**
 * The Route class.
 */
class Route
{
    const FOUND = 0;

    const NOT_FOUND = 404;

    /**
     * Class dependencies.
     *
     * @var array
     */
    protected $_classes = [];

    /**
     * The route's error.
     *
     * @var integer
     */
    protected $_error = 0;

    /**
     * The route's message.
     *
     * @var string
     */
    protected $_message = 'OK';

    /**
     * Route's name.
     *
     * @var string
     */
    public $name = '';

    /**
     * Named parameter.
     *
     * @var array
     */
    public $params = [];

    /**
     * List of parameters that should persist during dispatching.
     *
     * @var array
     */
    public $persist = [];

    /**
     * The attached namespace.
     *
     * @var string
     */
    public $namespace = '';

    /**
     * The attached request.
     *
     * @var mixed
     */
    public $request = null;

    /**
     * The attached response.
     *
     * @var mixed
     */
    public $response = null;

    /**
     * The dispatched instance (custom).
     *
     * @var object
     */
    public $dispatched = null;

    /**
     * The route scope.
     *
     * @var array
     */
    protected $_scope = null;

    /**
     * The route's host.
     *
     * @var object
     */
    protected $_host = null;

    /**
     * Route's allowed methods.
     *
     * @var array
     */
    protected $_methods = [];

    /**
     * Route's prefix.
     *
     * @var array
     */
    protected $_prefix = '';

    /**
     * Route's pattern.
     *
     * @var string
     */
    protected $_pattern = '';

    /**
     * The tokens structure extracted from route's pattern.
     *
     * @see Parser::tokenize()
     * @var array
     */
    protected $_token = null;

    /**
     * The route's regular expression pattern.
     *
     * @see Parser::compile()
     * @var string
     */
    protected $_regex = null;

    /**
     * The route's variables.
     *
     * @see Parser::compile()
     * @var array
     */
    protected $_variables = null;

    /**
     * The route's handler to execute when a request match.
     *
     * @var Closure
     */
    protected $_handler = null;

    /**
     * The middlewares.
     *
     * @var array
     */
    protected $_middleware = [];

    /**
     * Constructs a route
     *
     * @param array $config The config array.
     */
    public function __construct($config = [])
    {
        $defaults = [
            'error'          => static::FOUND,
            'message'        => 'OK',
            'scheme'         => '*',
            'host'           => null,
            'methods'        => '*',
            'prefix'         => '',
            'pattern'        => '',
            'name'           => '',
            'namespace'      => '',
            'handler'        => null,
            'params'         => [],
            'persist'        => [],
            'scope'          => null,
            'middleware'     => [],
            'classes'        => [
                'parser' => 'Lead\Router\Parser',
                'host'   => 'Lead\Router\Host'
            ]
        ];
        $config += $defaults;

        $this->name = $config['name'];
        $this->namespace = $config['namespace'];
        $this->params = $config['params'];
        $this->persist = $config['persist'];
        $this->handler($config['handler']);

        $this->_classes = $config['classes'];

        $this->_prefix = trim($config['prefix'], '/');
        if ($this->_prefix) {
            $this->_prefix = '/' . $this->_prefix;
        }

        $this->host($config['host'], $config['scheme']);
        $this->methods($config['methods']);

        $this->_scope = $config['scope'];
        $this->_middleware = (array) $config['middleware'];
        $this->_error = $config['error'];
        $this->_message = $config['message'];

        $this->pattern($config['pattern']);
    }

    /**
     * Gets/sets the route host.
     *
     * @param  object      $host The host instance to set or none to get the setted one.
     * @return object|self       The current host on get or `$this` on set.
     */
    public function host($host = null, $scheme = '*')
    {
        if (!func_num_args()) {
            return $this->_host;
        }
        if (!is_string($host)) {
            $this->_host = $host;
            return $this;
        }
        if ($host !== '*' || $scheme !== '*') {
            $class = $this->_classes['host'];
            $this->_host = new $class(['scheme' => $scheme, 'pattern' => $host]);
        }
        return $this;
    }

    /**
     * Gets/sets the allowed methods.
     *
     * @param  string|array $allowedMethods The allowed methods set or none to get the setted one.
     * @return array|self                   The allowed methods on get or `$this` on set.
     */
    public function methods($methods = null)
    {
        if (!func_num_args()) {
            return array_keys($this->_methods);
        }
        $methods = $methods ? (array) $methods : [];
        $methods = array_map('strtoupper', $methods);
        $this->_methods = array_fill_keys($methods, true);
        return $this;
    }

    /**
     * Allows additionnal methods.
     *
     * @param  string|array $methods The methods to allow.
     * @return self
     */
    public function allow($methods = [])
    {
        $methods = $methods ? (array) $methods : [];
        $methods = array_map('strtoupper', $methods);
        $this->_methods = array_fill_keys($methods, true) + $this->_methods;
        return $this;
    }

    /**
     * Gets/sets the route scope.
     *
     * @param  object      $scope The scope instance to set or none to get the setted one.
     * @return object|self        The current scope on get or `$this` on set.
     */
    public function scope($scope = null)
    {
        if (!func_num_args()) {
            return $this->_scope;
        }
        $this->_scope = $scope;
        return $this;
    }

    /**
     * Gets the routing error number.
     *
     * @return integer The routing error number.
     */
    public function error()
    {
        return $this->_error;
    }

    /**
     * Gets the routing error message.
     *
     * @return string The routing error message.
     */
    public function message()
    {
        return $this->_message;
    }

    /**
     * Gets the route's pattern.
     *
     * @return array The route's pattern.
     */
    public function pattern($pattern = null)
    {
        if (!func_num_args()) {
            return $this->_pattern;
        }
        $this->_token = null;
        $this->_regex = null;
        $this->_variables = null;

        if (!$pattern || $pattern[0] !== '[') {
            $pattern = '/' . trim($pattern, '/');
        }

        $this->_pattern = $this->_prefix . $pattern;
        return $this;
    }

    /**
     * Returns the route's token structures.
     *
     * @return array A collection route's token structure.
     */
    public function token()
    {
        if ($this->_token === null) {
            $parser = $this->_classes['parser'];
            $this->_token = [];
            $this->_regex = null;
            $this->_variables = null;
            $this->_token = $parser::tokenize($this->_pattern, '/');
        }
        return $this->_token;
    }

    /**
     * Gets the route's regular expression pattern.
     *
     * @return string the route's regular expression pattern.
     */
    public function regex()
    {
        if ($this->_regex !== null) {
            return $this->_regex;
        }
        $this->_compile();
        return $this->_regex;
    }

    /**
     * Gets the route's variables and their associated pattern in case of array variables.
     *
     * @return array The route's variables and their associated pattern.
     */
    public function variables()
    {
        if ($this->_variables !== null) {
            return $this->_variables;
        }
        $this->_compile();
        return $this->_variables;
    }

    /**
     * Compiles the route's patten.
     */
    protected function _compile()
    {
        $parser = $this->_classes['parser'];
        $rule = $parser::compile($this->token());
        $this->_regex = $rule[0];
        $this->_variables = $rule[1];
    }

    /**
     * Gets/sets the route's handler.
     *
     * @param  array      $handler The route handler.
     * @return array|self
     */
    public function handler($handler = null)
    {
        if (func_num_args() === 0) {
            return $this->_handler;
        }
        $this->_handler = $handler;
        return $this;
    }

    /**
     * Checks if the route instance matches a request.
     *
     * @param  array   $request a request.
     * @return boolean
     */
    public function match($request, &$variables = null, &$hostVariables = null)
    {
        $hostVariables = [];

        if (($host = $this->host()) && !$host->match($request, $hostVariables)) {
            return false;
        }

        $path = isset($request['path']) ? $request['path'] : '';
        $method = isset($request['method']) ? $request['method'] : '*';

        if (!isset($this->_methods['*']) && $method !== '*' && !isset($this->_methods[$method])) {
            if ($method !== 'HEAD' && !isset($this->_methods['GET'])) {
                return false;
            }
        }

        $path = '/' . trim($path, '/');

        if (!preg_match('~^' . $this->regex() . '$~', $path, $matches)) {
            return false;
        }
        $variables = $this->_buildVariables($matches);
        $this->params = $hostVariables + $variables;
        return true;
    }

    /**
     * Combines route's variables names with the regex matched route's values.
     *
     * @param  array $varNames The variable names array with their corresponding pattern segment when applicable.
     * @param  array $values   The matched values.
     * @return array           The route's variables.
     */
    protected function _buildVariables($values)
    {
        $variables = [];
        $parser = $this->_classes['parser'];

        $i = 1;
        foreach ($this->variables() as $name => $pattern) {
            if (!isset($values[$i])) {
                $variables[$name] = !$pattern ? null : [];
                continue;
            }
            if (!$pattern) {
                $variables[$name] = $values[$i] ?: null;
            } else {
                $token = $parser::tokenize($pattern, '/');
                $rule = $parser::compile($token);
                if (preg_match_all('~' . $rule[0] . '~', $values[$i], $parts)) {
                    foreach ($parts[1] as $value) {
                        if (strpos($value, '/') !== false) {
                            $variables[$name][] = explode('/', $value);
                        } else {
                            $variables[$name][] = $value;
                        }
                    }
                } else {
                    $variables[$name] = [];
                }
            }
            $i++;
        }
        return $variables;
    }

    /**
     * Dispatches the route.
     *
     * @param  mixed $response The outgoing response.
     * @return mixed           The handler return value.
     */
    public function dispatch($response = null)
    {
        if ($error = $this->error()) {
            throw new RouterException($this->message(), $error);
        }
        $this->response = $response;
        $request = $this->request;

        $generator = $this->middleware();

        $next = function() use ($request, $response, $generator, &$next) {
            $handler = $generator->current();
            $generator->next();
            return $handler($request, $response, $next);
        };
        return $next();
    }

    /**
     * Middleware generator.
     *
     * @return callable
     */
    public function middleware()
    {
        foreach ($this->_middleware as $middleware) {
            yield $middleware;
        }

        if ($scope = $this->scope()) {
            foreach ($scope->middleware() as $middleware) {
                yield $middleware;
            }
        }

        yield function() {
            $handler = $this->handler();
            return $handler($this, $this->response);
        };
    }

    /**
     * Adds a middleware to the list of middleware.
     *
     * @param object|Closure A callable middleware.
     */
    public function apply($middleware)
    {
        foreach (func_get_args() as $mw) {
            array_unshift($this->_middleware, $mw);
        }
        return $this;
    }

    /**
     * Returns the route's link.
     *
     * @param  array  $params  The route parameters.
     * @param  array  $options Options for generating the proper prefix. Accepted values are:
     *                         - `'absolute'` _boolean_: `true` or `false`.
     *                         - `'scheme'`   _string_ : The scheme.
     *                         - `'host'`     _string_ : The host name.
     *                         - `'basePath'` _string_ : The base path.
     *                         - `'query'`    _string_ : The query string.
     *                         - `'fragment'` _string_ : The fragment string.
     * @return string          The link.
     */
    public function link($params = [], $options = [])
    {
        $defaults = [
            'absolute' => false,
            'basePath' => '',
            'query'    => '',
            'fragment' => ''
        ];

        $options = array_filter($options, function($value) { return $value !== '*'; });
        $options += $defaults;

        $params = $params + $this->params;

        $link = $this->_link($this->token(), $params);

        $basePath = trim($options['basePath'], '/');
        if ($basePath) {
            $basePath = '/' . $basePath;
        }
        $link = isset($link) ? ltrim($link, '/') : '';
        $link = $basePath . ($link ? '/' . $link : $link);
        $query = $options['query'] ? '?' . $options['query'] : '';
        $fragment = $options['fragment'] ? '#' . $options['fragment'] : '';

        if ($options['absolute']) {
            if ($host = $this->host()) {
                $link = $host->link($params, $options) . "{$link}";
            } else {
                $scheme = !empty($options['scheme']) ? $options['scheme'] . '://' : '//';
                $host = isset($options['host']) ? $options['host'] : 'localhost';
                $link = "{$scheme}{$host}{$link}";
            }
        }

        return $link . $query . $fragment;
    }

    /**
     * Helper for `Route::link()`.
     *
     * @param  array  $token    The token structure array.
     * @param  array  $params   The route parameters.
     * @return string           The URL path representation of the token structure array.
     */
    protected function _link($token, $params)
    {
        $link = '';
        foreach ($token['tokens'] as $child) {
            if (is_string($child)) {
                $link .= $child;
                continue;
            }
            if (isset($child['tokens'])) {
                if ($child['repeat']) {
                    $name = $child['repeat'];
                    $values = isset($params[$name]) && $params[$name] !== null ? (array) $params[$name] : [];
                    if (!$values && !$child['optional']) {
                        throw new RouterException("Missing parameters `'{$name}'` for route: `'{$this->name}#{$this->_pattern}'`.");
                    }
                    foreach ($values as $value) {
                        $link .= $this->_link($child, [$name => $value] + $params);
                    }
                } else {
                    $link .= $this->_link($child, $params);
                }
                continue;
            }

            if (!isset($params[$child['name']])) {
                if (!$token['optional']) {
                    throw new RouterException("Missing parameters `'{$child['name']}'` for route: `'{$this->name}#{$this->_pattern}'`.");
                }
                return '';
            }

            if ($data = $params[$child['name']]) {
                $parts = is_array($data) ? $data : [$data];
            } else {
                $parts = [];
            }
            foreach ($parts as $key => $value) {
                $parts[$key] = rawurlencode($value);
            }
            $value = join('/', $parts);

            if (!preg_match('~^' . $child['pattern'] . '$~', $value)) {
                throw new RouterException("Expected `'" . $child['name'] . "'` to match `'" . $child['pattern'] . "'`, but received `'" . $value . "'`.");
            }
            $link .= $value;
        }
        return $link;
    }
}


<?php
namespace Lead\Router;

/**
 * The Route class.
 */
class Host
{
    /**
     * Class dependencies.
     *
     * @var array
     */
    protected $_classes = [];

    /**
     * The matching scheme.
     *
     * @var string
     */
    protected $_scheme = '*';

    /**
     * The matching host.
     *
     * @var string
     */
    protected $_pattern = '*';

    /**
     * The tokens structure extracted from host's pattern.
     *
     * @see Parser::tokenize()
     * @var array
     */
    protected $_token = null;

    /**
     * The host's regular expression pattern.
     *
     * @see Parser::compile()
     * @var string
     */
    protected $_regex = null;

    /**
     * The host's variables.
     *
     * @see Parser::compile()
     * @var array
     */
    protected $_variables = null;

    /**
     * Constructs a route
     *
     * @param array $config The config array.
     */
    public function __construct($config = [])
    {
        $defaults = [
            'scheme'     => '*',
            'pattern'     => '*',
            'classes'    => [
                'parser' => 'Lead\Router\Parser'
            ]
        ];
        $config += $defaults;

        $this->_classes = $config['classes'];

        $this->scheme($config['scheme']);
        $this->pattern($config['pattern']);
    }

    /**
     * Get/sets the host's scheme.
     *
     * @param  string      $scheme The scheme on set or none to get the setted one.
     * @return string|self         The scheme on get or `$this` on set.
     */
    public function scheme($scheme = null)
    {
        if (!func_num_args()) {
            return $this->_scheme;
        }
        $this->_scheme = $scheme;
        return $this;
    }

    /**
     * Get/sets the host's pattern.
     *
     * @param  string      $pattern The pattern on set or none to get the setted one.
     * @return string|self          The pattern on get or `$this` on set.
     */
    public function pattern($pattern = null)
    {
        if (!func_num_args()) {
            return $this->_pattern;
        }
        $this->_token = null;
        $this->_regex = null;
        $this->_variables = null;
        $this->_pattern = $pattern;
        return $this;
    }

    /**
     * Returns the route's token structures.
     *
     * @return array A collection route's token structure.
     */
    public function token()
    {
        if ($this->_token === null) {
            $parser = $this->_classes['parser'];
            $this->_token = [];
            $this->_regex = null;
            $this->_variables = null;
            $this->_token = $parser::tokenize($this->_pattern, '.');
        }
        return $this->_token;
    }

    /**
     * Gets the route's regular expression pattern.
     *
     * @return string the route's regular expression pattern.
     */
    public function regex()
    {
        if ($this->_regex !== null) {
            return $this->_regex;
        }
        $this->_compile();
        return $this->_regex;
    }

    /**
     * Gets the route's variables and their associated pattern in case of array variables.
     *
     * @return array The route's variables and their associated pattern.
     */
    public function variables()
    {
        if ($this->_variables !== null) {
            return $this->_variables;
        }
        $this->_compile();
        return $this->_variables;
    }

    /**
     * Compiles the host's patten.
     */
    protected function _compile()
    {
        if ($this->pattern() === '*') {
            return;
        }
        $parser = $this->_classes['parser'];
        $rule = $parser::compile($this->token());
        $this->_regex = $rule[0];
        $this->_variables = $rule[1];
    }

    /**
     * Checks if a host matches a host pattern.
     *
     * @param  string  $host          The host to check.
     * @param  string  $hostVariables The matches host variables
     * @return boolean                Returns `true` on success, false otherwise.
     */
    public function match($request, &$hostVariables = null)
    {
        $defaults = [
            'host'   => '*',
            'scheme' => '*'
        ];
        $request += $defaults;
        $scheme = $request['scheme'];
        $host = $request['host'];

        $hostVariables = [];

        $anyHost = $this->pattern() === '*' || $host === '*';
        $anyScheme = $this->scheme() === '*' || $scheme === '*';

        if ($anyHost) {
            if ($this->variables()) {
                $hostVariables = array_fill_keys(array_keys($this->variables()), null);
            }
            return $anyScheme || $this->scheme() === $scheme;
        }

        if (!$anyScheme && $this->scheme() !== $scheme) {
            return false;
        }

        if (!preg_match('~^' . $this->regex() . '$~', $host, $matches)) {
            $hostVariables = null;
            return false;
        }
        $i = 0;

        foreach ($this->variables() as $name => $pattern) {
            $hostVariables[$name] = $matches[++$i];
        }
        return true;
    }

    /**
     * Returns the host's link.
     *
     * @param  array  $params  The host parameters.
     * @param  array  $options Options for generating the proper prefix. Accepted values are:
     *                         - `'scheme'`   _string_ : The scheme.
     *                         - `'host'`     _string_ : The host name.
     * @return string          The link.
     */
    public function link($params = [], $options = [])
    {
        $defaults = [
            'scheme'   => $this->scheme()
        ];
        $options += $defaults;

        if (!isset($options['host'])) {
            $options['host'] = $this->_link($this->token(), $params);
        }

        $scheme = $options['scheme'] !== '*' ? $options['scheme'] . '://' : '//';
        return $scheme . $options['host'];
    }

    /**
     * Helper for `Host::link()`.
     *
     * @param  array  $token    The token structure array.
     * @param  array  $params   The route parameters.
     * @return string           The URL path representation of the token structure array.
     */
    protected function _link($token, $params)
    {
        $link = '';
        foreach ($token['tokens'] as $child) {
            if (is_string($child)) {
                $link .= $child;
                continue;
            }
            if (isset($child['tokens'])) {
                if ($child['repeat']) {
                    $name = $child['repeat'];
                    $values = isset($params[$name]) && $params[$name] !== null ? (array) $params[$name] : [];
                    foreach ($values as $value) {
                        $link .= $this->_link($child, [$name => $value] + $params);
                    }
                } else {
                    $link .= $this->_link($child, $params);
                }
                continue;
            }
            if (!array_key_exists($child['name'], $params)) {
                if (!$token['optional']) {
                    throw new RouterException("Missing parameters `'{$child['name']}'` for host: `'{$this->_pattern}'`.");
                }
                return '';
            }
            $link .= $params[$child['name']];
        }
        return $link;
    }
}


<?php
namespace Lead\Router;

/**
 * Parses route pattern.
 *
 * The parser can produce a tokens structure from route pattern using `Parser::tokenize()`.
 * A tokens structure root node is of the following form:
 *
 * ```php
 * $token = Parser::tokenize('/test/{param}');
 * ```
 *
 * The returned `$token` looks like the following:
 * ```
 * [
 *     'optional' => false,
 *     'greedy'   => '',
 *     'repeat'   => false,
 *     'pattern'  => '/test/{param}',
 *     'tokens'   => [
 *         '/test/',
 *         [
 *             'name'      => 'param',
 *             'pattern'   => '[^/]+'
 *         ]
 *     ]
 * ]
 * ```
 *
 * Then tokens structures can be compiled to get the regex representation with associated variable.
 *
 * ```php
 * $rule = Parser::compile($token);
 * ```
 *
 * `$rule` looks like the following:
 *
 * ```
 * [
 *     '/test/([^/]+)',
 *     ['param' => false]
 * ]
 * ```
 */
class Parser {

    /**
     * Variable capturing block regex.
     */
    const PLACEHOLDER_REGEX = <<<EOD
\{
    (
        [a-zA-Z][a-zA-Z0-9_]*
    )
    (?:
        :(
            [^{}]*
            (?:
                \{(?-1)\}[^{}]*
            )*
        )
    )?
\}
EOD;

    /**
     * Tokenizes a route pattern. Optional segments are identified by square brackets.
     *
     * @param string $pattern   A route pattern
     * @param string $delimiter The path delimiter.
     * @param array             The tokens structure root node.
     */
    public static function tokenize($pattern, $delimiter = '/')
    {
        // Checks if the pattern has some optional segments.
        if (count(preg_split('~' . static::PLACEHOLDER_REGEX . '(*SKIP)(*F)|\[~x', $pattern)) > 1) {
            $tokens = static::_tokenizePattern($pattern, $delimiter);
        } else {
            $tokens = static::_tokenizeSegment($pattern, $delimiter);
        }
        return [
            'optional' => false,
            'greedy'   => '',
            'repeat'   => false,
            'pattern'  => $pattern,
            'tokens'   => $tokens
        ];
    }

    /**
     * Tokenizes patterns.
     *
     * @param string $pattern   A route pattern
     * @param string $delimiter The path delimiter.
     * @param array             An array of tokens structure.
     */
    protected static function _tokenizePattern($pattern, $delimiter, &$variable = null)
    {
        $tokens = [];
        $index = 0;
        $path = '';
        $parts = static::split($pattern);

        foreach ($parts as $part) {
            if (is_string($part)) {
                $tokens = array_merge($tokens, static::_tokenizeSegment($part, $delimiter, $variable));
                continue;
            }

            $greedy = $part[1];
            $repeat = $greedy === '+' || $greedy === '*';
            $optional = $greedy === '?' || $greedy === '*';

            $children = static::_tokenizePattern($part[0], $delimiter, $variable);

            $tokens[] = [
                'optional' => $optional,
                'greedy'   => $greedy ?: '?',
                'repeat'   => $repeat ? $variable : false,
                'pattern'  => $part[0],
                'tokens'   => $children
            ];

        }
        return $tokens;
    }

    /**
     * Tokenizes segments which are patterns with optional segments filtered out.
     * Only classic placeholder are supported.
     *
     * @param string $pattern   A route pattern with no optional segments.
     * @param string $delimiter The path delimiter.
     * @param array             An array of tokens structure.
     */
    protected static function _tokenizeSegment($pattern, $delimiter, &$variable = null)
    {
        $tokens = [];
        $index = 0;
        $path = '';

        if (preg_match_all('~' . static::PLACEHOLDER_REGEX . '()~x', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $offset = $match[0][1];

                $path .= substr($pattern, $index, $offset - $index);
                $index = $offset + strlen($match[0][0]);

                if ($path) {
                    $tokens[] = $path;
                    $path = '';
                }

                $variable = $match[1][0];
                $capture = $match[2][0] ?: '[^' . $delimiter . ']+';

                $tokens[] = [
                    'name'      => $variable,
                    'pattern'   => $capture
                ];
            }
        }

        if ($index < strlen($pattern)) {
            $path .= substr($pattern, $index);
            if ($path) {
                $tokens[] = $path;
            }
        }
        return $tokens;
    }

    /**
     * Splits a pattern in segments and patterns.
     * segments will be represented by string value and patterns by an array containing
     * the string pattern as first value and the greedy value as second value.
     *
     * example:
     * `/user[/{id}]*` will gives `['/user', ['id', '*']]`
     *
     * Unfortunately recursive regex matcher can't help here so this function is required.
     *
     * @param string $pattern A route pattern.
     * @param array           The splitted pattern.
     */
    public static function split($pattern)
    {
        $segments = [];
        $len = strlen($pattern);
        $buffer = '';
        $opened = 0;
        for ($i = 0; $i < $len; $i++) {
            if ($pattern[$i] === '{') {
                do {
                    $buffer .= $pattern[$i++];
                    if ($pattern[$i] === '}') {
                        $buffer .= $pattern[$i];
                        break;
                    }
                } while ($i < $len);
            } elseif ($pattern[$i] === '[') {
                $opened++;
                if ($opened === 1) {
                    $segments[] = $buffer;
                    $buffer = '';
                } else {
                    $buffer .= $pattern[$i];
                }
            } elseif ($pattern[$i] === ']') {
                $opened--;
                if ($opened === 0) {
                    $greedy = '?';
                    if ($i < $len -1) {
                        if ($pattern[$i + 1] === '*' || $pattern[$i + 1] === '+') {
                            $greedy = $pattern[$i + 1];
                            $i++;
                        }
                    }
                    $segments[] = [$buffer, $greedy];
                    $buffer = '';
                } else {
                    $buffer .= $pattern[$i];
                }
            } else {
                $buffer .= $pattern[$i];
            }
        }
        if ($buffer) {
            $segments[] = $buffer;
        }
        if ($opened) {
            throw ParserException::squareBracketMismatch();
        }
        return $segments;
    }

    /**
     * Builds a regex from a tokens structure array.
     *
     * @param  array $token A tokens structure root node.
     * @return array        An array containing the regex pattern and its associated variable names.
     */
    public static function compile($token)
    {
        $variables = [];
        $regex = '';
        foreach ($token['tokens'] as $child) {
            if (is_string($child)) {
                $regex .= preg_quote($child, '~');
            } elseif (isset($child['tokens'])) {
                $rule = static::compile($child);
                if ($child['repeat']) {
                    if (count($rule[1]) > 1) {
                        throw ParserException::placeholderExceeded();
                    }
                    $regex .= '((?:' . $rule[0] . ')' . $child['greedy'] . ')';
                } elseif ($child['optional']) {
                    $regex .= '(?:' . $rule[0] . ')?';
                }
                foreach ($rule[1] as $name => $pattern) {
                    if (isset($variables[$name])) {
                        throw ParserException::duplicatePlaceholder($name);
                    }
                    $variables[$name] = $pattern;
                }
            } else {
                $name = $child['name'];
                if (isset($variables[$name])) {
                    throw ParserException::duplicatePlaceholder($name);
                }
                if ($token['repeat']) {
                    $variables[$name] = $token['pattern'];
                    $regex .= $child['pattern'];
                } else {
                    $variables[$name] = false;
                    $regex .= '(' . $child['pattern'] . ')';
                }
            }
        }
        return [$regex, $variables];
    }
}


<?php
namespace Lead\Router;

class Scope
{
    /**
     * The router instance.
     *
     * @var object
     */
    protected $_router = null;

    /**
     * The parent instance.
     *
     * @var object
     */
    protected $_parent = null;

    /**
     * The middleware array.
     *
     * @var array
     */
    protected $_middleware = [];

    /**
     * The scope data.
     *
     * @var array
     */
    protected $_scope = [];

    /**
     * The constructor.
     *
     * @param array $config The config array.
     */
    public function __construct($config = [])
    {
        $defaults = [
            'router' => null,
            'parent' => null,
            'middleware' => [],
            'scope'  => []
        ];
        $config += $defaults;

        $this->_router = $config['router'];
        $this->_parent = $config['parent'];
        $this->_middleware = $config['middleware'];
        $this->_scope = $config['scope'] + [
            'name'           => '',
            'scheme'         => '*',
            'host'           => '*',
            'methods'        => '*',
            'prefix'         => '',
            'namespace'      => '',
            'persist'        => []
        ];
    }

    /**
     * Creates a new sub scope based on the instance scope.
     *
     * @param  array  $options The route options to scopify.
     * @return object          The new sub scope.
     */
    public function seed($options)
    {
        return new static([
            'router' => $this->_router,
            'parent' => $this,
            'scope'  => $this->scopify($options)
        ]);
    }

    /**
     * Scopes an options array according to the instance scope data.
     *
     * @param  array $options The options to scope.
     * @return array          The scoped options.
     */
    public function scopify($options)
    {
        $scope = $this->_scope;

        if (!empty($options['name'])) {
            $options['name'] = $scope['name'] ? $scope['name'] . '.' . $options['name'] : $options['name'];
        }

        if (!empty($options['prefix'])) {
            $options['prefix'] = $scope['prefix'] . trim($options['prefix'], '/');
            $options['prefix'] = $options['prefix'] ? $options['prefix'] . '/' : '';
        }

        if (isset($options['persist'])) {
            $options['persist'] = ((array) $options['persist']) + $scope['persist'];
        }

        if (isset($options['namespace'])) {
            $options['namespace'] = $scope['namespace'] . trim($options['namespace'], '\\') . '\\';
        }

        return $options + $scope;
    }

    /**
     * Middleware generator.
     *
     * @return callable
     */
    public function middleware()
    {
        foreach ($this->_middleware as $middleware) {
            yield $middleware;
        }
        if ($this->_parent) {
            foreach ($this->_parent->middleware() as $middleware) {
                yield $middleware;
            }
        }
    }

    /**
     * Adds a middleware to the list of middleware.
     *
     * @param object|Closure A callable middleware.
     */
    public function apply($middleware)
    {
        foreach (func_get_args() as $mw) {
            array_unshift($this->_middleware, $mw);
        }
        return $this;
    }

    /**
     * Delegates calls to the router instance.
     *
     * @param  string $name   The method name.
     * @param  array  $params The parameters.
     */
    public function __call($name, $params)
    {
        $this->_router->pushScope($this);
        $result = call_user_func_array([$this->_router, $name], $params);
        $this->_router->popScope();
        return $result;
    }
}


<?php
namespace Lead\Router;

use Closure;
use Psr\Http\Message\RequestInterface;
use Lead\Router\ParseException;
use Lead\Router\RouterException;

/**
 * The Router class.
 */
class Router extends \Lead\Collection\Collection
{
    /**
     * Class dependencies.
     *
     * @var array
     */
    protected $_classes = [];

    /**
     * Hosts.
     *
     * @var array
     */
    protected $_hosts = [];

    /**
     * Routes.
     *
     * @var array
     */
    protected $_routes = [];

    /**
     * Scopes stack.
     *
     * @var array
     */
    protected $_scopes = [];

    /**
     * Base path.
     *
     * @param string
     */
    protected $_basePath = '';

    /**
     * Dispatching strategies.
     *
     * @param array
     */
    protected $_strategies = [];

    /**
     * Defaults parameters to use when generating URLs in a dispatching context.
     *
     * @var array
     */
    protected $_defaults = [];

    /**
     * Constructor
     *
     * @param array $config
     */
    public function __construct($config = [])
    {
        $defaults = [
            'basePath'      => '',
            'scope'         => [],
            'strategies'    => [],
            'classes'       => [
                'parser'    => 'Lead\Router\Parser',
                'host'      => 'Lead\Router\Host',
                'route'     => 'Lead\Router\Route',
                'scope'     => 'Lead\Router\Scope'
            ]
        ];
        $config += $defaults;
        $this->_classes = $config['classes'];
        $this->_strategies = $config['strategies'];
        $this->basePath($config['basePath']);

        $scope = $this->_classes['scope'];
        $this->_scopes[] = new $scope(['router' => $this]);
    }

    /**
     * Returns the current router scope.
     *
     * @return object The current scope instance.
     */
    public function scope()
    {
        return end($this->_scopes);
    }

    /**
     * Pushes a new router scope context.
     *
     * @param  object $scope A scope instance.
     * @return self
     */
    public function pushScope($scope)
    {
        $this->_scopes[] = $scope;
        return $this;
    }

    /**
     * Pops the current router scope context.
     *
     * @return object The poped scope instance.
     */
    public function popScope()
    {
        return array_pop($this->_scopes);
    }

    /**
     * Gets/sets the base path of the router.
     *
     * @param  string      $basePath The base path to set or none to get the setted one.
     * @return string|self
     */
    public function basePath($basePath = null)
    {
        if (!func_num_args()) {
            return $this->_basePath;
        }
        $basePath = trim($basePath, '/');
        $this->_basePath = $basePath ? '/' . $basePath : '';
        return $this;
    }

    /**
     * Adds a route.
     *
     * @param  string|array  $pattern The route's pattern.
     * @param  Closure|array $options An array of options or the callback handler.
     * @param  Closure|null  $handler The callback handler.
     * @return self
     */
    public function bind($pattern, $options = [], $handler = null)
    {
        if (!is_array($options)) {
            $handler = $options;
            $options = [];
        }
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
        }

        if (isset($options['method'])) {
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
        }

        $scope = end($this->_scopes);
        $options = $scope->scopify($options);
        $options['pattern'] = $pattern;
        $options['handler'] = $handler;
        $options['scope'] = $scope;

        $scheme = $options['scheme'];
        $host = $options['host'];

        if (isset($this->_hosts[$scheme][$host])) {
            $options['host'] = $this->_hosts[$scheme][$host];
        }

        if (isset($this->_pattern[$scheme][$host][$pattern])) {
            $instance = $this->_pattern[$scheme][$host][$pattern];
        } else {
            $route = $this->_classes['route'];
            $instance = new $route($options);
            $this->_hosts[$scheme][$host] = $instance->host();
        }

        if (!isset($this->_pattern[$scheme][$host][$pattern])) {
            $this->_pattern[$scheme][$host][$pattern] = $instance;
        }

        $methods = $options['methods'] ? (array) $options['methods'] : [];

        $instance->allow($methods);

        foreach ($methods as $method) {
            $this->_routes[$scheme][$host][strtoupper($method)][] = $instance;
        }

        if (isset($options['name'])) {
            $this->_data[$options['name']] = $instance;
        }
        return $instance;
    }

    /**
     * Groups some routes inside a new scope.
     *
     * @param  string|array  $prefix  The group's prefix pattern or the options array.
     * @param  Closure|array $options An array of options or the callback handler.
     * @param  Closure|null  $handler The callback handler.
     * @return object                 The newly created scope instance.
     */
    public function group($prefix, $options, $handler = null)
    {
        if (!is_array($options)) {
            $handler = $options;
            if (is_string($prefix)) {
                $options = [];
            } else {
                $options = $prefix;
                $prefix = '';
            }
        }
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
        }

        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;

        $scope = $this->scope();

        $this->pushScope($scope->seed($options));

        $handler($this);

        return $this->popScope();
    }

    /**
     * Routes a Request.
     *
     * @param  mixed  $request The request to route.
     * @return object          A route matching the request or a "route not found" route.
     */
    public function route($request)
    {
        $defaults = [
            'path'   => '',
            'method' => 'GET',
            'host'   => '*',
            'scheme' => '*'
        ];

        $this->_defaults = [];

        if ($request instanceof RequestInterface) {
            $uri = $request->getUri();
            $r = [
                'scheme' => $uri->getScheme(),
                'host'   => $uri->getHost(),
                'method' => $request->getMethod(),
                'path'   => $uri->getPath()
            ];
            if (method_exists($request, 'basePath')) {
                $this->basePath($request->basePath());
            }
        } elseif (!is_array($request)) {
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
        } else {
            $r = $request + $defaults;
        }
        $r = $this->_normalizeRequest($r);

        if ($route = $this->_route($r)) {
            $route->request = is_object($request) ? $request : $r;
            foreach ($route->persist as $key) {
                if (isset($route->params[$key])) {
                    $this->_defaults[$key] = $route->params[$key];
                }
            }
        } else {
            $route = $this->_classes['route'];
            $error = $route::NOT_FOUND;
            $message = "No route found for `{$r['scheme']}:{$r['host']}:{$r['method']}:/{$r['path']}`.";
            $route = new $route(compact('error', 'message'));
        }

        return $route;
    }

    /**
     * Normalizes a request.
     *
     * @param  array $request The request to normalize.
     * @return array          The normalized request.
     */
    protected function _normalizeRequest($request)
    {
        if (preg_match('~^(?:[a-z]+:)?//~i', $request['path'])) {
            $parsed = array_intersect_key(parse_url($request['path']), $request);
            $request = $parsed + $request;
        }
        $request['path'] = (ltrim(strtok($request['path'], '?'), '/'));
        $request['method'] = strtoupper($request['method']);
        return $request;
    }

    /**
     * Routes a request.
     *
     * @param array $request The request to route.
     */
    protected function _route($request)
    {
        $path = $request['path'];
        $httpMethod = $request['method'];
        $host = $request['host'];
        $scheme = $request['scheme'];

        $allowedSchemes = array_unique([$scheme => $scheme, '*' => '*']);
        $allowedMethods = array_unique([$httpMethod => $httpMethod, '*' => '*']);

        if ($httpMethod === 'HEAD') {
            $allowedMethods += ['GET' => 'GET'];
        }

        foreach ($this->_routes as $scheme => $hostBasedRoutes) {
            if (!isset($allowedSchemes[$scheme])) {
                continue;
            }
            foreach ($hostBasedRoutes as $routeHost => $methodBasedRoutes) {
                foreach ($methodBasedRoutes as $method => $routes) {
                    if (!isset($allowedMethods[$method]) && $httpMethod !== '*') {
                        continue;
                    }
                    foreach ($routes as $route) {
                        if (!$route->match($request, $variables, $hostVariables)) {
                            if ($hostVariables === null) {
                                continue 3;
                            }
                            continue;
                        }
                        return $route;
                    }
                }
            }
        }
    }

    /**
     * Middleware generator.
     *
     * @return callable
     */
    public function middleware()
    {
        foreach ($this->_scopes[0]->middleware() as $middleware) {
            yield $middleware;
        }
    }

    /**
     * Adds a middleware to the list of middleware.
     *
     * @param object|Closure A callable middleware.
     */
    public function apply($middleware)
    {
        foreach (func_get_args() as $mw) {
            $this->_scopes[0]->apply($mw);
        }
        return $this;
    }

    /**
     * Gets/sets router's strategies.
     *
     * @param  string $name    A routing strategy name.
     * @param  mixed  $handler The strategy handler or none to get the setted one.
     * @return mixed           The strategy handler (or `null` if not found) on get or `$this` on set.
     */
    public function strategy($name, $handler = null)
    {
        if (func_num_args() === 1) {
            if (!isset($this->_strategies[$name])) {
                return;
            }
            return $this->_strategies[$name];
        }
        if ($handler === false) {
            unset($this->_strategies[$name]);
            return;
        }
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
        }
        $this->_strategies[$name] = $handler;
        return $this;
    }

    /**
     * Adds a route based on a custom HTTP verb.
     *
     * @param  string $name   The HTTP verb to define a route on.
     * @param  array  $params The route's parameters.
     */
    public function __call($name, $params)
    {
        if ($strategy = $this->strategy($name)) {
            array_unshift($params, $this);
            return call_user_func_array($strategy, $params);
        }
        if (is_callable($params[1])) {
            $params[2] = $params[1];
            $params[1] = [];
        }
        $params[1]['methods'] = [$name];
        return call_user_func_array([$this, 'bind'], $params);
    }

    /**
     * Returns a route's link.
     *
     * @param  string $name    A route name.
     * @param  array  $params  The route parameters.
     * @param  array  $options Options for generating the proper prefix. Accepted values are:
     *                         - `'absolute'` _boolean_: `true` or `false`.
     *                         - `'scheme'`   _string_ : The scheme.
     *                         - `'host'`     _string_ : The host name.
     *                         - `'basePath'` _string_ : The base path.
     *                         - `'query'`    _string_ : The query string.
     *                         - `'fragment'` _string_ : The fragment string.
     * @return string          The link.
     */
    public function link($name, $params = [], $options = [])
    {
        $defaults = [
            'basePath' => $this->basePath()
        ];
        $options += $defaults;

        $params += $this->_defaults;

        if (!isset($this[$name])) {
            throw new RouterException("No binded route defined for `'{$name}'`, bind it first with `bind()`.");
        }
        $route = $this[$name];
        return $route->link($params, $options);
    }

    /**
     * Clears the router.
     */
    public function clear()
    {
        $this->_basePath = '';
        $this->_strategies = [];
        $this->_defaults = [];
        $this->_routes = [];
        $scope = $this->_classes['scope'];
        $this->_scopes = [new $scope(['router' => $this])];
    }
}


<?php
namespace raoul2000\workflow\view;

use yii\base\Widget;
use yii\base\InvalidConfigException;

class WorkflowViewWidget extends Widget
{
	/**
	 * @var mixed the Workflow object to display, or a model attached to a
	 * SimpleWorkflowBehavior. In this case, if the model is in a status, its parent workflow
	 * is used, otherwise the default workflow is used.
	 */
    public $workflow;
    /**
     * @var string Id of the HTML element that is as a container for the
     * workflow view
     */
    public $containerId;
    /**
     * @var string Id of the VIS javascript objects instance created by this widget to render the workflow.
     * If not set a unique id is automatically created.
     */
    public $visNetworkId;
    /**
     * @var WorkflowInterface the workflow instance to display
     */
	private $_workflow;
	/**
	 * @var string unique Id
	 */
	private $_visId;

	/**
	 * (non-PHPdoc)
	 * @see \yii\base\Object::init()
	 */
    public function init()
    {
        parent::init();
        if ( ! isset($this->containerId)){
        	throw new InvalidConfigException("Parameter 'containerId' is missing ");
        }
        if ( ! isset($this->visNetworkId)){
        	$this->visNetworkId = uniqid('vis_');
        }
        if ( ! isset($this->workflow)){
        	throw new InvalidConfigException("Parameter 'workflow' is missing ");
        }

        if ( $this->workflow instanceof \yii\base\Model ) {

        	if ( ! \raoul2000\workflow\base\SimpleWorkflowBehavior::isAttachedTo($this->workflow)) {
        		throw new InvalidConfigException("The model passed as parameter 'workflow' is not attached to a SimpleWorkflowBehavior.");
        	}

        	$this->_workflow = $this->workflow->getWorkflow();
        	if ( $this->_workflow == null) {
        		$this->_workflow = $this->workflow->getWorkflowSource()->getWorkflow(
        			$this->workflow->getDefaultWorkflowId()
        		);
        	}
        } elseif ( $this->workflow instanceof \raoul2000\workflow\base\WorkflowInterface ) {
        	$this->_workflow = $this->workflow;
        }

        if ( $this->_workflow == null) {
        	throw new InvalidConfigException("Failed to find workflow instance from parameter 'workflow'");
        }
        $this->_visId = uniqid();
    }

    /**
     * @see \yii\base\Widget::run()
     */
    public function run()
    {
        $this->getView()->registerJs(
        	$this->createJs()
        );
        WorkflowViewAsset::register($this->getView());
    }

    /**
     * Creates and returns the JS code.
     * The JS code is used to initialize the VIS instances in charge of displaying the workflow.
     *
     * @return string the JS code
     */
    private function createJs()
    {
    	$nodes = $this->_workflow->getAllStatuses();
    	$trList = [];
    	$nodeList = [];
    	foreach($nodes as $node) {
    		$n = new \stdClass();
    		$n->id = $node->getId();
    		$n->label = $node->getLabel();
    		if( $node->getMetadata('color') ){
    			if( $node->getWorkflow()->getInitialStatusId() == $node->getId() ){
    				$n->borderWidth = 4;
    				$n->color = new \stdClass();
    				$n->color->border = 'rgb(0,255,42)';
    				// 	$n->color->background = $node->getMetadata('color');
    			} else {
    				//	$n->color = $node->getMetadata('color');
    			}
    		}
    		$nodeList[] = $n;

    		$transitions = $node->getTransitions();
    		foreach($transitions as $transition){
    			$t = new \stdClass();
    			$t->from = $n->id;
    			$t->to = $transition->getEndStatus()->getId();
    			$t->arrows = 'to';
    			$trList[] = $t;
    		}
    	}
    	$jsonNodes = \yii\helpers\Json::encode($nodeList);
    	$jsonTransitions = \yii\helpers\Json::encode($trList);

    	$js=<<<EOS
		var {$this->visNetworkId} = new vis.Network(
			document.getElementById('{$this->containerId}'),
	    	{
			    nodes: new vis.DataSet($jsonNodes),
			    edges: new vis.DataSet($jsonTransitions)
		  	},
	    	{
				"physics": {
					"solver": "repulsion"
				}
			}
	    );

EOS;
    	return $js;
    }
}


<?php

namespace NikitaKiselev\SendPulse;

use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;
use NikitaKiselev\SendPulse\SendPulseApi;
use NikitaKiselev\SendPulse\Token\FileTokenStorage;
use NikitaKiselev\SendPulse\Contracts\TokenStorage;
use NikitaKiselev\SendPulse\Token\SessionTokenStorage;
use Illuminate\Contracts\Filesystem\Factory as Filesystem;
use NikitaKiselev\SendPulse\Contracts\SendPulseApi as SendPulseApiContract;

class SendPulseProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->publishes([
            __DIR__.'/config/sendpulse.php' => config_path('sendpulse.php'),
        ], 'config');
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->mergeConfigFrom(
            __DIR__ . '/config/sendpulse.php', 'sendpulse'
        );

        $this->registerStorageManager();

        $this->app->singleton(SendPulseApiContract::class, function ($app) {

            $config = $app['config']->get('sendpulse');

            return new SendPulseApi(
                $app['token.manager']->driver(),
                $config['api_user_id'],
                $config['api_secret']
            );
        });

        $this->app->alias(SendPulseApiContract::class, 'sendpulse');
    }

    /**
     * Register the storage driver manager
     */
    public function registerStorageManager()
    {
        $this->app->bind('token.manager', function ($app) {
            return new TokenStorageManager($app);
        });
    }
}


<?php

namespace NikitaKiselev\SendPulse\Storage;

use Illuminate\Contracts\Filesystem\Factory;
use NikitaKiselev\SendPulse\Contracts\TokenStorage;

class FileTokenStorage implements TokenStorage
{
    /**
     * @var Factory
     */
    private $file;

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

    /**
     * FileTokenStorage constructor.
     *
     * @param Factory $file
     */
    public function __construct(Factory $file, string $hashName)
    {
        $this->file = $file;
        $this->hashName = $hashName;
    }

    /**
     * Get token value
     *
     * @return string
     */
    public function get()
    {
        if (!$this->empty())
        {
            return $this->file->disk()->get(
                $this->hashName
            );
        }

        return null;
    }

    /**
     * Set token
     *
     * @param string $value
     * @return mixed
     */
    public function set(string $value)
    {
        return $this->file->disk()->put(
            $this->hashName,
            $value
        );
    }

    /**
     * Check token empty
     *
     * @return boolean
     */
    public function empty()
    {
        return ! $this->file->disk()->exists(
            $this->hashName
        );
    }
}


<?php

namespace NikitaKiselev\SendPulse;

/*
 * SendPulse REST API PHP Class
 *
 * Documentation
 * https://login.sendpulse.com/manual/rest-api/
 * https://sendpulse.com/api
 *
 */

use NikitaKiselev\SendPulse\Contracts\TokenStorage;
use stdClass;
use Memcache;
use Exception;
use NikitaKiselev\SendPulse\Contracts\SendPulseApi as SendPulseApiContract;

class SendpulseApi implements SendPulseApiContract
{
    private $apiUrl = 'https://api.sendpulse.com';

    private $userId = NULL;
    private $secret = NULL;
    private $token = NULL;

    private $refreshToken = 0;

    /**
     * @var
     */
    private $storage;

    /**
     * Sendpulse API constructor
     *
     * @param $userId
     * @param $secret
     * @param TokenStorage $storageType
     * @throws Exception
     */
    public function __construct(TokenStorage $storage, $userId, $secret) {
        if( empty( $userId ) || empty( $secret ) ) {
            throw new Exception( 'Empty ID or SECRET' );
        }

        $this->userId = $userId;
        $this->secret = $secret;
        $this->storage = $storage;

        $hashName = md5( $userId . '::' . $secret );

        $this->token = $this->storage->get();

        if( empty( $this->token ) ) {
            if( !$this->getToken() ) {
                throw new Exception( 'Could not connect to api, check your ID and SECRET' );
            }
        }
    }

    /**
     * Get token and store it
     *
     * @return bool
     */
    private function getToken() {
        $data = array(
            'grant_type'    => 'client_credentials',
            'client_id'     => $this->userId,
            'client_secret' => $this->secret,
        );

        $requestResult = $this->sendRequest( 'oauth/access_token', 'POST', $data, false );

        if( $requestResult->http_code != 200 ) {
            return false;
        }

        $this->refreshToken = 0;
        $this->token = $requestResult->data->access_token;

        $this->storage->set($this->token);

        return true;
    }

    /**
     * Form and send request to API service
     *
     * @param $path
     * @param string $method
     * @param array $data
     * @param bool $useToken
     * @return array|NULL
     */
    private function sendRequest( $path, $method = 'GET', $data = array(), $useToken = true ) {
        $url = $this->apiUrl . '/' . $path;
        $method = strtoupper( $method );
        $curl = curl_init();

        if( $useToken && !empty( $this->token ) ) {
            $headers = array( 'Authorization: Bearer ' . $this->token );
            curl_setopt( $curl, CURLOPT_HTTPHEADER, $headers );
        }

        switch( $method ) {
            case 'POST':
                curl_setopt( $curl, CURLOPT_POST, count( $data ) );
                curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query( $data ) );
                break;
            case 'PUT':
                curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "PUT" );
                curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query( $data ) );
                break;
            case 'DELETE':
                curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, 'DELETE' );
                curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query( $data ) );
                break;
            default:
                if( !empty( $data ) ) {
                    $url .= '?' . http_build_query( $data );
                }
        }

        curl_setopt( $curl, CURLOPT_URL, $url );
        curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt( $curl, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt( $curl, CURLOPT_RETURNTRANSFER, 1 );
        curl_setopt( $curl, CURLOPT_HEADER, 1 );

        $response = curl_exec( $curl );
        $header_size = curl_getinfo( $curl, CURLINFO_HEADER_SIZE );
        $headerCode = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
        $responseBody = substr( $response, $header_size );

        curl_close( $curl );

        if( $headerCode == 401 && $this->refreshToken == 0 ) {
            $this->refreshToken += 1;
            $this->getToken();
            $return = $this->sendRequest( $path, $method, $data );
        } else {
            $return = new stdClass();
            $return->data = json_decode( $responseBody );
            $return->http_code = $headerCode;
        }

        return $return;
    }

    /**
     * Process results
     *
     * @param $data
     * @return mixed
     */
    private function handleResult( $data ) {
        if( empty( $data->data ) ) {
            $data->data = new stdClass();
        }
        if( $data->http_code != 200 ) {
            $data->data->is_error = true;
            $data->data->http_code = $data->http_code;
        }

        return $data->data;
    }

    /**
     * Process errors
     *
     * @param null $customMessage
     * @return stdClass
     */
    private function handleError( $customMessage = NULL ) {
        $message = new stdClass();
        $message->is_error = true;
        if( !is_null( $customMessage ) ) {
            $message->message = $customMessage;
        }

        return $message;
    }

    /*
     * API interface implementation
     */

    /**
     * Create address book
     *
     * @param $bookName
     * @return mixed|stdClass
     */
    public function createAddressBook( $bookName ) {
        if( empty( $bookName ) ) {
            return $this->handleError( 'Empty book name' );
        }

        $data = array( 'bookName' => $bookName );
        $requestResult = $this->sendRequest( 'addressbooks', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Edit address book name
     *
     * @param $id
     * @param $newName
     * @return mixed|stdClass
     */
    public function editAddressBook( $id, $newName ) {
        if( empty( $newName ) || empty( $id ) ) {
            return $this->handleError( 'Empty new name or book id' );
        }

        $data = array( 'name' => $newName );
        $requestResult = $this->sendRequest( 'addressbooks/' . $id, 'PUT', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Remove address book
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function removeAddressBook( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty book id' );
        }

        $requestResult = $this->sendRequest( 'addressbooks/' . $id, 'DELETE' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of address books
     *
     * @param null $limit
     * @param null $offset
     * @return mixed
     */
    public function listAddressBooks( $limit = NULL, $offset = NULL ) {
        $data = array();
        if( !is_null( $limit ) ) {
            $data['limit'] = $limit;
        }
        if( !is_null( $offset ) ) {
            $data['offset'] = $offset;
        }

        $requestResult = $this->sendRequest( 'addressbooks', 'GET', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get information about book
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function getBookInfo( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty book id' );
        }

        $requestResult = $this->sendRequest( 'addressbooks/' . $id );

        return $this->handleResult( $requestResult );
    }

    /**
     * List email addresses from book
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function getEmailsFromBook( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty book id' );
        }

        $requestResult = $this->sendRequest( 'addressbooks/' . $id . '/emails' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Add new emails to address book
     *
     * @param $bookId
     * @param $emails
     * @return mixed|stdClass
     */
    public function addEmails( $bookId, $emails ) {
        if( empty( $bookId ) || empty( $emails ) ) {
            return $this->handleError( 'Empty book id or emails' );
        }

        $data = array(
            'emails' => serialize( $emails )
        );

        $requestResult = $this->sendRequest( 'addressbooks/' . $bookId . '/emails', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Remove email addresses from book
     *
     * @param $bookId
     * @param $emails
     * @return mixed|stdClass
     */
    public function removeEmails( $bookId, $emails ) {
        if( empty( $bookId ) || empty( $emails ) ) {
            return $this->handleError( 'Empty book id or emails' );
        }

        $data = array(
            'emails' => serialize( $emails )
        );

        $requestResult = $this->sendRequest( 'addressbooks/' . $bookId . '/emails', 'DELETE', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get information about email address from book
     *
     * @param $bookId
     * @param $email
     * @return mixed|stdClass
     */
    public function getEmailInfo( $bookId, $email ) {
        if( empty( $bookId ) || empty( $email ) ) {
            return $this->handleError( 'Empty book id or email' );
        }

        $requestResult = $this->sendRequest( 'addressbooks/' . $bookId . '/emails/' . $email );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get cost of campaign based on address book
     *
     * @param $bookId
     * @return mixed|stdClass
     */
    public function campaignCost( $bookId ) {
        if( empty( $bookId ) ) {
            return $this->handleError( 'Empty book id' );
        }

        $requestResult = $this->sendRequest( 'addressbooks/' . $bookId . '/cost' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of campaigns
     *
     * @param null $limit
     * @param null $offset
     * @return mixed
     */
    public function listCampaigns( $limit = NULL, $offset = NULL ) {
        $data = array();
        if( !empty( $limit ) ) {
            $data['limit'] = $limit;
        }
        if( !empty( $offset ) ) {
            $data['offset'] = $offset;
        }
        $requestResult = $this->sendRequest( 'campaigns', 'GET', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get information about campaign
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function getCampaignInfo( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty campaign id' );
        }

        $requestResult = $this->sendRequest( 'campaigns/' . $id );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get campaign statistic by countries
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function campaignStatByCountries( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty campaign id' );
        }

        $requestResult = $this->sendRequest( 'campaigns/' . $id . '/countries' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get campaign statistic by referrals
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function campaignStatByReferrals( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty campaign id' );
        }

        $requestResult = $this->sendRequest( 'campaigns/' . $id . '/referrals' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Create new campaign
     *
     * @param $senderName
     * @param $senderEmail
     * @param $subject
     * @param $body
     * @param $bookId
     * @param string $name
     * @param string $attachments
     * @return mixed
     */
    public function createCampaign( $senderName, $senderEmail, $subject, $body, $bookId, $name = '', $attachments = '' ) {
        if( empty( $senderName ) || empty( $senderEmail ) || empty( $subject ) || empty( $body ) || empty( $bookId ) ) {
            return $this->handleError( 'Not all data.' );
        }

        if( !empty( $attachments ) ) {
            $attachments = serialize( $attachments );
        }
        $data = array(
            'sender_name'  => $senderName,
            'sender_email' => $senderEmail,
            'subject'      => $subject,
            'body'         => base64_encode( $body ),
            'list_id'      => $bookId,
            'name'         => $name,
            'attachments'  => $attachments
        );

        $requestResult = $this->sendRequest( 'campaigns', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Cancel campaign
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function cancelCampaign( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty campaign id' );
        }

        $requestResult = $this->sendRequest( 'campaigns/' . $id, 'DELETE' );

        return $this->handleResult( $requestResult );
    }

    /**
     * List all senders
     *
     * @return mixed
     */
    public function listSenders() {
        $requestResult = $this->sendRequest( 'senders' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Add new sender
     *
     * @param $senderName
     * @param $senderEmail
     * @return mixed|stdClass
     */
    public function addSender( $senderName, $senderEmail ) {
        if( empty( $senderName ) || empty( $senderEmail ) ) {
            return $this->handleError( 'Empty sender name or email' );
        }

        $data = array(
            'email' => $senderEmail,
            'name'  => $senderName
        );

        $requestResult = $this->sendRequest( 'senders', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Remove sender
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function removeSender( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $data = array(
            'email' => $email
        );

        $requestResult = $this->sendRequest( 'senders', 'DELETE', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Activate sender using code
     *
     * @param $email
     * @param $code
     * @return mixed|stdClass
     */
    public function activateSender( $email, $code ) {
        if( empty( $email ) || empty( $code ) ) {
            return $this->handleError( 'Empty email or activation code' );
        }

        $data = array(
            'code' => $code
        );

        $requestResult = $this->sendRequest( 'senders/' . $email . '/code', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Request mail with activation code
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function getSenderActivationMail( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $requestResult = $this->sendRequest( 'senders/' . $email . '/code' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get global information about email
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function getEmailGlobalInfo( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $requestResult = $this->sendRequest( 'emails/' . $email );

        return $this->handleResult( $requestResult );
    }

    /**
     * Remove email from all books
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function removeEmailFromAllBooks( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $requestResult = $this->sendRequest( 'emails/' . $email, 'DELETE' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get email statistic by all campaigns
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function emailStatByCampaigns( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $requestResult = $this->sendRequest( 'emails/' . $email . '/campaigns' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get all emails from blacklist
     *
     * @return mixed
     */
    public function getBlackList() {
        $requestResult = $this->sendRequest( 'blacklist' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Add email to blacklist
     *
     * @param $emails - string with emails, separator - ,
     * @param string $comment
     * @return mixed|stdClass
     */
    public function addToBlackList( $emails, $comment = '' ) {
        if( empty( $emails ) ) {
            return $this->handleError( 'Empty email' );
        }

        $data = array(
            'emails'  => base64_encode( $emails ),
            'comment' => $comment
        );

        $requestResult = $this->sendRequest( 'blacklist', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Remove emails from blacklist
     *
     * @param $emails - string with emails, separator - ,
     * @return mixed|stdClass
     */
    public function removeFromBlackList( $emails ) {
        if( empty( $emails ) ) {
            return $this->handleError( 'Empty email' );
        }

        $data = array(
            'emails' => base64_encode( $emails )
        );

        $requestResult = $this->sendRequest( 'blacklist', 'DELETE', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get balance
     *
     * @param string $currency
     * @return mixed
     */
    public function getBalance( $currency = '' ) {
        $currency = strtoupper( $currency );
        $url = 'balance';
        if( !empty( $currency ) ) {
            $url .= '/' . strtoupper( $currency );
        }

        $requestResult = $this->sendRequest( $url );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: get list of emails
     *
     * @param int $limit
     * @param int $offset
     * @param string $fromDate
     * @param string $toDate
     * @param string $sender
     * @param string $recipient
     * @return mixed
     */
    public function smtpListEmails( $limit = 0, $offset = 0, $fromDate = '', $toDate = '', $sender = '', $recipient = '' ) {
        $data = array(
            'limit'     => $limit,
            'offset'    => $offset,
            'from'      => $fromDate,
            'to'        => $toDate,
            'sender'    => $sender,
            'recipient' => $recipient
        );

        $requestResult = $this->sendRequest( '/smtp/emails', 'GET', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get information about email by id
     *
     * @param $id
     * @return mixed|stdClass
     */
    public function smtpGetEmailInfoById( $id ) {
        if( empty( $id ) ) {
            return $this->handleError( 'Empty id' );
        }

        $requestResult = $this->sendRequest( '/smtp/emails/' . $id );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: add emails to unsubscribe list
     *
     * @param $emails
     * @return mixed|stdClass
     */
    public function smtpUnsubscribeEmails( $emails ) {
        if( empty( $emails ) ) {
            return $this->handleError( 'Empty emails' );
        }

        $data = array(
            'emails' => serialize( $emails )
        );

        $requestResult = $this->sendRequest( '/smtp/unsubscribe', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: remove emails from unsubscribe list
     *
     * @param $emails
     * @return mixed|stdClass
     */
    public function smtpRemoveFromUnsubscribe( $emails ) {
        if( empty( $emails ) ) {
            return $this->handleError( 'Empty emails' );
        }

        $data = array(
            'emails' => serialize( $emails )
        );

        $requestResult = $this->sendRequest( '/smtp/unsubscribe', 'DELETE', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of IP
     *
     * @return mixed
     */
    public function smtpListIP() {
        $requestResult = $this->sendRequest( 'smtp/ips' );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: get list of allowed domains
     *
     * @return mixed
     */
    public function smtpListAllowedDomains() {
        $requestResult = $this->sendRequest( 'smtp/domains' );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: add new domain
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function smtpAddDomain( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $data = array(
            'email' => $email
        );

        $requestResult = $this->sendRequest( 'smtp/domains', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: verify domain
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function smtpVerifyDomain( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email' );
        }

        $requestResult = $this->sendRequest( 'smtp/domains/' . $email );

        return $this->handleResult( $requestResult );
    }

    /**
     * SMTP: send mail
     *
     * @param $email
     * @return mixed|stdClass
     */
    public function smtpSendMail( $email ) {
        if( empty( $email ) ) {
            return $this->handleError( 'Empty email data' );
        }

        $email['html'] = base64_encode( $email['html'] );
        $data = array(
            'email' => serialize( $email )
        );

        $requestResult = $this->sendRequest( 'smtp/emails', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of push campaigns
     *
     * @param null $limit
     * @param null $offset
     * @return mixed
     */
    public function pushListCampaigns($limit = NULL, $offset = NULL) {
        $data = array();
        if( !is_null( $limit ) ) {
            $data['limit'] = $limit;
        }
        if( !is_null( $offset ) ) {
            $data['offset'] = $offset;
        }

        $requestResult = $this->sendRequest( 'push/tasks', 'GET', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of websites
     *
     * @param null $limit
     * @param null $offset
     * @return mixed
     */
    public function pushListWebsites( $limit = NULL, $offset = NULL ) {
        $data = array();
        if( !is_null( $limit ) ) {
            $data['limit'] = $limit;
        }
        if( !is_null( $offset ) ) {
            $data['offset'] = $offset;
        }

        $requestResult = $this->sendRequest( 'push/websites', 'GET', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get amount of websites
     *
     * @return mixed
     */
    public function pushCountWebsites() {
        $requestResult = $this->sendRequest( 'push/websites/total', 'GET' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of all variables for website
     *
     * @param $websiteId
     * @return mixed
     */
    public function pushListWebsiteVariables( $websiteId ) {
        $requestResult = $this->sendRequest( 'push/websites/'.$websiteId.'/variables', 'GET' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get list of subscriptions for the website
     *
     * @param $websiteId
     * @return mixed
     */
    public function pushListWebsiteSubscriptions( $websiteId, $limit = NULL, $offset = NULL ) {
        $data = array();
        if( !is_null( $limit ) ) {
            $data['limit'] = $limit;
        }
        if( !is_null( $offset ) ) {
            $data['offset'] = $offset;
        }

        $requestResult = $this->sendRequest( 'push/websites/'.$websiteId.'/subscriptions', 'GET', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Get amount of subscriptions for the site
     *
     * @param $websiteId
     * @return mixed
     */
    public function pushCountWebsiteSubscriptions( $websiteId ) {
        $requestResult = $this->sendRequest( 'push/websites/'.$websiteId.'/subscriptions/total', 'GET' );

        return $this->handleResult( $requestResult );
    }

    /**
     * Set state for subscription
     *
     * @param $subscriptionId
     * @param $stateValue
     * @return mixed
     */
    public function pushSetSubscriptionState( $subscriptionId, $stateValue ) {
        $data = array(
            'id' => $subscriptionId,
            'state' => $stateValue
        );

        $requestResult = $this->sendRequest( 'push/subscriptions/state', 'POST', $data );

        return $this->handleResult( $requestResult );
    }

    /**
     * Create new push campaign
     *
     * @param $taskInfo
     * @param array $additionalParams
     * @return mixed|stdClass
     */
    public function createPushTask( $taskInfo, $additionalParams = array() ) {
        $data = $taskInfo;
        if (!isset($data['ttl'])) {
            $data['ttl'] = 0;
        }
        if( empty($data['title']) || empty($data['website_id']) || empty($data['body']) ) {
            return $this->handleError( 'Not all data' );
        }
        if ($additionalParams) {
            foreach($additionalParams as $key=>$val) {
                $data[$key] = $val;
            }
        }

        $requestResult = $this->sendRequest( '/push/tasks', 'POST', $data );

        return $this->handleResult( $requestResult );
    }
}


<?php

namespace NikitaKiselev\SendPulse;

use Illuminate\Support\Manager;
use NikitaKiselev\SendPulse\Storage\FileTokenStorage;
use NikitaKiselev\SendPulse\Storage\SessionTokenStorage;

class TokenStorageManager extends Manager
{
    /**
     * Get the default storage driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['sendpulse.token_storage'];
    }

    /**
     * @return FileTokenStorage
     */
    public function createFileDriver()
    {
        return new FileTokenStorage(
            $this->app->make(\Illuminate\Contracts\Filesystem\Factory::class),
            $this->hashName()
        );
    }

    /**
     * @return SessionTokenStorage
     */
    public function createSessionDriver()
    {
        return new SessionTokenStorage(
            $this->app->make(\Illuminate\Session\Store::class),
            $this->hashName()
        );
    }

    /**
     * Get hash name
     *
     * @return string
     */
    protected function hashName()
    {
        $userId = $this->app['config']->get('sendpulse.api_user_id');
        $secret = $this->app['config']->get('sendpulse.api_secret');

        return md5($userId . '::' . $secret);
    }
}


<?php
namespace Checkdomain\Comodo;

use Checkdomain\Comodo\Model\Account;
use GuzzleHttp\Client;

/**
 * Class CommunicationAdapter
 *
 * Manages the communication with comodo
 */
class CommunicationAdapter
{
    const RESPONSE_NEW_LINE    = 0;
    const RESPONSE_URL_ENCODED = 1;

    /**
     * @var Client
     */
    protected $client;

    /**
     * @var Account
     */
    protected $account;

    /**
     * @param \Checkdomain\Comodo\Model\Account $account
     *
     * @return CommunicationAdapter
     */
    public function setAccount($account)
    {
        $this->account = $account;

        return $this;
    }

    /**
     * @return \Checkdomain\Comodo\Model\Account
     */
    public function getAccount()
    {
        return $this->account;
    }

    /**
     * Constructs a communication adapter with an account
     *
     * @param Account $account
     */
    public function __construct(Account $account = null)
    {
        $this->account = $account;
    }

    /**
     * @param \GuzzleHttp\Client $client
     *
     * @return CommunicationAdapter
     */
    public function setClient($client)
    {
        $this->client = $client;

        return $this;
    }

    /**
     * @return \GuzzleHttp\Client
     */
    public function getClient()
    {
        if ($this->client === null) {
            $this->client = new Client();
        }

        return $this->client;
    }

    /**
     * Sends a query to the provided url and return the response body.
     *
     * @param string $url
     * @param array  $params
     *
     * @return string
     */
    public function sendToWebsite($url, array $params)
    {
        // Sending request
        $client  = $this->getClient();
        $response = $client->request('POST', $url, [
            'query' => http_build_query($params, '', '&')
        ]);

        // Getting response body
        $responseString = $response->getBody()->getContents();
        $responseString = trim($responseString);

        return $responseString;
    }

    /**
     * Send a request to the comodo API, and decodes the response as given
     *
     * @param string     $url
     * @param array      $params
     * @param int        $responseType
     * @param array|null $notDecode
     * @param array      $forceArray
     *
     * @return array|bool
     */
    public function sendToApi($url, array $params, $responseType = self::RESPONSE_NEW_LINE, array $notDecode = null, $forceArray = array())
    {
        $this->preSendToApiCheck();

        // Merging post-data
        $fields                  = array();
        $fields['loginName']     = $this->getAccount()->getLoginName();
        $fields['loginPassword'] = $this->getAccount()->getLoginPassword();
        $fields                  = array_merge($fields, $params);

        // Sending request
        $client   = $this->getClient();
        $response = $client->request(
            'POST',
            $url,
            ['form_params' => $fields]
        );
        $query    = http_build_query($params);

        // Getting response body
        $responseString = $response->getBody()->getContents();
        $responseString = trim($responseString);

        // Decoding and returning response
        if ($responseType == self::RESPONSE_NEW_LINE) {
            return $this->decodeNewLineEncodedResponse($responseString, $query, $forceArray);
        } else {
            return $this->decodeUrlEncodedResponse($responseString, $query, $notDecode, $forceArray);
        }
    }

    /**
     * Checks, if a valid account has been provided
     *
     * @return bool
     * @throws \Exception
     */
    protected function preSendToApiCheck()
    {
        if ($this->getAccount() === null) {
            throw new \Exception('Please provided an account');
        }

        if (!$this->getAccount()->isValid()) {
            throw new \Exception('Please provided valid account');
        }

        return true;
    }

    /**
     * Decodes a responseString, separated by new lines and returns an response array
     *
     * @param string $responseString
     * @param string $requestQuery
     * @param array  $forceArray
     *
     * @return array
     */
    protected function decodeNewLineEncodedResponse($responseString, $requestQuery, array $forceArray = array())
    {
        // Splitting response body
        $parts = explode(chr(10), $responseString);

        // Getting the status
        $status = trim($parts[0]);

        $responseArray = [];

        // Valid answer?
        if (is_numeric($status)) {
            // Successful?
            if ($status != '0') {
                $responseArray['errorCode']    = $status;
                $responseArray['errorMessage'] = trim($parts[1]);
            } else {
                $responseArray['errorCode']    = $status;

                $partCount = count($parts);
                for ($i = 1; $i < $partCount; $i++) {
                    $tmp = preg_split('/[\s\t]+/', $parts[$i], 2);

                    $key   = trim($tmp[0]);
                    $value = trim($tmp[1]);

                    // if key already exists, open new array dimension
                    if (isset($responseArray[$key])) {
                        if (!is_array($responseArray[$key])) {
                            $tmpValue              = $responseArray[$key];
                            $responseArray[$key]   = array();
                            $responseArray[$key][] = $tmpValue;
                            $responseArray[$key][] = $value;
                        } else {
                            $responseArray[$key][] = $value;
                        }
                    } else {
                        // Just save
                        $responseArray[$key] = $value;
                    }
                }
            }
        } else {
            $responseArray['errorCode']    = '';
            $responseArray['errorMessage'] = trim($responseString);
        }

        foreach ($forceArray as $value) {
            if (isset($responseArray[$value]) && !is_array($responseArray[$value])) {
                $responseArray[$value] = array($responseArray[$value]);
            }
        }

        $responseArray['responseString'] = $responseString;
        $responseArray['requestQuery']   = $requestQuery;

        return $responseArray;
    }

    /**
     *  Decodes a responseString, encoded in query-string-format and returns an response array
     *
     * @param string   $responseString
     * @param string   $requestQuery
     * @param array    $notDecode
     * @param string[] $forceArray
     *
     * @return mixed
     */
    protected function decodeUrlEncodedResponse(
        $responseString,
        $requestQuery,
        array $notDecode = null,
        $forceArray = array()
    )
    {
        if (empty($notDecode)) {
            $responseString = urldecode($responseString);
        }

        if (!empty($forceArray)) {
            foreach ($forceArray as $param) {
                $responseString = str_replace($param.'=', $param.'[]=', $responseString);
            }
        }

        // Splitting response body
        $responseArray = [];
        parse_str($responseString, $responseArray);

        if (!empty($notDecode) && is_array($responseArray)) {
            foreach ($responseArray as $index => $value) {
                if (!in_array($index, $notDecode)) {
                    $value = urldecode($value);
                }

                $responseArray[$index] = $value;
            }
        }

        $responseArray['responseString'] = $responseString;
        $responseArray['requestQuery']   = $requestQuery;

        if (!isset($responseArray['errorCode'])) {
            $responseArray['errorCode'] = 99;
            $responseArray['errorMessage'] = $responseString;
        }

        return $responseArray;
    }
}