<?php
namespace Paradox\pod;
use Paradox\Toolbox;
use Paradox\AModel;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Edge pod
 * Represents an ArangoDB edge. Implements IObserver to listen to events from the Pod Manager.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class Edge extends Document
{
    /**
     * Store a reference to the "from" vertex.
     * @var AModel
     */
    protected $_from;

    /**
     * Store a reference to the "to" vertex.
     * @var AModel
     */
    protected $_to;

    /**
     * Instantiates the pod.
     * @param Toolbox $toolbox      A reference to the toolbox that owns this pod.
     * @param array   $data         Any data that we wish to instantiate this pod with.
     * @param string  $new          Whether this pod should be marked as new (never been saved to the server).
     * @param string  $internalFrom The id (document handle) of the "from" vertex.
     * @param string  $internalTo   The id (document handle) of the "to" vertex.
     */
    public function __construct(Toolbox $toolbox, array $data = array(), $new = true, $internalFrom = null, $internalTo = null)
    {
        parent::__construct($toolbox, "edge", $data, $new);

        $this->setInternalFrom($internalFrom);
        $this->setInternalTo($internalTo);
    }

    /**
     * A convinence function to set the label of the edge.
     * @param string $label
     */
    public function setLabel($label)
    {
        $this->set('$label', $label);
    }

    /**
     * A convinence function to get the label of the edge.
     * @return null|string
     */
    public function getLabel()
    {
        return $this->get('$label');
    }

    /**
     * Set the "to" vertex of the edge.
     * @param AModel $to
     */
    public function setTo(AModel $to)
    {
        $this->_to = $to;
        $this->setInternalTo($to->getPod()->getId());
    }

    /**
     * Get the key (id without the collection) of the to node by checking for it in the actual _to property or checking an internal property.
     * This is used because when documents are retrieved from the server, we will not have a model in the _to property.
     * This allows us to lazy load any vertices we need later.
     * @return null|string
     */
    public function getToKey()
    {
        if (isset($this->_to)) {
            return $this->_to->getPod()->getKey();
        } elseif (isset($this->_data['_to'])) {
            return $this->parseIdForKey($this->_data['_to']);
        } else {
            return null;
        }
    }

    /**
     * Get the id of the to node by checking for it in the actual _to property or checking an internal property.
     * This is used because when documents are retrieved from the server, we will not have a model in the _to property.
     * This allows us to lazy load any vertices we need later.
     * @return null|string
     */
    public function getToId()
    {
        if (isset($this->_to)) {
            return $this->_to->getPod()->getId();
        } elseif (isset($this->_data['_to'])) {
            return $this->_data['_to'];
        } else {
            return null;
        }
    }

    /**
     * Set the "from" vertex of the edge.
     * @param AModel $from
     */
    public function setFrom(AModel $from)
    {
        $this->_from = $from;
        $this->setInternalFrom($from->getPod()->getId());
    }

    /**
     * Get the key (id without the collection) of the to node by checking for it in the actual _from property or checking an internal property.
     * This is used because when documents are retrieved from the server, we will not have a model in the _from property.
     * This allows us to lazy load any vertices we need later.
     * @return null|string
     */
    public function getFromKey()
    {
        if (isset($this->_from)) {
            return $this->_from->getPod()->getKey();
        } elseif (isset($this->_data['_from'])) {
            return $this->parseIdForKey($this->_data['_from']);
        } else {
            return null;
        }
    }

    /**
     * Get the id of the to node by checking for it in the actual _from property or checking an internal property.
     * This is used because when documents are retrieved from the server, we will not have a model in the _from property.
     * This allows us to lazy load any vertices we need later.
     * @return null|string
     */
    public function getFromId()
    {
        if (isset($this->_from)) {
            return $this->_from->getPod()->getId();
        } elseif (isset($this->_data['_from'])) {
            return $this->_data['_from'];
        } else {
            return null;
        }
    }

    /**
     * Get the from vertex. If we do not have the vertex yet (edge was loaded from the server), then fetch it from the server.
     * @return null|AModel
     */
    public function getFrom()
    {
        if (isset($this->_from)) {
            return $this->_from;
        }

        if (!isset($this->_from) && isset($this->_data['_from'])) {
            $toolbox = $this->_toolbox;

            $driverVertex = $toolbox->getDriver()->getVertex($toolbox->getGraph(), $this->parseIdForKey($this->_data['_from']));

            $this->_from = $toolbox->getPodManager()->convertDriverDocumentToPod($driverVertex);

            return $this->_from;
        }

        return null;
    }

    /**
     * Get the to vertex. If we do not have the vertex yet (edge was loaded from the server), then fetch it from the server.
     * @return null|AModel
     */
    public function getTo()
    {
        if (isset($this->_to)) {
            return $this->_to;
        }

        if (!isset($this->_to) && isset($this->_data['_to'])) {
            $toolbox = $this->_toolbox;

            $driverVertex = $toolbox->getDriver()->getVertex($toolbox->getGraph(), $this->parseIdForKey($this->_data['_to']));

            $this->_to = $toolbox->getPodManager()->convertDriverDocumentToPod($driverVertex);

            return $this->_to;
        }

        return null;
    }

    /**
     * Set the internal id pointing to the "to" vertex.
     * @param string $to
     */
    protected function setInternalTo($to)
    {
        $this->_data['_to'] = $to;
    }

    /**
     * Set the internal id pointing to the "from" vertex.
     * @param string $from
     */
    protected function setInternalFrom($from)
    {
        $this->_data['_from'] = $from;
    }

    /**
     * Get a list of reserved fields used by ArangoDB.
     * @return array
     */
    protected function getReservedFields()
    {
        return array('_id', '_key', '_rev', '_from', '_to');
    }

    /**
     * Returns an ArangoDB-PHP edge object representing this edge.
     * @return \triagens\ArangoDb\Edge
     */
    public function toDriverDocument()
    {
        $edge = new \triagens\ArangoDb\Edge;

        foreach ($this->_data as $key => $value) {
            $edge->set($key, $value);
        }

        if ($this->_id) {
            $edge->setInternalId($this->_id);
        }

        if ($this->_key) {
            $edge->setInternalKey($this->_key);
        }

        if ($this->_rev) {
            $edge->setRevision($this->_rev);
        }

        return $edge;
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Service;

use Core23\LastFm\Client\ApiClientInterface;
use Core23\LastFm\Model\Album;
use Core23\LastFm\Model\Artist;
use Core23\LastFm\Model\Chart;
use Core23\LastFm\Model\Song;
use Core23\LastFm\Model\Tag;
use Core23\LastFm\Model\TagInfo;
use Core23\LastFm\Util\ApiHelper;

final class TagService implements TagServiceInterface
{
    /**
     * @var ApiClientInterface
     */
    private $client;

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

    /**
     * {@inheritdoc}
     */
    public function getInfo(string $tag, string $lang = null): ?TagInfo
    {
        $response = $this->client->unsignedCall('tag.getInfo', [
            'tag'  => $tag,
            'lang' => $lang,
        ]);

        if (!isset($response['tag'])) {
            return null;
        }

        return TagInfo::fromApi($response['tag']);
    }

    /**
     * {@inheritdoc}
     */
    public function getSimilar(string $tag): array
    {
        $response = $this->client->unsignedCall('tag.getSimilar', [
            'tag' => $tag,
        ]);

        if (!isset($response['similartags']['tag'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Tag::fromApi($data);
            },
            $response['similartags']['tag']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getTopAlbums(string $tag, int $limit = 50, int $page = 1): array
    {
        $response = $this->client->unsignedCall('tag.getTopAlbums', [
            'tag'   => $tag,
            'limit' => $limit,
            'page'  => $page,
        ]);

        if (!isset($response['albums']['album'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Album::fromApi($data);
            },
            $response['albums']['album']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getTopArtists(string $tag, int $limit = 50, int $page = 1): array
    {
        $response = $this->client->unsignedCall('tag.getTopArtists', [
            'tag'   => $tag,
            'limit' => $limit,
            'page'  => $page,
        ]);

        if (!isset($response['topartists']['artist'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Artist::fromApi($data);
            },
            $response['topartists']['artist']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getTopTags(): array
    {
        $response = $this->client->unsignedCall('tag.getTopTags');

        if (!isset($response['toptags']['tag'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Tag::fromApi($data);
            },
            $response['toptags']['tag']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getTopTracks(string $tag, int $limit = 50, int $page = 1): array
    {
        $response = $this->client->unsignedCall('tag.getTopTracks', [
            'tag'   => $tag,
            'limit' => $limit,
            'page'  => $page,
        ]);

        if (!isset($response['tracks']['track'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Song::fromApi($data);
            },
            $response['tracks']['track']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getWeeklyChartList(string $tag): array
    {
        $response = $this->client->unsignedCall('tag.getWeeklyChartList', [
            'tag' => $tag,
        ]);

        if (!isset($response['weeklychartlist']['chart'])) {
            return [];
        }

        return ApiHelper::mapList(
            static function ($data) {
                return Chart::fromApi($data);
            },
            $response['weeklychartlist']['chart']
        );
    }
}


<?php
namespace Paradox\toolbox;
use Paradox\Toolbox;
use Paradox\pod\Vertex;
use Paradox\exceptions\TransactionManagerException;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Transaction
 * Manages transactions.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class TransactionManager
{
    /**
     * A reference to the toolbox.
     * @var Toolbox
     */
    private $_toolbox;

    /**
     * Whether there is an active transaction or not.
     * @var boolean
     */
    private $_activeTransaction = false;

    /**
     * Whether the transaction is paused.
     * @var boolean
     */
    private $_transactionPaused = true;

    /**
     * An array holding the collections that will be locked and written to or read from in the transaction.
     * @var array
     */
    private $_collections = array('write' => array(), 'read' => array());

    /**
     * A list of commands for the transaction.
     * @var array
     */
    private $_commands = array();

    /**
     * An array of names for which we want results from the transaction to be returned.
     * @var array
     */
    private $_registeredResults = array();

    /**
     * Initiate the transaction manager.
     * @param Toolbox $toolbox
     */
    public function __construct(Toolbox $toolbox)
    {
        $this->_toolbox = $toolbox;
    }

    /**
     * Begin a transaction.
     * @return boolean
     */
    public function begin()
    {
        if ($this->_activeTransaction) {
            throw new TransactionManagerException("An active transaction already exists.");
        }

        $this->_activeTransaction = true;
        $this->_transactionPaused = false;

        return true;
    }

    /**
     * Commit the transaction.
     * @return array
     */
    public function commit()
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction to commit.");
        }

        if (empty($this->_commands)) {
            throw new TransactionManagerException("There is no transaction operations to commit.");
        }

        $commandText = 'function () { var db = require("internal").db; ';

        //Check if there are graph operations
        foreach ($this->_commands as $command) {

            if ($command['isGraph']) {
                $commandText .= "var g = require('org/arangodb/graph').Graph; var graph = new g('{$this->_toolbox->getGraph()}'); ";
                break;
            }
        }

        $commandText .= 'var result = {}; ';

        //Now, add the commands
        foreach ($this->_commands as $id => $command) {

            $commandText .= "result.$id = {$command['command']} ";
        }

        $commandText .= "return result; }";

        //Send the transaction
        $result = $this->executeTransaction($commandText, $this->_collections['read'], $this->_collections['write']);

        //Process the result
        $processed = $this->processResult($result);

        $this->clearTransactionInfo();

        return $processed;
    }

    /**
     * Cancel the transaction.
     * @return boolean
     */
    public function cancel()
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction to cancel.");
        }

        $this->clearTransactionInfo();

        return true;
    }

    /**
     * Clear the saved information for the transaction.
     */
    private function clearTransactionInfo()
    {
        $this->_activeTransaction = false;
        $this->_transactionPaused = true;
        $this->_collections = array('write' => array(), 'read' => array());
        $this->_commands = array();
        $this->_registeredResults = array();
    }

    /**
     * Add a collection that will be locked for reading.
     * @param string $collection The name of the collection.
     */
    public function addReadCollection($collection)
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction.");
        }

        if (!in_array($collection, $this->_collections['read'])) {
            $this->_collections['read'][] = $collection;
        }
    }

    /**
     * Add a collection that will be locked for writing.
     * @param string $collection The name of the collection.
     */
    public function addWriteCollection($collection)
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction.");
        }

        if (!in_array($collection, $this->_collections['write'])) {
            $this->_collections['write'][] = $collection;
        }
    }

    /**
     * Register the result of a transaction operation under a name, so that the result can be retrieved after the transaction has finished.
     * @param  string  $name    The name to store the results under.
     * @param  string  $command A placebo that does nothing, but allows one to explicitly associate a registration with a transaction operation.
     * @return boolean
     */
    public function registerResult($name, $command = null)
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction.");
        }

        if (empty($this->_commands)) {
            throw new TransactionManagerException("There are no commands for this transaction.");
        }

        //Get the last command's id (current array element)
        end($this->_commands);
        $id = key($this->_commands);
        reset($this->_commands);

        $this->_registeredResults[$id] = $name;

        return true;
    }

    /**
     * Send a raw transaction to the server and return the result.
     * @param  string                      $action           The javascript function for the transaction.
     * @param  array                       $readCollections  An array of collections to be locked for reading.
     * @param  array                       $writeCollections An array of collections to be locked for writing.
     * @param  array                       $parameters       An array of parameters for executing the transaction multiple times.
     * @throws TransactionManagerException
     * @return mixed
     */
    public function executeTransaction($action, $readCollections = array(), $writeCollections = array(), $parameters = array())
    {
        $transaction = $this->_toolbox->getTransactionObject();
        $transaction->setAction($action);
        $transaction->setReadCollections($readCollections);
        $transaction->setWriteCollections($writeCollections);

        if (!empty($parameters)) {
            $transaction->setParams($parameters);
        }

        //Send
        try {
            $result = $transaction->execute();

            return $result;
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new TransactionManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Pause the transaction, so that operations after this point are not part of the transaction.
     */
    public function pause()
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction.");
        }

        if ($this->_transactionPaused) {
            throw new TransactionManagerException("The transaction is already paused.");
        }

        $this->_transactionPaused = true;
    }

    /**
     * Resume the transaction, so that operations after this point are part of the transaction.
     */
    public function resume()
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction.");
        }

        if (!$this->_transactionPaused) {
            throw new TransactionManagerException("The transaction is not paused.");
        }

        $this->_transactionPaused = false;
    }

    /**
     * Add a command to be executed for the transaction.
     * @param  string $command The javascript command for the transaction.
     * @param  string $action  The name of the operation.
     * @param  string $object  An optional object or model that can be used to process results after committing the transaction.
     * @param  string $isGraph Whether this command is for a graph or not.
     * @param  array  $data    Any other data that should be stored with this command.
     * @return string $id The id of this command.
     */
    public function addCommand($command, $action, $object = null, $isGraph = false, $data = array())
    {
        if (!$this->_activeTransaction) {
            throw new TransactionManagerException("There is no active transaction.");
        }

        $id = $this->random();
        $this->_commands[$id] = array('command' => $command, 'action' => $action, 'object' => $object, 'isGraph' => $isGraph, 'data' => $data);

        return $id;
    }

    /**
     * Processes the transaction result after committing.
     * @param  array                       $results The results array from the server.
     * @throws TransactionManagerException
     * @return array
     */
    private function processResult($results)
    {
        $processedResults = array();

        foreach ($results as $id => $result) {

            $command = $this->_commands[$id];

            switch ($command['action']) {

                case "PodManager:store":
                    $processed = $this->_toolbox->getPodManager()->processStoreResult($command['object']->getPod(), $result['_rev'], $result['_id']);
                    break;

                case "PodManager:delete":
                    $this->_toolbox->getPodManager()->processDeleteResult($command['object']->getPod());
                    $processed = true;
                    break;

                case "PodManager:load":

                    $parsedId = $this->_toolbox->parseId($result['_id']);

                    if (!$result) {
                        $processed = null;
                        break;
                    }

                    $processed = $this->_toolbox->getPodManager()->convertArrayToPod($command['data']['type'], $result);
                    break;

                case "Query:getOne":
                case "Query:getAll":
                    $processed = $result;
                    break;

                case "Finder:find":
                case "Finder:findAll":
                case "Finder:findNear":
                case "Finder:findAllNear":
                case "Finder:findWithin":
                case "Finder:findAllWithin":
                case "Finder:search":
                case "Finder:searchAll":
                    if (isset($command['data']['coordinates'])) {
                        $processed = $this->_toolbox->getFinder()->convertToPods($command['data']['type'], $result, $command['data']['coordinates']);
                    } else {
                        $processed = $this->_toolbox->getFinder()->convertToPods($command['data']['type'], $result);
                    }

                    break;

                case "Finder:findOne":
                case "Finder:any":
                case "Finder:findOneNear":
                case "Finder:findOneWithin":
                case "Finder:searchForOne":

                    if ($result == null) {
                        $processed = null;
                        break;
                    }

                    if (isset($command['data']['coordinates'])) {
                        $processed = $this->_toolbox->getFinder()->convertToPods($command['data']['type'], array($result), $command['data']['coordinates']);
                    } else {
                        $processed = $this->_toolbox->getFinder()->convertToPods($command['data']['type'], array($result));
                    }

                    $processed = reset($processed);

                    break;

                case "GraphManager:getInboundEdges":
                case "GraphManager:getOutboundEdges":
                case "GraphManager:getEdges":
                    $processed = $this->_toolbox->getGraphManager()->convertToPods("edge", $result);
                    break;

                case "GraphManager:getNeighbours":
                    $processed = $this->_toolbox->getGraphManager()->convertToPods("vertex", $result);
                    break;

                default:
                    throw new TransactionManagerException("Invalid or unimplemented action ({$command['action']}) while processing the transaction results.");
            }

            if (array_key_exists($id, $this->_registeredResults)) {
                $processedResults[$this->_registeredResults[$id]] = $processed;
            }
        }

        return $processedResults;
    }

    /**
     * Search the commands for an action and its object. If a match is found, the position and id is returned.
     * The search starts in reverse direction.
     * @param  string $action The action to search on
     * @param  mixed  $object The object to match.
     * @return array
     */
    public function searchCommandsByActionAndObject($action, $object)
    {
        $position = 0;
        $length = count($this->_commands);
        foreach (array_reverse($this->_commands) as $id => $command) {

            if ($command['action'] == $action && $command['object'] === $object) {
                return array('position' => $length - 1 - $position, 'id' => $id);
            }

            $position++;
        }

        return null;
    }

    /**
     * Convinence function to generate a random name for each command that will be sent to the server.
     * @return string
     */
    private function random()
    {
        $characters = 'abcdefghijklmnopqrstuvwxyz';

        $id = '';

        while (strlen($id) < 7 || in_array($id, array_keys($this->_commands))) {
            $id .= $characters[rand(0, strlen($characters) - 1)];
        }

        return $id;
    }

    /**
     * Whether a transaction has been started or not.
     * @return boolean
     */
    public function transactionStarted()
    {
        return $this->_activeTransaction;
    }

    /**
     * Returns whether there is an active transaction that has not been paused.
     * @return boolean
     */
    public function hasTransaction()
    {
        return $this->_activeTransaction && !$this->_transactionPaused;
    }
}


<?php

declare(strict_types=1);

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Model;

use Core23\LastFm\Exception\ApiException;

final class Chart
{
    /**
     * @var \DateTime
     */
    private $from;

    /**
     * @var \DateTime
     */
    private $to;

    /**
     * @param \DateTime $from
     * @param \DateTime $to
     */
    public function __construct(\DateTime $from, \DateTime $to)
    {
        $this->from = $from;
        $this->to   = $to;
    }

    /**
     * @return \DateTime
     */
    public function getFrom(): \DateTime
    {
        return $this->from;
    }

    /**
     * @return \DateTime
     */
    public function getTo(): \DateTime
    {
        return $this->to;
    }

    /**
     * @param array $data
     *
     * @throws ApiException
     *
     * @return Chart
     */
    public static function fromApi(array $data): self
    {
        $fromDate = \DateTime::createFromFormat('U', $data['from']);

        if (false === $fromDate) {
            throw new ApiException('Error fetching from date');
        }

        $toDate = \DateTime::createFromFormat('U', $data['to']);

        if (false === $toDate) {
            throw new ApiException('Error fetching to date');
        }

        return new self($fromDate, $toDate);
    }
}


<?php

/*
 * (c) Christian Gripp <mail@core23.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Core23\LastFm\Builder;

final class SimilarTrackBuilder
{
    /**
     * @var array
     */
    private $query;

    private function __construct()
    {
        $this->query = [];
    }

    /**
     * @param string $artist
     * @param string $track
     *
     * @return SimilarTrackBuilder
     */
    public static function forTrack(string $artist, string $track): self
    {
        $builder = new static();

        $builder->query['artist'] = $artist;
        $builder->query['track']  = $track;

        return $builder;
    }

    /**
     * @param string $mbid
     *
     * @return SimilarTrackBuilder
     */
    public static function forMbid(string $mbid): self
    {
        $builder = new static();

        $builder->query['mbid'] = $mbid;

        return $builder;
    }

    /**
     * @param bool $autocorrect
     *
     * @return SimilarTrackBuilder
     */
    public function autocorrect(bool $autocorrect): self
    {
        $this->query['autocorrect'] =  $autocorrect ? 1 : 0;

        return $this;
    }

    /**
     * @param int $limit
     *
     * @return SimilarTrackBuilder
     */
    public function limit(int $limit): self
    {
        $this->query['limit'] = $limit;

        return $this;
    }

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


<?php
namespace Paradox\toolbox;
use Paradox\Toolbox;
use Paradox\exceptions\GraphManagerException;
use Paradox\AModel;
use Paradox\pod\Document;

/**
 * Paradox is an elegant Object Document Mananger (ODM) to use with the ArangoDB Document/Graph database server.
 * Paradox requires ArangoDB-PHP to communication with the server, so it needs to be installed and avaliable.
 *
 * Graph manager
 * Manages graphs, for example, creating and deleting graphs.
 *
 * @version 1.3.0
 *
 * @author Francis Chuang <francis.chuang@gmail.com>
 * @link https://github.com/F21/Paradox
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache 2 License
 */
class GraphManager
{
    /**
     * A reference to the toolbox.
     * @var Toolbox
     */
    private $_toolbox;

    /**
     * Instantiates the finder.
     * @param Toolbox $toolbox
     */
    public function __construct(Toolbox $toolbox)
    {
        $this->_toolbox = $toolbox;
    }

    /**
     * Create a graph using the current connection's settings and add it as a connection.
     * You do not need to set the vertex and edge collections. They are automatically named for you.
     * @param  string                $name The name of the graph.
     * @throws GraphManagerException
     * @return boolean
     */
    public function createGraph($name)
    {
        try {
            $graph = new \triagens\ArangoDb\Graph($name);
            $graph->setVerticesCollection($name . 'VertexCollection');
            $graph->setEdgesCollection($name . 'EdgeCollection');

            return $this->_toolbox->getGraphHandler()->createGraph($graph);

        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new GraphManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Delete a graph.
     * @param  string                $name The name of the graph.
     * @throws GraphManagerException
     */
    public function deleteGraph($name)
    {
        try {
            $graphHandler = $this->_toolbox->getGraphHandler();

            return $graphHandler->dropGraph($name);
        } catch (\Exception $e) {
            $normalised = $this->_toolbox->normaliseDriverExceptions($e);
            throw new GraphManagerException($normalised['message'], $normalised['code']);
        }
    }

    /**
     * Get information about a graph.
     * @param  string                $name The name of the graph.
     * @throws GraphManagerException
     * @return array
     */
    public function getGraphInfo($name)
    {
        $graphHandler = $this->_toolbox->getGraphHandler();

        $graph = $graphHandler->getGraph($name);

        if(!$graph){
            throw new GraphManagerException("Graph does not exist");
        }

        $result = array();

        $result['id'] = $graph->getKey();
        $result['name'] = $graph->getKey();
        $result['revision'] = $graph->getRevision();
        $result['verticesCollection'] = $graph->getVerticesCollection();
        $result['edgesCollection'] = $graph->getEdgesCollection();

        return $result;

    }

    /**
     * Get all inbound edges to this vertex. The edges can be filtered by their labels and AQL.
     * @param Document|Model|string A vertex pod, model or string of the vertex id.
     * @param  string $label       A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string $aql         An optional AQL fragment if we want to filter the edges, for example: FILTER doc.someproperty == "somevalue"
     * @param  array  $params      An optional associative array containing parameters to bind to the query.
     * @param  string $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getInboundEdges($model, $label = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        $id = $this->getVertexId($model);

        if (!$id) {
            return array();
        }

        $collectionParameter = $this->_toolbox->generateBindingParameter('@collection', $params);
        $vertexParameter = $this->_toolbox->generateBindingParameter('vertexid', $params);
        $directionParameter = $this->_toolbox->generateBindingParameter('direction', $params);

        if (!$label) {
            $query = "FOR $placeholder in EDGES(@$collectionParameter, @$vertexParameter, @$directionParameter) " . $aql . " return $placeholder";
        } else {
            $labelParameter = $this->_toolbox->generateBindingParameter('label', $params);
            $params[$labelParameter] = $label;
            $query = "FOR $placeholder in EDGES(@$collectionParameter, @$vertexParameter, @$directionParameter, [{'\$label': @$labelParameter}]) " . $aql . " return $placeholder";
        }

        $params[$collectionParameter] = $this->_toolbox->getEdgeCollectionName();
        $params[$vertexParameter] = $id;
        $params[$directionParameter] = "inbound";

        if ($this->_toolbox->getTransactionManager()->hasTransaction()) {
            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getEdgeCollectionName());
            $statement = json_encode(array('query' => $query, 'bindVars' => $params), JSON_FORCE_OBJECT);
            $this->_toolbox->getTransactionManager()->addCommand("db._createStatement($statement).execute().elements();", "GraphManager:getInboundEdges", null, true);

        } else {
            try {
                $result = $this->_toolbox->getQuery()->getAll($query, $params);
            } catch (\Exception $e) {
                throw new GraphManagerException($e->getMessage(), $e->getCode());
            }

            if (empty($result)) {
                return array();
            }

            return $this->convertToPods("edge", $result);
        }
    }

    /**
     * Get all outbound edges to this vertex. The edges can be filtered by their labels and AQL.
     * @param Document|Model|string A vertex pod, model or string of the vertex id.
     * @param  string $label       A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string $aql         An optional AQL fragment if we want to filter the edges, for example: FILTER doc.someproperty == "somevalue"
     * @param  array  $params      An optional associative array containing parameters to bind to the query.
     * @param  string $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getOutboundEdges($model, $label = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        $id = $this->getVertexId($model);

        if (!$id) {
            return array();
        }

        $collectionParameter = $this->_toolbox->generateBindingParameter('@collection', $params);
        $vertexParameter = $this->_toolbox->generateBindingParameter('vertexid', $params);
        $directionParameter = $this->_toolbox->generateBindingParameter('direction', $params);

        if (!$label) {
            $query = "FOR $placeholder in EDGES(@$collectionParameter, @$vertexParameter, @$directionParameter) " . $aql . " return $placeholder";
        } else {
            $labelParameter = $this->_toolbox->generateBindingParameter('label', $params);
            $params[$labelParameter] = $label;
            $query = "FOR $placeholder in EDGES(@$collectionParameter, @$vertexParameter, @$directionParameter, [{'\$label': @$labelParameter}]) " . $aql . " return $placeholder";
        }

        $params[$collectionParameter] = $this->_toolbox->getEdgeCollectionName();
        $params[$vertexParameter] = $id;
        $params[$directionParameter] = "outbound";

        if ($this->_toolbox->getTransactionManager()->hasTransaction()) {
            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getEdgeCollectionName());
            $statement = json_encode(array('query' => $query, 'bindVars' => $params), JSON_FORCE_OBJECT);
            $this->_toolbox->getTransactionManager()->addCommand("db._createStatement($statement).execute().elements();", "GraphManager:getOutboundEdges", null, true);

        } else {
            try {
                $result = $this->_toolbox->getQuery()->getAll($query, $params);
            } catch (\Exception $e) {
                throw new GraphManagerException($e->getMessage(), $e->getCode());
            }

            if (empty($result)) {
                return array();
            }

            return $this->convertToPods("edge", $result);
        }
    }

    /**
     * Get all edges connected to this vertex. The edges can be filtered by their labels and AQL.
     * @param Document|Model|string A vertex pod, model or string of the vertex id.
     * @param  string $label       A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string $aql         An optional AQL fragment if we want to filter the edges, for example: FILTER doc.someproperty == "somevalue"
     * @param  array  $params      An optional associative array containing parameters to bind to the query.
     * @param  string $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getEdges($model, $label = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        $id = $this->getVertexId($model);

        if (!$id) {
            return array();
        }

        $collectionParameter = $this->_toolbox->generateBindingParameter('@collection', $params);
        $vertexParameter = $this->_toolbox->generateBindingParameter('vertexid', $params);
        $directionParameter = $this->_toolbox->generateBindingParameter('direction', $params);

        if (!$label) {
            $query = "FOR $placeholder in EDGES(@$collectionParameter, @$vertexParameter, @$directionParameter) " . $aql . " return $placeholder";
        } else {
            $labelParameter = $this->_toolbox->generateBindingParameter('label', $params);
            $params[$labelParameter] = $label;
            $query = "FOR $placeholder in EDGES(@$collectionParameter, @$vertexParameter, @$directionParameter, [{'\$label': @$labelParameter}]) " . $aql . " return $placeholder";
        }

        $params[$collectionParameter] = $this->_toolbox->getEdgeCollectionName();
        $params[$vertexParameter] = $id;
        $params[$directionParameter] = "any";

        if ($this->_toolbox->getTransactionManager()->hasTransaction()) {
            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getEdgeCollectionName());
            $statement = json_encode(array('query' => $query, 'bindVars' => $params), JSON_FORCE_OBJECT);
            $this->_toolbox->getTransactionManager()->addCommand("db._createStatement($statement).execute().elements();", "GraphManager:getEdges", null, true);

        } else {
            try {
                $result = $this->_toolbox->getQuery()->getAll($query, $params);
            } catch (\Exception $e) {
                throw new GraphManagerException($e->getMessage(), $e->getCode());
            }

            if (empty($result)) {
                return array();
            }

            return $this->convertToPods("edge", $result);
        }
    }

    /**
     * Get the neighbour vertices connected to this vertex via some edge. The vertices and their connecting edges can be filtered by AQL.
     * @param Document|AModel|string A vertex pod, model or string of the vertex id.
     * @param  string       $direction   "in" for inbound neighbours, "out" for outbound neighbours and "any" for all neighbours.
     * @param  string|array $labels      A string representing one label or an array of labels we want the inbound edges to have.
     * @param  string       $aql         An optional AQL fragment if we want to filter the edges or vertices, for example:
     *                                   FILTER doc.edge.someproperty == "somevalue" && doc.vertex.anotherproperty == "anothervalue"
     * @param  array        $params      An optional associative array containing parameters to bind to the query.
     * @param  string       $placeholder Set this to something else if you do not wish to use "doc" to refer to documents in your query.
     * @return array
     */
    public function getNeighbours($model, $direction = "any", $labels = null, $aql = "", $params = array(), $placeholder = "doc")
    {
        $id = $this->getVertexId($model);

        if (!$id) {
            return array();
        }

        if (strtolower($direction) == "in") {
            $direction = "inbound";
        }

        if (strtolower($direction) == "out") {
            $direction = "outbound";
        }

        if ($labels && !is_array($labels)) {
            $labels = array($labels);
        }

        $vertexCollection = $this->_toolbox->generateBindingParameter('@vertexCollection', $params);
        $edgeCollection = $this->_toolbox->generateBindingParameter('@edgeCollection', $params);
        $vertexParameter = $this->_toolbox->generateBindingParameter('vertexid', $params);
        $directionParameter = $this->_toolbox->generateBindingParameter('direction', $params);

        if (!$labels) {
            $query = "FOR $placeholder in NEIGHBORS(@$vertexCollection, @$edgeCollection, @$vertexParameter, @$directionParameter) " . $aql . " return $placeholder.vertex";
        } else {
            $count = 0;

            $labelString = '';

            foreach ($labels as $label) {
                $labelParameter = $this->_toolbox->generateBindingParameter('label', $params);

                if ($count > 0) {
                    $labelString .= ", ";
                }

                $labelString .= "{'\$label': @$labelParameter}";

                $params[$labelParameter] = $label;
                $count++;
            }

            $query = "FOR $placeholder in NEIGHBORS(@$vertexCollection, @$edgeCollection, @$vertexParameter, @$directionParameter, [$labelString]) " . $aql . " return $placeholder.vertex";
        }

        $params[$vertexCollection] = $this->_toolbox->getVertexCollectionName();
        $params[$edgeCollection] = $this->_toolbox->getEdgeCollectionName();
        $params[$vertexParameter] = $id;
        $params[$directionParameter] = $direction;

        if ($this->_toolbox->getTransactionManager()->hasTransaction()) {
            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getEdgeCollectionName());
            $this->_toolbox->getTransactionManager()->addReadCollection($this->_toolbox->getVertexCollectionName());
            $statement = json_encode(array('query' => $query, 'bindVars' => $params), JSON_FORCE_OBJECT);
            $this->_toolbox->getTransactionManager()->addCommand("db._createStatement($statement).execute().elements();", "GraphManager:getNeighbours", null, true);

        } else {
            try {
                $result = $this->_toolbox->getQuery()->getAll($query, $params);
            } catch (\Exception $e) {
                throw new GraphManagerException($e->getMessage(), $e->getCode());
            }

            if (empty($result)) {
                return array();
            }

            return $this->convertToPods("vertex", $result);
        }
    }

    /**
     * Convinence function that gets the document id (handle) from models, vertex pods and strings.
     * @param  Document|AModel|string $model
     * @throws GraphManagerException
     */
    protected function getVertexId($model)
    {
        if ($model instanceof AModel) {
            return $model->getPod()->getId();
        } elseif ($model instanceof Document) {
            return $model->getId();
        } elseif (is_string($model)) {
            return $model;
        } else {
            throw new GraphManagerException('$model can be either a model, a vertex pod or the id of the vertex.');
        }
    }

    /**
     * Converts the an array of associative arrays (each representing a document) received from the server into pods.
     * @param  string $type   The collection type. For graphs, only "vertex" or "edge" is valid.
     * @param  array  $result The array of documents to convert.
     * @return array
     */
    public function convertToPods($type, $result)
    {
        $converted = $this->_toolbox->getPodManager()->convertToPods($type, $result);

        foreach ($converted as $model) {

            $model->getPod()->setSaved();
        }

        return $converted;
    }
}


<?php
namespace ListFilter\View\Helper;

use Cake\Routing\Router;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use Cake\View\Helper;
use Cake\View\StringTemplateTrait;

class ListFilterHelper extends Helper
{
    use StringTemplateTrait;

    /**
     * Used Helpers
     *
     * @var array
     */
    public $helpers = ['Html', 'Form'];

    /**
     * ListFilter Config
     *
     * @var array
     */
    protected $_filters = [];

    /**
     * Default Config
     *
     * @var array
     */
    protected $_defaultConfig = [
        'formOptions' => [],
        'includeJavascript' => true,
        'templates' => [
            'containerStart' => '
',
            'containerEnd' => '
',
            'toggleButton' => '',
            'header' => '
{{title}}
{{toggleButton}}

',
            'contentStart' => '
',
            'contentEnd' => '
',
            'buttons' => '
{{buttons}}
'
        ],
        'containerClasses' => 'panel panel-default list-filter',
        'contentClasses' => 'panel-body',
        'toggleButtonClasses' => 'btn btn-xs toggle-btn',
        'title' => 'Filter',
        'filterButtonOptions' => [
            'div' => false,
            'class' => 'btn btn-xs btn-primary'
        ],
        'resetButtonOptions' => [
            'class' => 'btn btn-default btn-xs',
            'pass' => [
                'resetFilters' => true
            ]
        ]
    ];

    /**
     * Returns the filter config
     *
     * @return array
     */
    public function getFilters()
    {
        if ($this->_filters) {
            return $this->_filters;
        } elseif (isset($this->_View->viewVars['filters'])) {
            return $this->_View->viewVars['filters'];
        }

        return [];
    }

    /**
     * Render the complete filter widget
     *
     * @param array $filters Filters to render. If none given, it uses a viewVar called "filters"
     * @return string
     */
    public function renderFilterbox($filters = null)
    {
        if ($filters) {
            $this->_filters = $filters;
        }

        $filterBox = $this->openContainer();
        $filterBox .= $this->openForm();
        $filterBox .= $this->renderAll();
        $filterBox .= $this->closeForm();
        $filterBox .= $this->closeContainer();

        $ret = $this->_View->element('ListFilter.wrapper', [
            'filterBox' => $filterBox,
            'options' => $this->config()
        ]);

        return $ret;
    }

    /**
     * Render all filter widgets
     *
     * @return string
     */
    public function renderAll()
    {
        $widgets = [];

        foreach ($this->getFilters() as $field => $options) {
            $w = $this->filterWidget($field, $options, false);
            if ($w) {
                $widgets = array_merge($widgets, $w);
            }
        }
        // FIXME allow column-layout to be configured with templates.
        $ret = '
';
        foreach ($widgets as $i => $widget) {
            $ret .= '
';
            $ret .= $widget;
            $ret .= '
';
            if (($i + 1) % 2 === 0) {
                $ret .= '

';
            }
        }
        $ret .= '
';

        return $ret;
    }

    /**
     * Outputs a filter widget based on configuration
     *
     * @param string $field The field to filter with
     * @param array $options Options defined in Controller::listFilters()
     * @param bool $returnString If false, an array of the generated widget markup is being returned
     * @return string|array Markup string or array of the markup of one or multiple filter input widgets
     */
    public function filterWidget($field, $options = [], $returnString = true)
    {
        $filters = $this->getFilters();
        if (empty($options) && !isset($filters[$field])) {
            trigger_error("No config found for field '{{$field}}'", E_USER_WARNING);

            return false;
        }

        // make sure options isn't merged, as it potentially has int keys which will be doubled
        if (isset($options['options'])) {
            unset($filters[$field]['options']);
        }

        $options = Hash::merge($filters[$field], $options);

        $ret = [];
        switch ($options['searchType']) {
            case 'betweenDates':
                $empty = isset($options['inputOptions']['empty']) ? $options['inputOptions']['empty'] : true;

                $fromFieldName = 'Filter.' . $field . '_from';
                $toFieldName = 'Filter.' . $field . '_to';

                if (empty($options['inputOptions']['label'])) {
                    $options['inputOptions']['label'] = $field;
                }

                $fromOptions = [
                    'label' => $options['inputOptions']['label'] . ' ' . __d('list_filter', 'from'),
                    'type' => 'date'
                ];
                $toOptions = [
                    'label' => $options['inputOptions']['label'] . ' ' . __d('list_filter', 'to'),
                    'type' => 'date'
                ];
                if (!empty($options['inputOptions']['from'])) {
                    $fromOptions = Hash::merge($fromOptions, $options['inputOptions']['from']);
                }
                if (!empty($options['inputOptions']['to'])) {
                    $toOptions = Hash::merge($toOptions, $options['inputOptions']['to']);
                }

                if ($empty) {
                    // map the empty option to both option arrays
                    $fromOptions['empty'] = true;
                    $toOptions['empty'] = true;
                    // if the empty option was set, make sure date inputs are not set to the current date by default
                    if (empty($fromOptions['val']) && empty($this->Form->context()->val($fromFieldName))) {
                        $fromOptions['val'] = '';
                    }
                    if (empty($toOptions['val']) && empty($this->Form->context()->val($toFieldName))) {
                        $toOptions['val'] = '';
                    }
                }

                $ret[] = $this->Form->input($fromFieldName, $fromOptions);
                $ret[] = $this->Form->input($toFieldName, $toOptions);
                break;
            case 'multipleselect':
                $inputOptions = Hash::merge([
                    'type' => 'select',
                    'options' => $options['options'],
                    'multiple' => true,
                ], $options['inputOptions']);
                $ret[] = $this->Form->input('Filter.' . $field, $inputOptions);
                break;
            default:
                $inputOptions = Hash::merge([
                    'options' => isset($options['options']) ? $options['options'] : false,
                ], $options['inputOptions']);
                $ret[] = $this->Form->input('Filter.' . $field, $inputOptions);
                break;
        }
        if ($returnString) {
            return implode("\n", $ret);
        }

        return $ret;
    }

    /**
     * Opens the HTML container
     *
     * @return HTML
     */
    public function openContainer()
    {
        $classes = $this->config('containerClasses');

        $title = __d('list_filter', 'list_filter.filter_fieldset_title');

        if ($this->filterActive()) {
            $classes .= ' opened';
        } else {
            $classes .= ' closed';
        }

        $ret = $this->templater()->format('containerStart', [
            'attrs' => $this->templater()->formatAttributes([
                'class' => $classes
            ])
        ]);
        $ret .= $this->header();
        $ret .= $this->templater()->format('contentStart', [
            'attrs' => $this->templater()->formatAttributes([
                'class' => $this->config('contentClasses')
            ])
        ]);

        return $ret;
    }

    /**
     * Closes the HTML container
     *
     * @return string
     */
    public function closeContainer()
    {
        $ret = $this->templater()->format('contentEnd', []);
        $ret .= $this->templater()->format('containerEnd', []);

        return $ret;
    }

    /**
     * Opens the listfilter form
     *
     * @return string
     */
    public function openForm()
    {
        $options = Hash::merge(['url' => $this->here], $this->config('formOptions'));
        $ret = $this->Form->create('Filter', $options);

        return $ret;
    }

    /**
     * Closes the listfilter form
     *
     * @param bool $includeFilterButton add the search button
     * @param bool $includeResetButton add the reset button
     * @return string
     */
    public function closeForm($includeFilterButton = true, $includeResetButton = true)
    {
        $buttons = '';
        if ($includeFilterButton) {
            $buttons .= $this->filterButton();
        }
        if ($includeResetButton) {
            $buttons .= ' ' . $this->resetButton();
        }
        $ret = $this->templater()->format('buttons', [
            'buttons' => $buttons
        ]);
        $ret .= $this->Form->end();

        return $ret;
    }

    /**
     * Renders the header containing title and toggleButton
     *
     * @return string HTML
     */
    public function header()
    {
        return $this->templater()->format('header', [
            'title' => $this->config('title'),
            'toggleButton' => $this->toggleButton()
        ]);
    }

    /**
     * Renders the button for toggling the filter box
     *
     * @return string HTML
     */
    public function toggleButton()
    {
        return $this->templater()->format('toggleButton', [
            'attrs' => $this->templater()->formatAttributes([
                'class' => $this->config('toggleButtonClasses')
            ])
        ]);
    }

    /**
     * Determines if any ListFilter parameters are set
     *
     * @return bool
     */
    public function filterActive()
    {
        $filterActive = (isset($this->_View->viewVars['filterActive'])
                        && $this->_View->viewVars['filterActive'] === true);

        return $filterActive;
    }

    /**
     * Outputs the search button
     *
     * @param string $title button caption
     * @param array $options Options
     * @return string
     */
    public function filterButton($title = null, array $options = [])
    {
        if (!$title) {
            $title = __d('list_filter', 'list_filter.search');
        }
        $options = Hash::merge($this->config('filterButtonOptions'), $options);

        return $this->Form->button($title, $options);
    }

    /**
     * Outputs the filter reset link
     *
     * @param string $title caption for the reset button
     * @param array $options Options
     * @return string
     */
    public function resetButton($title = null, array $options = [])
    {
        if (!$title) {
            $title = __d('list_filter', 'list_filter.reset');
        }
        $params = $this->_View->request->query;
        if (!empty($params)) {
            foreach ($params as $field => $value) {
                if (substr($field, 0, 7) == 'Filter-') {
                    unset($params[$field]);
                }
            }
        }
        $options = Hash::merge($this->config('resetButtonOptions'), $options);
        $url = Hash::merge($this->request->params, [
            'resetFilter' => true
        ]);

        return $this->Html->link($title, Router::reverse($url), $options);
    }

    /**
     * Adds ListFilter-relevant named params to the given url. Used for detail links
     *
     * @param array $url URL to link to
     * @return array
     */
    public function addListFilterParams(array $url)
    {
        foreach ($this->_View->request->query as $key => $value) {
            if (substr($key, 0, 7) == 'Filter-' || in_array($key, ['page', 'sort', 'direction'])) {
                $url[$key] = $value;
            }
        }

        return $url;
    }

    /**
     * Renders a back-to-list button using the ListFilter-relevant named params
     *
     * @param string $title button caption
     * @param array $url url to link to
     * @param array $options link() config
     * @return string
     */
    public function backToListButton($title = null, array $url = null, array $options = [])
    {
        if (empty($url)) {
            $url = [
                'action' => 'index'
            ];
        }
        $options = Hash::merge([
            'class' => 'btn btn-default btn-xs',
            'escape' => false,
            'additionalClasses' => null,
            'useReferer' => false
        ], $options);

        if (!empty($options['useReferer']) && $this->request->referer(true) != '/' && $this->request->referer(true) != $this->request->here) {
            $url = $this->request->referer(true);
        }

        if (!$title) {
            $title = '' . __d('list_filter', 'forms.back_to_list') . '';
        }

        if ($options['additionalClasses']) {
            $options['class'] .= ' ' . $options['additionalClasses'];
        }

        if (empty($options['useReferer'])) {
            $url = $this->addListFilterParams($url);
        }
        $button = $this->Html->link(' ' . $title, $url, $options);

        return $button;
    }
}


<?php
namespace ListFilter\Controller\Component;

use Cake\Controller\Component;
use Cake\Routing\Router;
use Cake\Utility\Hash;

/**
 * Todos
 * - make class configurable with InstanceConfigTrait
 * - consolidate naming of searchTypes, better default list filter config
 * - fix weird escaping of [] for array values in URLs
 *
 * @package default
 */

class ListFilterComponent extends Component
{

    public $components = ['Cookie'];

    /**
     * Controller Instance to work with
     *
     * @var Cake\Controller\Controller
     */
    protected $_controller;

    /**
     * Default array structure of a list filter.
     *
     * @var array
     */
    public $defaultListFilter = [
        'searchType' => 'wildcard',
        'inputOptions' => [
            'type' => 'text'
        ],
    ];

    public $config = [
        'FormSession' => [
            'active' => false,
            'namespace' => 'ListFilter'
        ],
        'FormCookie' => [
            'active' => false,
            'namespace' => 'ListFilter'
        ],
        'Validation' => [
            'validateOptions' => true
        ],
        'searchTermsConjunction' => 'AND'
    ];

    /**
     * Initializes the instance
     *
     * @param array $config Component configuration
     * @return void
     */
    public function initialize(array $config)
    {
        $this->config = Hash::merge($this->config, $config);
    }

    /**
     * Startup callback
     *
     * @param Event $event Controller::startup event
     * @return void
     */
    public function startup(\Cake\Event\Event $event)
    {
        $this->_controller = $event->subject();
        $controllerListFilters = $this->getFilters();

        if (empty($controllerListFilters)) {
            return;
        }
        // Redirect on Form Cookie or Form Session Data
        $this->_handlePersistedFilters();

        // Redirect POST to GET, include FormSession and FormCookie Data
        if ($this->_controller->request->is('post') && !empty($this->_controller->request->data['Filter'])) {
            $redirectUrl = $this->getRedirectUrlFromPostData($this->_controller->request->data);
            // Remove page param to paginate from the first page
            unset($redirectUrl['page']);
            // Save ListFilter Form Selection in Session
            if ($this->config['FormSession']['active']) {
                $this->_controller->request->session()->write(
                    $this->_getPersistendStorageKey('FormSession'),
                    $this->_controller->request->data['Filter']
                );
            }
            if ($this->config['FormCookie']['active']) {
                $this->Cookie->write(
                    $this->_getPersistendStorageKey('FormCookie'),
                    $this->_controller->request->data['Filter']
                );
            }

            return $this->_controller->redirect($redirectUrl);
        }

        // Do the ListFilter Job
        $filterConditions = [];
        if (!empty($this->_controller->request->query)) {
            $filterConditions = $this->_prepareFilterConditions($controllerListFilters);
            $conditions = isset($this->_controller->paginate['conditions']) ? $this->_controller->paginate['conditions'] : [];

            // Handle namespaced ListFilter
            if (isset($this->_controller->paginate[$this->_controller->name])) {
                $conditions = [
                    $this->_controller->name => [
                        'conditions' => Hash::merge($conditions, $filterConditions)
                    ]
                ];
            } else {
                $conditions = [
                    'conditions' => Hash::merge($conditions, $filterConditions)
                ];
            }
            $this->_controller->paginate = Hash::merge($this->_controller->paginate, $conditions);
        }
        foreach ($controllerListFilters['fields'] as $field => $options) {
            if (!empty($controllerListFilters['fields'][$field]['options'])) {
                $tmpOptions = $controllerListFilters['fields'][$field]['options'];
            }
            $controllerListFilters['fields'][$field] = Hash::merge($this->defaultListFilter, $options);
            if (isset($tmpOptions)) {
                $controllerListFilters['fields'][$field]['options'] = $tmpOptions;
            }
            unset($tmpOptions);
        }
        $this->_controller->set('filters', $controllerListFilters['fields']);
        $this->_controller->set('filterActive', !empty($filterConditions));
    }

    /**
     * Handles filters which were persisted through cookies and / or the session.
     *
     * @return mixed
     */
    protected function _handlePersistedFilters()
    {
        if (isset($this->request->query['resetFilter'])) {
            $this->_controller->request->session()->delete($this->_getPersistendStorageKey('FormSession'));
            $this->Cookie->delete($this->_getPersistendStorageKey('FormCookie'));

            return $this->_controller->redirect($this->request->here);
        }
        $persistedFilterData = [];
        if ($this->config['FormSession']['active']) {
            $persistedFilterData = Hash::merge(
                $persistedFilterData,
                $this->_controller->request->session()->read($this->_getPersistendStorageKey('FormSession'))
            );
        }
        if ($this->config['FormCookie']['active']) {
            $persistedFilterData = Hash::merge(
                $persistedFilterData,
                $this->Cookie->read($this->_getPersistendStorageKey('FormCookie'))
            );
        }
        if (!empty($persistedFilterData)) {
            // Redirect the first time, $persistedFilterData is present (Filterredirect param not set)
            if (empty($this->_controller->request->query['Filterredirect'])) {
                if (!empty($persistedFilterData['Pagination']['page'])) {
                    $this->_controller->passedArgs['page'] = $persistedFilterData['Pagination']['page'];
                }
                if (!empty($persistedFilterData['Pagination']['sort'])) {
                    $this->_controller->passedArgs['sort'] = $persistedFilterData['Pagination']['sort'];
                }
                if (!empty($persistedFilterData['Pagination']['direction'])) {
                    $this->_controller->passedArgs['direction'] = $persistedFilterData['Pagination']['direction'];
                }
                unset($persistedFilterData['Pagination']);
                $redirectUrl = $this->getRedirectUrlFromPostData(['Filter' => $persistedFilterData]);
                $redirectUrl['Filterredirect'] = 1;
                // Redirect
                return $this->_controller->redirect($redirectUrl);
            }
            // Reset Session, if no Filter is set

            if (!$this->filterUrlParameterStatus()) {
                unset($persistedFilterData);
                $this->_controller->request->session()->delete($this->_getPersistendStorageKey('FormSession'));
                $this->Cookie->delete($this->_getPersistendStorageKey('FormCookie'));
            }

            $persistedFilterData['Pagination'] = [];
            if (!empty($this->_controller->request->query['page'])) {
                $persistedFilterData['Pagination']['page'] = $this->_controller->request->query['page'];
            }
            if (!empty($this->_controller->request->query['sort'])) {
                $persistedFilterData['Pagination']['sort'] = $this->_controller->request->query['sort'];
            }
            if (!empty($this->_controller->request->query['direction'])) {
                $persistedFilterData['Pagination']['direction'] = $this->_controller->request->query['direction'];
            }
            $this->_controller->request->session()->write(
                $this->_getPersistendStorageKey('FormSession'),
                $persistedFilterData
            );
            $this->Cookie->write($this->_getPersistendStorageKey('FormCookie'), $persistedFilterData);
        }
    }

    /**
     * Get persistent storage key for ListFilter form handling
     *
     * @param  string  $key  The storage key defined in config
     * @return array
     */
    protected function _getPersistendStorageKey($key)
    {
        if (!isset($this->config[$key]['namespace'])) {
            $namespace = 'ListFilter';
        } else {
            $namespace = $this->config[$key]['namespace'];
        }
        $sessionKey = [
            'namespace' => $namespace,
            'plugin' => 'App',
            'controller' => $this->_controller->request->controller,
            'action' => $this->_controller->request->action
        ];
        if (!empty($this->_controller->request->plugin)) {
            $sessionKey['plugin'] = $this->_controller->request->plugin;
        }
        $sessionKey = implode('.', $sessionKey);

        return $sessionKey;
    }

    /**
     * checks URL for any Filter Parameter
     *
     * @return bool
     */
    public function filterUrlParameterStatus()
    {
        foreach ($this->_controller->request->query as $arg => $value) {
            if (substr($arg, 0, 7) == 'Filter-') {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether the filter has actually manipulated the pagination conditions
     *
     * @return bool
     */
    public function filterActive()
    {
        return isset($this->_controller->viewVars['filterActive']) ? $this->_controller->viewVars['filterActive'] : false;
    }

    /**
     * Uses the query parameters to construct the $filters config array to be passed to the front end
     * and sets request data so form fields are pre-filled
     *
     * @param array $controllerListFilters ListFilter configuration
     * @return array
     */
    protected function _prepareFilterConditions(array $controllerListFilters)
    {
        $filterConditions = [];
        foreach ($this->_controller->request->query as $arg => $value) {
            if (substr($arg, 0, 7) != 'Filter-') {
                continue;
            }
            unset($betweenDate);
            list($filter, $model, $field) = explode('-', $arg);

            // if betweenDate
            if (preg_match("/([a-z_\-\.]+)_(from|to)$/i", $field, $matches)) {
                $betweenDate = $matches[2];
                $field = $matches[1];
            }
            $conditionField = "{$model}.{$field}";
            if (!isset($controllerListFilters['fields'][$conditionField])) {
                continue;
            }

            $options = $controllerListFilters['fields'][$conditionField];

            if (is_string($value)) {
                $value = trim($value);
            }

            if (empty($value) && $value != 0) {
                continue;
            }
            if (isset($options['options'])) {
                $validOptions = $this->_flattenValueOptions($options['options']);
            }

            if ($this->config['Validation']['validateOptions']) {
                // for non-multiselects, check if the value is present in the defined valid options
                if (!empty($options['options'])
                    && $options['searchType'] != 'multipleselect'
                    && !isset($validOptions[$value])
                ) {
                    continue;
                }
                // for multiselects, filter out values not defined in value options
                if ($options['searchType'] == 'multipleselect') {
                    $value = array_intersect($value, array_keys($validOptions));
                }
            }

            // value to be used for form fields
            $viewValue = $value;

            if ($options['searchType'] == 'wildcard') {
                $value = "%{$value}%";
                $conditionField = $conditionField . ' LIKE';
            } elseif ($options['searchType'] == 'fulltext') {
                $filterConditions = $this->_getFulltextSearchConditions($conditionField, $value, $options);
            } elseif ($options['searchType'] == 'betweenDates') {
                $conditionField = 'DATE(' . $conditionField . ')';
                if ($betweenDate == 'from') {
                    $operator = '>=';
                } elseif ($betweenDate == 'to') {
                    $operator = '<=';
                }
                if (!empty($options['conditionField'])) {
                    $conditionField = $options['conditionField'];
                }
                $conditionField .= ' ' . $operator;
                list($year, $month, $day) = explode('-', $value);
                $viewValue = compact('year', 'month', 'day');
                $field .= '_' . $betweenDate;
            } elseif ($options['searchType'] == 'multipleselect') {
                $conditionField .= ' IN';
            }

            // fulltext search adds to $filterConditions itself
            if ($options['searchType'] != 'fulltext') {
                $filterConditions[$conditionField] = $value;
            }
            $this->_controller->request->data['Filter'][$model][$field] = $viewValue;
        }

        return $filterConditions;
    }

    /**
     * Splits the search term in value to multiple terms and constructs a OR-style
     * conditions array for each term, for each field to be searched.
     *
     * @param string $conditionField Primary field to search in
     * @param string $value Search Term
     * @param array $options Filter configuration
     * @return array
     */
    protected function _getFulltextSearchConditions($conditionField, $value, array $options)
    {
        $searchTerms = explode(' ', $value);
        $searchTerms = array_map('trim', $searchTerms);
        $searchTerms = array_map('mb_strtolower', $searchTerms);

        if (isset($options['termsCallback']) && is_callable($options['termsCallback'])) {
            $searchTerms = $options['termsCallback']($searchTerms);
        }

        $searchFields = [$conditionField];
        if (!empty($options['searchFields'])) {
            $searchFields = $options['searchFields'];
        }
        $conditions = [];
        $conjunction = $this->config['searchTermsConjunction'];
        foreach ($searchTerms as $key => $term) {
            $searchFieldConditions = [];

            if (is_array($term)) {
                foreach ($term as $item) {
                    foreach ($searchFields as $searchField) {
                        $searchFieldConditions["{$searchField} LIKE"] = "%{$item}%";
                    }

                    $conditions[$conjunction][$key]['OR'][] = [
                        'OR' => $searchFieldConditions
                    ];
                }
            } else {
                foreach ($searchFields as $searchField) {
                    $searchFieldConditions["{$searchField} LIKE"] = "%{$term}%";
                }

                $conditions[$conjunction][$key] = [
                    'OR' => $searchFieldConditions
                ];
            }
        }

        return $conditions;
    }

    /**
     * Make sure options arrays are flattened and consolidated before they are used for value
     * checking. This applies to optgroup-style arrays currently
     *
     * @param array $options Options Config
     * @return array
     */
    protected function _flattenValueOptions(array $options)
    {
        $flatOptions = $options;
        if (is_array(current($options))) {
            $flatOptions = [];
            foreach ($options as $group => $valueGroup) {
                $flatOptions = $flatOptions + $valueGroup;
            }
        }

        return $flatOptions;
    }

    /**
     * Formats and enriches field configs
     *
     * @return array
     */
    public function getFilters()
    {
        $filters = [];
        if (method_exists($this->_controller, 'getListFilters')) {
            $filters = $this->_controller->getListFilters($this->_controller->request->action);
        } elseif (isset($this->_controller->listFilters) && !empty($this->_controller->listFilters[$this->_controller->request->action])) {
            $filters = $this->_controller->listFilters[$this->_controller->request->action];
        }
        if (empty($filters)) {
            return [];
        }
        foreach ($filters['fields'] as $field => &$fieldConfig) {
            if (isset($fieldConfig['type']) && $fieldConfig['type'] == 'select'
                && !isset($fieldConfig['searchType'])
            ) {
                $fieldConfig['searchType'] = 'select';
                $fieldConfig['inputOptions']['type'] = 'select';
                unset($fieldConfig['type']);
            }
            // backwards compatibility
            if (isset($fieldConfig['type'])) {
                $fieldConfig['inputOptions']['type'] = $fieldConfig['type'];
                unset($fieldConfig['type']);
            }
            if (isset($fieldConfig['label'])) {
                $fieldConfig['inputOptions']['label'] = $fieldConfig['label'];
                unset($fieldConfig['label']);
            }
            if (isset($fieldConfig['searchType'])
                && in_array($fieldConfig['searchType'], ['select', 'multipleselect'])
            ) {
                if (!isset($fieldConfig['inputOptions']['type'])) {
                    $fieldConfig['inputOptions']['type'] = 'select';
                }
                if (!isset($fieldConfig['inputOptions']['empty'])) {
                    $fieldConfig['inputOptions']['empty'] = true;
                }
            }
            $fieldConfig = Hash::merge($this->defaultListFilter, $fieldConfig);
        }

        return $filters;
    }

    /**
     * Converts POST filter data in array format to a URL
     *
     * @param array $postData Array with Postdata
     * @return array
     */
    public function getRedirectUrlFromPostData(array $postData)
    {
        $urlParams = [];
        foreach ($postData['Filter'] as $model => $fields) {
            foreach ($fields as $field => $value) {
                if (is_array($value) && isset($value['year'])) {
                    $value = "{$value['year']}-{$value['month']}-{$value['day']}";
                    if ($value == '--') {
                        continue;
                    }
                }
                if ($value !== 0 && $value !== '0' && empty($value)) {
                    continue;
                }
                $urlParams["Filter-{$model}-{$field}"] = $value;
            }
        }
        $passedArgs = $this->_controller->passedArgs;
        if (!empty($passedArgs)) {
            $urlParams = Hash::merge($passedArgs, $urlParams);
        }
        $params = $this->_controller->request->query;
        if (!empty($params)) {
            $cleanParams = [];
            foreach ($params as $key => $value) {
                if (substr($key, 0, 7) != 'Filter-') {
                    $cleanParams[$key] = $value;
                }
            }
            $urlParams = Hash::merge($cleanParams, $urlParams);
        }

        return $urlParams;
    }

    /**
     * Adds ListFilter-relevant named params to the given url. Used for detail links
     *
     * @param array $url URL to link to
     * @return array
     */
    public function addListFilterParams(array $url)
    {
        foreach ($this->request->query as $key => $value) {
            if (substr($key, 0, 7) == 'Filter-' || in_array($key, ['page', 'sort', 'direction'])) {
                $url[$key] = $value;
            }
        }

        return $url;
    }

    /**
     * Set fields given as keys in FilterList and paginator conditions to given values if not set by URL.
     *
     * To reset a default value (normally the empty option), an option with key 'all' must be selected.
     * To imitate the behaviour of empty, set empty in inputOptions to false and add ['all' => ''] to options
     *
     * Array syntax of $filters has to be:
     * [
     *   '{$Tables}.{$field}' => {$defaultValue}
     * ]
     *
     * @param  array  $filters array of fields and their default values like described above
     * @return bool   false if anything goes wrong, else true
     */
    public function defaultFilters($filters = [])
    {
        if (empty($filters)) {
            return false;
        }
        foreach ($filters as $key => $value) {
            $filterName = 'Filter-' . str_replace('.', '-', $key);
            $explodedFilterName = explode('-', $filterName);
            if (count($explodedFilterName) !== 3) {
                return false;
            }
            if (empty($this->_controller->request->query[$filterName])) {
                // set request POST data so the inputs will be filled
                $this->_controller->request->data[$explodedFilterName[0]][$explodedFilterName[1]][$explodedFilterName[2]] = $value;
                $this->_controller->paginate['conditions'][$key] = $value;
            } elseif ($this->_controller->request->query[$filterName] == 'all') {
                unset($this->_controller->request->data[$explodedFilterName[0]][$explodedFilterName[1]][$explodedFilterName[2]]);
                unset($this->_controller->paginate['conditions'][$key]);
            }
        }

        return true;
    }
}


<?php
namespace Teto\SQL;

/**
 * Safe query builder
 *
 * @copyright 2016 pixiv Inc.
 * @license   https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0
 */
class Query
{
    const INT64_MAX =  '9223372036854775807';
    const INT64_MIN = '-9223372036854775808';

    const RE_HOLDER = '(?<holder>(?<key>:[a-zA-Z0-9_]+)(?<type>(?:@[a-zA-Z_\[\]]+)?))';

    /**
     * Build SQL query and execute
     *
     * @param \PDO|PDOInterface|PDOAggregate $pdo
     * @param string $sql
     * @param array  $params
     */
    public static function execute($pdo, $sql, array $params)
    {
        $stmt = Query::build(($pdo instanceof PDOAggregate) ? $pdo->getPDO() : $pdo, $sql, $params);
        $stmt->execute();

        return $stmt;
    }

    /**
     * Build SQL query and execute
     *
     * @param \PDO|PDOInterface|PDOAggregate $pdo
     * @param string $sql
     * @param array  $params
     */
    public static function executeAndReturnInsertId($pdo, $sql, array $params, $name = null)
    {
        $stmt = Query::build(($pdo instanceof PDOAggregate) ? $pdo->getPDO() : $pdo, $sql, $params);
        $stmt->execute();

        return $pdo->lastInsertId($name);
    }

    /**
     * Build SQL query
     *
     * @param \PDO|PDOInterface $pdo
     * @param string $sql
     * @param array  $params
     */
    public static function build($pdo, $sql, array $params)
    {
        $bind_values = [];
        $sql = strtr($sql, "\n", ' ');
        $sql = preg_replace_callback(
            '/'.Query::RE_HOLDER.'/',
            function ($m) use ($pdo, $params, &$bind_values) {
                $key  = $m['key'];
                $type = $m['type'];

                if (!isset($params[$key])) {
                    throw new \OutOfRangeException(sprintf('param "%s" expected but not assigned', $key));
                }

                return self::replaceHolder($pdo, $key, $type, $params[$key], $bind_values);
            },
            $sql
        );

        $stmt = $pdo->prepare($sql);

        foreach ($bind_values as $key => list($type, $value)) {
            $stmt->bindParam($key, $value, $type);
        }

        return $stmt;
    }

    protected static function replaceHolder($pdo, $key, $type, $value, &$bind_values)
    {
        if ($type === '@ascdesc') {
            if (!in_array($value, ['ASC', 'DESC', 'asc', 'desc'], true)) {
                throw new \DomainException(sprintf('param "%s" must be "ASC", "DESC", "asc" or "desc"', $key));
            }

            return $value;
        }

        if ($type === '@int') {
            if (!is_numeric($value)) {
                throw new \DomainException(sprintf('param "%s" must be numeric', $key));
            }

            $s = (string)$value;
            if ($s < self::INT64_MIN || self::INT64_MAX < $s) {
                throw new \DomainException(sprintf('param "%s" is integer out of range.', $key));
            }

            if (!preg_match('/\A-?[1-9][0-9]*\z/', $s)) {
                throw new \DomainException();
            }

            return (int)$s;
        }

        if ($type === '@int[]') {
            if (!is_array($value)) {
                throw new \DomainException(sprintf('param "%s" must be int array', $key));
            }
            if (count($value) === 0) {
                throw new \DomainException(sprintf('param "%s" must be not empty int array', $key));
            }

            foreach ($value as $i => $item) {
                $s = (string)$item;
                if ($s < self::INT64_MIN || self::INT64_MAX < $s) {
                    throw new \DomainException(sprintf('param "%s[%i]" is integer out of range.', $key, $i));
                }
            }

            $valuesString = implode(',', $value);
            if (strpos(',' . $valuesString . ',', ',,') !== false) {
                throw new \LogicException('Validation Error.');
            }

            if (!preg_match('/\A(?:-?[1-9][0-9]*)(?:,-?[1-9][0-9]*)*\z/', $valuesString)) {
                throw new \DomainException(sprintf('param "%s[%]"'));
            }

            return $valuesString;
        }

        if ($type === '@string') {
            if (!is_string($value) && !is_numeric($value)) {
                throw new \DomainException(sprintf('param "%s" must be string or numeric', $key));
            }

            return $pdo->quote((string)$value, \PDO::PARAM_STR);
        }

        if ($type === '@string[]') {
            if (!is_array($value)) {
                throw new \DomainException(sprintf('param "%s" must be string array', $key));
            }
            if (count($value) == 0) {
                throw new \DomainException(sprintf('param "%s" must be not empty string array', $key));
            }
            foreach ($value as $i => $item) {
                if (!is_string($item) && !is_numeric($item)) {
                    throw new \DomainException(sprintf('element of param "%s" must be string or numeric', $key));
                }
                $value[$i] = $pdo->quote((string)$item, \PDO::PARAM_STR);
            }
            return implode(',', $value);
        }

        if ($type === '@lob') {
            if (!is_resource($value)) {
                throw new \DomainException(sprintf('param "%s" must be resource', $key));
            }
            $bind_values[$key] = [\PDO::PARAM_LOB, $value];

            return $key;
        }

        if ($type === '') {
            throw new \DomainException(sprintf('type specifier for param "%s" not found', $key));
        } else {
            throw new \DomainException(sprintf('unexpected type "%s"', $type));
        }
    }
}


<?php

namespace Lahaxearnaud\LaravelPushbullet;

use Illuminate\Support\ServiceProvider;
use \Config as Config;
use Lahaxearnaud\LaravelPushbullet\Services\LaravelPushbullet;

/**
 * Class LaravelPushbulletServiceProvider
 * @package Lahaxearnaud\LaravelPushbullet
 */
class LaravelPushbulletServiceProvider extends ServiceProvider
{

    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('pushbullet', function () {
            $apiKey = $this->app['config']->get('services.pushbullet.apiKey', null);

            if (!$apiKey) {
                $apiKey = $this->app['config']->get('pushbullet.apiKey', null);
            }

            return new LaravelPushbullet($apiKey);
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return ['pushbullet'];
    }

}


<?php declare(strict_types=1);

namespace Improved\IteratorPipeline\Traits;

use Improved as i;
use Improved\Iterator\CombineIterator;

/**
 * Mapping and projection methods for iterator pipeline
 */
trait MappingTrait
{
    /**
     * Define the next step via a callback that returns an array or Traversable object.
     *
     * @param callable $callback
     * @param mixed    ...$args
     * @return static
     */
    abstract public function then(callable $callback, ...$args);

    /**
     * Map each element to a value using a callback function.
     *
     * @param callable $callback
     * @return static
     */
    public function map(callable $callback)
    {
        return $this->then(i\iterable_map, $callback);
    }

    /**
     * Map the key of each element to a new key using a callback function.
     *
     * @param callable $callback
     * @return static
     */
    public function mapKeys(callable $callback)
    {
        return $this->then(i\iterable_map_keys, $callback);
    }

    /**
     * Apply a callback to each element of an iterator.
     * Any value returned by the callback is ignored.
     *
     * @param callable $callback
     * @return static
     */
    public function apply(callable $callback)
    {
        return $this->then(i\iterable_apply, $callback);
    }

    /**
     * Divide iterable into chunks of specified size.
     *
     * @param int $size
     * @return static
     */
    public function chunk(int $size)
    {
        return $this->then(i\iterable_chunk, $size);
    }

    /**
     * Group elements of an iterator, with the group name as key and an array of elements as value.
     *
     * @param callable $grouping
     * @return static
     */
    public function group(callable $grouping)
    {
        return $this->then(i\iterable_group, $grouping);
    }

    /**
     * Walk through all sub-iterables and concatenate them.
     *
     * @param bool $preserveKeys
     * @return static
     */
    public function flatten(bool $preserveKeys = false)
    {
        return $this->then(i\iterable_flatten, $preserveKeys);
    }

    /**
     * Set all values of the iterable.
     *
     * @param mixed $value
     * @return static
     */
    public function fill($value)
    {
        return $this->then(i\iterable_fill, $value);
    }

    /**
     * Return the values from a single column / property.
     * Create key/value pairs by specifying the key.
     *
     * @param string|int|null $valueColumn  null to keep values
     * @param string|int|null $keyColumn    null to keep keys
     * @return static
     */
    public function column($valueColumn, $keyColumn = null)
    {
        return $this->then(i\iterable_column, $valueColumn, $keyColumn);
    }

    /**
     * Project each element of an iterator to an associated (or numeric) array.
     * Each element should be an array or object.
     *
     * @param array $mapping  [new key => old key, ...]
     * @return static
     */
    public function project(array $mapping)
    {
        return $this->then(i\iterable_project, $mapping);
    }

    /**
     * Reshape each element of an iterator, adding or removing properties or keys.
     *
     * @param array $columns  [key => bool|string|int, ...]
     * @return static
     */
    public function reshape(array $columns)
    {
        return $this->then(i\iterable_reshape, $columns);
    }

    /**
     * Keep the values, drop the keys. The keys become an incremental number.
     *
     * @return static
     */
    public function values()
    {
        return $this->then(i\iterable_values);
    }

    /**
     * Use the keys as values. The keys become an incremental number.
     *
     * @return static
     */
    public function keys()
    {
        return $this->then(i\iterable_keys);
    }

    /**
     * Use another iterator as keys and the current iterator as values.
     *
     * @param iterable $keys
     * @return static
     */
    public function setKeys(iterable $keys)
    {
        $combine = function ($values, $keys) {
            return new CombineIterator($keys, $values);
        };

        return $this->then($combine, $keys);
    }

    /**
     * Use values as keys and visa versa.
     *
     * @return static
     */
    public function flip()
    {
        return $this->then(i\iterable_flip);
    }
}


<?php

namespace Maba\Bundle\TwigTemplateModificationBundle\Service;

use Maba\Bundle\TwigTemplateModificationBundle\Entity\TemplateContext;
use Maba\Bundle\TwigTemplateModificationBundle\NodeAttributes;
use Maba\Bundle\TwigTemplateModificationBundle\Twig\Parser;
use SplStack;
use Twig_Lexer as Lexer;
use Twig_LoaderInterface as LoaderInterface;
use Twig_Node as Node;

class TemplateReplacer
{
    protected $loader;
    protected $lexer;
    protected $parser;
    private $nodeReplaceHelper;

    /**
     * @var TwigNodeReplacerInterface[]
     */
    protected $replacers = array();

    public function __construct(
        LoaderInterface $loader,
        Lexer $lexer,
        Parser $parser,
        NodeReplaceHelper $nodeReplaceHelper,
        array $nodeReplacers
    ) {
        $this->loader = $loader;
        $this->lexer = $lexer;
        $this->parser = $parser;
        $this->nodeReplaceHelper = $nodeReplaceHelper;
        $this->replacers = $nodeReplacers;
    }

    /**
     * @param TemplateContext $context
     *
     * @return null|string string if any replacements were done
     * @throws \Twig_Error_Syntax
     */
    public function getReplacedContents(TemplateContext $context)
    {
        $templateName = $context->getTemplateName();
        $source = $this->loader->getSource($templateName);

        $stream = $this->lexer->tokenize(
            $source,
            $templateName
        );

        $rootNode = $this->parser->parse($stream);
        $rootNode->setAttribute(NodeAttributes::ATTRIBUTE_SOURCE, $source);

        $this->setRootNode($rootNode, $rootNode);

        $replacements = new SplStack();
        $this->handleNode($rootNode, $replacements, $context);

        if (count($replacements) === 0) {
            return null;
        }

        return $this->handleReplacements($source, $replacements);
    }

    protected function handleNode(Node $node, SplStack $replacements, TemplateContext $context)
    {
        foreach ($this->replacers as $replacer) {
            $replacedContent = $replacer->replace($node, $context);
            if ($replacedContent !== null) {
                $replacements->push(array('content' => $replacedContent, 'node' => $node));
                return;
            }
        }

        foreach ($node as $childNode) {
            if ($childNode !== null) {
                $this->handleNode($childNode, $replacements, $context);
            }
        }
    }

    protected function handleReplacements($source, SplStack $replacements)
    {
        foreach ($replacements as $data) {
            $replacement = $data['content'];
            /** @var Node $node */
            $node = $data['node'];
            $start = $this->nodeReplaceHelper->getStartPosition($node);
            $length = $this->nodeReplaceHelper->getLength($node);
            $source = substr_replace($source, $replacement, $start, $length);
        }

        return $source;
    }

    protected function setRootNode(Node $parentNode, Node $rootNode)
    {
        /** @var Node $childNode */
        foreach ($parentNode as $childNode) {
            if ($childNode === null) {
                continue;
            }

            $childNode->setAttribute(NodeAttributes::ATTRIBUTE_ROOT_NODE, $rootNode);
            $childNode->setAttribute(NodeAttributes::ATTRIBUTE_PARENT_NODE, $parentNode);
            $this->setRootNode($childNode, $rootNode);
        }
    }
}


<?php declare(strict_types=1);

namespace Improved\IteratorPipeline\Traits;

use Improved as i;

/**
 * Pipeline aggregation methods.
 */
trait AggregationTrait
{
    /**
     * @var iterable
     */
    protected $iterable;

    /**
     * Count elements of an iterable.
     *
     * @return int
     */
    public function count(): int
    {
        return i\iterable_count($this->iterable);
    }

    /**
     * Reduce all elements to a single value using a callback.
     *
     * @param callable  $callback
     * @param mixed     $initial
     * @return mixed
     */
    public function reduce(callable $callback, $initial = null)
    {
        return i\iterable_reduce($this->iterable, $callback, $initial);
    }

    /**
     * Calculate the sum of all numbers.
     * If no elements are present, the result is 0.
     *
     * @return int|float
     */
    public function sum()
    {
        return i\iterable_sum($this->iterable);
    }

    /**
     * Return the arithmetic mean.
     * If no elements are present, the result is NAN.
     *
     * @return float
     */
    public function average(): float
    {
        return i\iterable_average($this->iterable);
    }

    /**
     * Concatenate all elements into a single string.
     *
     * @param string   $glue
     * @return string
     */
    public function concat(string $glue = ''): string
    {
        return i\iterable_concat($this->iterable, $glue);
    }
}


<?php declare(strict_types=1);

namespace Improved\IteratorPipeline\Traits;

use Improved as i;

/**
 * Type handling methods for iterator pipeline.
 */
trait TypeHandlingTrait
{
    /**
     * Define the next step via a callback that returns an array or Traversable object.
     *
     * @param callable $callback
     * @param mixed    ...$args
     * @return static
     */
    abstract public function then(callable $callback, ...$args);

    /**
     * Validate that a value has a specific type.
     * @deprecated
     *
     * @param string|string[]        $type
     * @param string|\Throwable|null $error
     * @return static
     */
    public function expectType($type, $error = null)
    {
        return $this->then(i\iterable_expect_type, $type, $error);
    }

    /**
     * Validate that a value has a specific type.
     *
     * @param string|string[] $type
     * @param \Throwable|null $throwable
     * @return static
     */
    public function typeCheck($type, ?\Throwable $throwable = null)
    {
        return $this->then(i\iterable_type_check, $type, $throwable);
    }

    /**
     * Cast a value to the specific type or throw an error.
     *
     * @param string          $type
     * @param \Throwable|null $throwable
     * @return static
     */
    public function typeCast(string $type, ?\Throwable $throwable = null)
    {
        return $this->then(i\iterable_type_cast, $type, $throwable);
    }
}


<?php

namespace Bugsnag\Silex;

use Bugsnag\Silex\Request\SilexResolver;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;

class Silex2ServiceProvider extends AbstractServiceProvider implements ServiceProviderInterface
{
    /**
     * Registers services on the container.
     *
     * @param \Pimple\Container $app
     *
     * @return void
     */
    public function register(Container $app)
    {
        $app['bugsnag.resolver'] = function () {
            return new SilexResolver();
        };

        $app['bugsnag'] = function () use ($app) {
            return $this->makeClient($app);
        };

        $app['bugsnag.notifier'] = function() use ($app) {
            return function($error) use ($app) {
                $this->autoNotify($app['bugsnag'], $error);
            };
        };

        $app->before(function (Request $request) use ($app) {
            $app['bugsnag']->setFallbackType('HTTP');
            $app['bugsnag.resolver']->set($request);
        }, Application::EARLY_EVENT);
    }
}