<?php

namespace Basho\Riak\Api;

use Basho\Riak\Api;
use Basho\Riak\ApiInterface;
use Basho\Riak\Bucket;
use Basho\Riak\Command;
use Basho\Riak\DataType\Counter;
use Basho\Riak\DataType\Map;
use Basho\Riak\DataType\Set;
use Basho\Riak\DataType\Hll;
use Basho\Riak\Location;
use Basho\Riak\Node;
use Basho\Riak\Object;
use Basho\Riak\Search\Doc;
use Basho\Riak\TimeSeries\Cell;

/**
 * Handles communications between end user app & Riak via Riak HTTP API using cURL
 *
 * @author Christopher Mancini <cmancini at basho d0t com>
 */
class Http extends Api implements ApiInterface
{
    // Header keys
    const VCLOCK_KEY = 'X-Riak-Vclock';
    const CONTENT_TYPE_KEY = 'Content-Type';
    const METADATA_PREFIX = 'X-Riak-Meta-';
    const LAST_MODIFIED_KEY = 'Last-Modified';

    // Content Types
    const CONTENT_TYPE_JSON = 'application/json';
    const CONTENT_TYPE_XML = 'application/xml';

    const TS_API_PREFIX = '/ts/v1';

    /**
     * Request body to be sent
     *
     * @var string
     */
    protected $requestBody = '';

    /**
     * Request url the request is being sent to
     *
     * @var string
     */
    protected $requestURL = '';

    /**
     * Response headers returned from request
     *
     * @var array
     */
    protected $responseHeaders = [];

    /**
     * Response body returned from request
     *
     * @var string
     */
    protected $responseBody = '';

    /**
     * HTTP Status Code from response
     *
     * @var int
     */
    protected $statusCode = 0;

    /**
     * cURL connection handle
     *
     * @var null
     */
    protected $connection = null;

    /**
     * API path
     *
     * @var string
     */
    protected $path = '';

    /**
     * Query string
     *
     * @var string
     */
    protected $query = '';

    private $options = [];

    protected $headers = [];

    /**
     * @return int
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

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

    /**
     * @return array
     */
    public function getResponseHeaders()
    {
        return $this->responseHeaders;
    }

    /**
     * @return string
     */
    public function getRequest()
    {
        return $this->request . $this->requestBody;
    }

    public function closeConnection()
    {
        if ($this->connection) {
            curl_close($this->connection);
            $this->connection = null;
        }
    }

    /**
     * Prepare request to be sent
     *
     * @param Command $command
     * @param Node $node
     *
     * @return $this
     */
    public function prepare(Command $command, Node $node)
    {
        if ($this->connection) {
            $this->resetConnection();
        }

        // call parent prepare method to setup object members
        parent::prepare($command, $node);

        // set the API path to be used
        $this->buildPath();

        // general connection preparation
        $this->prepareConnection();

        // request specific connection preparation
        $this->prepareRequest();

        return $this;
    }

    public function resetConnection()
    {
        $this->command = null;
        $this->options = [];
        $this->path = '';
        $this->query = '';
        $this->requestBody = '';
        $this->requestURL = '';
        $this->responseHeaders = [];
        $this->responseBody = '';

        if (version_compare(PHP_VERSION, '5.5.0') >= 0) {
            curl_reset($this->connection);
        } else {
            curl_close($this->connection);
            $this->connection = null;
        }
    }

    /**
     * Sets the API path for the command
     *
     * @return $this
     */
    protected function buildPath()
    {
        $bucket = null;
        $key = '';

        $bucket = $this->command->getBucket();

        $location = $this->command->getLocation();
        if (!empty($location) && $location instanceof Location) {
            $key = $location->getKey();
        }
        switch (get_class($this->command)) {
            /** @noinspection PhpMissingBreakStatementInspection */
            case 'Basho\Riak\Command\Bucket\Store':
                $this->headers[static::CONTENT_TYPE_KEY] = static::CONTENT_TYPE_JSON;
            case 'Basho\Riak\Command\Bucket\Fetch':
            case 'Basho\Riak\Command\Bucket\Delete':
                $this->path = sprintf('/types/%s/buckets/%s/props', $bucket->getType(), $bucket->getName());
                break;
            /** @noinspection PhpMissingBreakStatementInspection */
            case 'Basho\Riak\Command\Object\Fetch':
                $this->headers['Accept'] = '*/*, multipart/mixed';
            case 'Basho\Riak\Command\Object\Store':
            case 'Basho\Riak\Command\Object\Delete':
                $this->path = sprintf('/types/%s/buckets/%s/keys/%s', $bucket->getType(), $bucket->getName(), $key);
                break;
            case 'Basho\Riak\Command\Object\Keys\Fetch':
                $this->headers[static::CONTENT_TYPE_KEY] = static::CONTENT_TYPE_JSON;
                $this->path = sprintf('/types/%s/buckets/%s/keys', $bucket->getType(), $bucket->getName());
                break;
            case 'Basho\Riak\Command\DataType\Counter\Store':
            case 'Basho\Riak\Command\DataType\GSet\Store':
            case 'Basho\Riak\Command\DataType\Set\Store':
            /** @noinspection PhpMissingBreakStatementInspection */
            case 'Basho\Riak\Command\DataType\Map\Store':
            case 'Basho\Riak\Command\DataType\Hll\Store':
                $this->headers[static::CONTENT_TYPE_KEY] = static::CONTENT_TYPE_JSON;
            case 'Basho\Riak\Command\DataType\Counter\Fetch':
            case 'Basho\Riak\Command\DataType\Set\Fetch':
            case 'Basho\Riak\Command\DataType\Map\Fetch':
            case 'Basho\Riak\Command\DataType\Hll\Fetch':
                $this->path = sprintf('/types/%s/buckets/%s/datatypes/%s', $bucket->getType(), $bucket->getName(), $key);
                break;
            /** @noinspection PhpMissingBreakStatementInspection */
            case 'Basho\Riak\Command\Search\Index\Store':
                $this->headers[static::CONTENT_TYPE_KEY] = static::CONTENT_TYPE_JSON;
            case 'Basho\Riak\Command\Search\Index\Fetch':
            case 'Basho\Riak\Command\Search\Index\Delete':
                $this->path = sprintf('/search/index/%s', $this->command);
                break;
            /** @noinspection PhpMissingBreakStatementInspection */
            case 'Basho\Riak\Command\Search\Schema\Store':
                $this->headers[static::CONTENT_TYPE_KEY] = static::CONTENT_TYPE_XML;
            case 'Basho\Riak\Command\Search\Schema\Fetch':
                $this->path = sprintf('/search/schema/%s', $this->command);
                break;
            case 'Basho\Riak\Command\Search\Fetch':
                $this->path = sprintf('/search/query/%s', $this->command);
                break;
            case 'Basho\Riak\Command\MapReduce\Fetch':
                $this->headers[static::CONTENT_TYPE_KEY] = static::CONTENT_TYPE_JSON;
                $this->path = sprintf('/%s', $this->config['mapred_prefix']);
                break;
            case 'Basho\Riak\Command\Indexes\Query':
                $this->path = $this->createIndexQueryPath($bucket);
                break;
            case 'Basho\Riak\Command\Ping':
                $this->path = '/ping';
                break;
            case 'Basho\Riak\Command\Stats':
                $this->path = '/stats';
                break;
            case 'Basho\Riak\Command\Object\FetchPreflist':
                $this->path = sprintf('/types/%s/buckets/%s/keys/%s/preflist', $bucket->getType(), $bucket->getName(), $key);
                break;
            case 'Basho\Riak\Command\TimeSeries\Fetch':
            case 'Basho\Riak\Command\TimeSeries\Delete':
                /** @var $command Command\TimeSeries\Fetch */
                $command = $this->command;
                $this->path = sprintf('%s/tables/%s/keys/%s', static::TS_API_PREFIX, $command->getTable(), implode("/", $command->getData()));
                break;
            case 'Basho\Riak\Command\TimeSeries\Store':
                /** @var $command Command\TimeSeries\Store */
                $command = $this->command;
                $this->path = sprintf('%s/tables/%s/keys', static::TS_API_PREFIX, $command->getTable());
                break;
            case 'Basho\Riak\Command\TimeSeries\Query\Fetch':
                $this->path = sprintf('%s/query', static::TS_API_PREFIX);
                break;
            default:
                $this->path = '';
        }

        return $this;
    }

    /**
     * Generates the URL path for a 2i Query
     *
     * @param Bucket $bucket
     * @return string
     * @throws Api\Exception if 2i query is invalid.
     */
    private function createIndexQueryPath(Bucket $bucket)
    {
        /**  @var Command\Indexes\Query $command */
        $command = $this->command;

        if($command->isMatchQuery()) {
            $path =  sprintf('/types/%s/buckets/%s/index/%s/%s', $bucket->getType(),
                        $bucket->getName(),
                        $command->getIndexName(),
                        $command->getMatchValue());
        }
        elseif($command->isRangeQuery()) {
            $path =  sprintf('/types/%s/buckets/%s/index/%s/%s/%s', $bucket->getType(),
                        $bucket->getName(),
                        $command->getIndexName(),
                        $command->getLowerBound(),
                        $command->getUpperBound());
        }
        else
        {
            throw new Api\Exception("Invalid Secondary Index Query.");
        }

        return $path;
    }

    /**
     * Prepare Connection
     *
     * Sets general connection options that are used with every request
     *
     * @return $this
     * @throws Api\Exception
     */
    protected function prepareConnection()
    {
        // record outgoing headers
        $this->options[CURLINFO_HEADER_OUT] = 1;

        if ($this->command->getConnectionTimeout()) {
            $this->options[CURLOPT_TIMEOUT] = $this->command->getConnectionTimeout();
        }

        if ($this->node->useTls()) {
            // CA File
            if ($this->node->getCaFile()) {
                $this->options[CURLOPT_CAINFO] = $this->node->getCaFile();
            } elseif ($this->node->getCaDirectory()) {
                $this->options[CURLOPT_CAPATH] = $this->node->getCaDirectory();
            } else {
                throw new Api\Exception('A Certificate Authority file is required for secure connections.');
            }

            // verify CA file
            $this->options[CURLOPT_SSL_VERIFYPEER] = true;

            // verify host common name
            $this->options[CURLOPT_SSL_VERIFYHOST] = 0;

            if ($this->node->getUserName()) {
                $this->options[CURLOPT_USERPWD] = sprintf('%s:%s', $this->node->getUserName(),
                    $this->node->getPassword());
            } elseif ($this->node->getCertificate()) {
                /*
                 * NOT CURRENTLY SUPPORTED ON HTTP
                 *
                $this->options[CURLOPT_SSLCERT] = $this->node->getCertificate();
                $this->options[CURLOPT_SSLCERTTYPE] = 'P12';
                if ($this->node->getCertificatePassword()) {
                    $this->options[CURLOPT_SSLCERTPASSWD] = $this->node->getCertificatePassword();
                }
                if ($this->node->getPrivateKey()) {
                    $this->options[CURLOPT_SSLKEY] = $this->node->getPrivateKey();
                    if ($this->node->getPrivateKeyPassword()) {
                        $this->options[CURLOPT_SSLKEYPASSWD] = $this->node->getPrivateKeyPassword();
                    }
                }
                */
            }
        }

        return $this;
    }

    /**
     * Prepare request
     *
     * Sets connection options that are specific to this request
     *
     * @return $this
     */
    protected function prepareRequest()
    {
        return $this->prepareRequestMethod()
            ->prepareRequestHeaders()
            ->prepareRequestParameters()
            ->prepareRequestUrl()
            ->prepareRequestData();
    }

    /**
     * Prepare request data
     *
     * @return $this
     */
    protected function prepareRequestData()
    {
        // if POST or PUT, add parameters to post data, else add to uri
        if (in_array($this->command->getMethod(), ['POST', 'PUT'])) {
            $this->requestBody = $this->command->getEncodedData();
            $this->options[CURLOPT_POSTFIELDS] = $this->requestBody;
        }

        return $this;
    }

    /**
     * Prepares the complete request URL
     *
     * @return $this
     */
    protected function prepareRequestUrl()
    {
        $protocol = $this->node->useTls() ? 'https' : 'http';
        $this->requestURL = sprintf('%s://%s%s?%s', $protocol, $this->node->getUri(), $this->path, $this->query);

        // set the built request URL on the connection
        $this->options[CURLOPT_URL] = $this->requestURL;

        return $this;
    }

    /**
     * Prepare request parameters
     *
     * @return $this
     */
    protected function prepareRequestParameters()
    {
        if ($this->command->hasParameters()) {
            // build query using RFC 3986 (spaces become %20 instead of '+')
            $this->query = http_build_query($this->command->getParameters(), '', '&', PHP_QUERY_RFC3986);
        }

        return $this;
    }

    /**
     * Prepares the request headers
     *
     * @return $this
     */
    protected function prepareRequestHeaders()
    {
        $curl_headers = [];

        foreach ($this->headers as $key => $value) {
            $curl_headers[] = sprintf('%s: %s', $key, $value);
        }

        // if we have an object, set appropriate object headers
        $object = $this->command->getObject();
        if ($object) {
            if ($object->getVclock()) {
                $curl_headers[] = sprintf('%s: %s', static::VCLOCK_KEY, $object->getVclock());
            }

            if ($object->getContentType()) {
                $charset = '';
                if ($object->getCharset()) {
                    $charset = sprintf('; charset=%s', $object->getCharset());
                }
                $curl_headers[] = sprintf('%s: %s', static::CONTENT_TYPE_KEY, $object->getContentType(), $charset);
            }

            // setup index headers
            $translator = new Api\Http\Translator\SecondaryIndex();
            $indexHeaders = $translator->createHeadersFromIndexes($object->getIndexes());
            foreach ($indexHeaders as $value) {
                $curl_headers[] = sprintf('%s: %s', $value[0], $value[1]);
            }

            // setup metadata headers
            foreach($object->getMetaData() as $key => $value) {
                $curl_headers[] = sprintf('%s%s: %s', static::METADATA_PREFIX, $key, $value);
            }
        }

        // set the request headers on the connection
        $this->options[CURLOPT_HTTPHEADER] = $curl_headers;

        // dump local headers to start fresh
        $this->headers = [];

        return $this;
    }

    /**
     * Prepare the request method
     *
     * @return $this
     */
    protected function prepareRequestMethod()
    {
        switch ($this->command->getMethod()) {
            case "POST":
                $this->options[CURLOPT_POST] = 1;
                break;
            case "PUT":
                $this->options[CURLOPT_CUSTOMREQUEST] = 'PUT';
                break;
            case "DELETE":
                $this->options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
                break;
            case "HEAD":
                $this->options[CURLOPT_NOBODY] = 1;
                break;
            default:
                // reset http method to get in case its changed
                $this->options[CURLOPT_HTTPGET] = 1;
        }

        return $this;
    }

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

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

    /**
     * @return bool
     */
    public function send()
    {
        // set the response header and body callback functions
        $this->options[CURLOPT_HEADERFUNCTION] = [$this, 'responseHeaderCallback'];
        $this->options[CURLOPT_WRITEFUNCTION] = [$this, 'responseBodyCallback'];
        if ($this->command->isVerbose()) {
            // set curls output to be the output buffer stream
            $this->options[CURLOPT_STDERR] = fopen('php://stdout', 'w+');
            $this->options[CURLOPT_VERBOSE] = 1;

            // there is a bug when verbose is enabled, header out causes no output
            // @see https://bugs.php.net/bug.php?id=65348
            unset($this->options[CURLINFO_HEADER_OUT]);

            echo "cURL Command:\n\tcurl -X{$this->command->getMethod()} {$this->requestURL} --data \"{$this->requestBody}\"\n";
        }

        // set all options on the resource
        curl_setopt_array($this->getConnection(), $this->options);

        // execute the request
        $this->success = curl_exec($this->getConnection());
        if ($this->success === false) {
            $this->error = curl_error($this->getConnection());
        } elseif ($this->success === true) {
            $this->error = '';
        }

        $this->request = curl_getinfo($this->getConnection(), CURLINFO_HEADER_OUT);

        // set the response http code
        $this->statusCode = curl_getinfo($this->getConnection(), CURLINFO_HTTP_CODE);

        $this->parseResponse();

        return $this->success;
    }

    /**
     * Add a custom header to the request
     *
     * @param $key
     * @param $value
     */
    public function addHeader($key, $value)
    {
        $this->headers[$key] = $value;
    }

    /**
     * @return resource
     */
    public function getConnection()
    {
        if (!$this->connection) {
            $this->openConnection();
        }

        return $this->connection;
    }

    public function openConnection()
    {
        $this->connection = curl_init();

        return $this;
    }

    /**
     * Response header callback
     *
     * Handles callback from curl when the response is received, it parses the headers into an array sets them as
     * member of the class.
     *
     * Has to be public for curl to be able to access it.
     *
     * @param $ch
     * @param $header
     *
     * @return int
     */
    public function responseHeaderCallback($ch, $header)
    {
        if (strpos($header, ':')) {
            list ($key, $value) = explode(':', $header, 2);

            $value = trim($value);

            if (!empty($value)) {
                if (!isset($this->responseHeaders[$key])) {
                    $this->responseHeaders[$key] = $value;
                } elseif (is_array($this->responseHeaders[$key])) {
                    $this->responseHeaders[$key] = array_merge($this->responseHeaders[$key], [$value]);
                } else {
                    $this->responseHeaders[$key] = array_merge([$this->responseHeaders[$key]], [$value]);
                }
            }
        }

        return strlen($header);
    }

    /**
     * Response body callback
     *
     * Handles callback from curl when the response is received, it sets the response body as a member of the class.
     *
     * Has to be public for curl to be able to access it.
     *
     * @param $ch
     * @param $body
     *
     * @return int
     */
    public function responseBodyCallback($ch, $body)
    {
        $this->responseBody .= $body;
        return strlen($body);
    }

    protected function parseResponse()
    {
        // trim leading / trailing whitespace
        $body = $this->responseBody;
        $location = null;
        if ($this->getResponseHeader('Location')) {
            $location = Location::fromString($this->getResponseHeader('Location'));
        }

        if ($this->statusCode == 500) {
            $this->success = false;
            $this->error = $body;
        }

        switch (get_class($this->command)) {
            case 'Basho\Riak\Command\Bucket\Store':
            case 'Basho\Riak\Command\Bucket\Fetch':
                $bucket = null;
                $modified = $this->getResponseHeader(static::LAST_MODIFIED_KEY, '');
                $properties = json_decode($body, true);
                if (isset($properties['props']) && $this->command->getBucket()) {
                    $bucket = new Bucket($this->command->getBucket()->getName(), $this->command->getBucket()->getType(), $properties['props']);
                }
                $response = new Command\Bucket\Response($this->success, $this->statusCode, $this->error, $bucket, $modified);
                break;

            case 'Basho\Riak\Command\Object\Fetch':
            case 'Basho\Riak\Command\Object\Store':
                /** @var Command\Object $command */
                $command = $this->command;
                $objects = (new Api\Http\Translator\ObjectResponse($command, $this->statusCode))
                    ->parseResponse($body, $this->responseHeaders);
                $response = new Command\Object\Response($this->success, $this->statusCode, $this->error, $location, $objects);
                break;

            case 'Basho\Riak\Command\Object\FetchPreflist':
                $response = new Command\Object\Response($this->success, $this->statusCode, $this->error, $location, [new Object(json_decode($body))]);
                break;

            case 'Basho\Riak\Command\Object\Keys\Fetch':
                $data = json_decode($body);
                $keys = [];
                foreach ($data->keys as $key) {
                    $keys[] = new Location($key, $this->command->getBucket());
                }
                $response = new Command\Object\Keys\Response($this->success, $this->statusCode, $this->error, $keys);
                break;

            case 'Basho\Riak\Command\DataType\Counter\Store':
            case 'Basho\Riak\Command\DataType\Counter\Fetch':
                $counter = null;
                $json_object = json_decode($body);
                if ($json_object && isset($json_object->value)) {
                    $counter = new Counter($json_object->value);
                }
                $response = new Command\DataType\Counter\Response(
                    $this->success, $this->statusCode, $this->error, $location, $counter, $this->getResponseHeader('Date')
                );
                break;

            case 'Basho\Riak\Command\DataType\GSet\Store':
            case 'Basho\Riak\Command\DataType\Set\Store':
            case 'Basho\Riak\Command\DataType\Set\Fetch':
                $set = null;
                $json_object = json_decode($body);
                if ($json_object && isset($json_object->value)) {
                    $context = '';
                    if (isset($json_object->context)) {
                        $context = $json_object->context;
                    }
                    $set = new Set($json_object->value, $context);
                }
                $response = new Command\DataType\Set\Response(
                    $this->success, $this->statusCode, $this->error, $location, $set, $this->getResponseHeader('Date')
                );
                break;

            case 'Basho\Riak\Command\DataType\Map\Store':
            case 'Basho\Riak\Command\DataType\Map\Fetch':
                $map = null;
                $json_object = json_decode($body, true);
                if ($json_object && isset($json_object['value'])) {
                    $map = new Map($json_object['value'], $json_object['context']);
                }
                $response = new Command\DataType\Map\Response(
                    $this->success, $this->statusCode, $this->error, $location, $map, $this->getResponseHeader('Date')
                );
                break;

            case 'Basho\Riak\Command\DataType\Hll\Store':
            case 'Basho\Riak\Command\DataType\Hll\Fetch':
                $hll = null;
                $json_object = json_decode($body);
                if ($json_object && isset($json_object->value)) {
                    $hll = new Hll($json_object->value);
                }
                $response = new Command\DataType\Hll\Response(
                    $this->success, $this->statusCode, $this->error, $location, $hll, $this->getResponseHeader('Date')
                );
                break;

            case 'Basho\Riak\Command\Search\Fetch':
                $results = in_array($this->statusCode, [200,204]) ? json_decode($body) : null;
                $docs = [];
                if (!empty($results->response->docs)) {
                    foreach ($results->response->docs as $doc) {
                        $docs[] = new Doc($doc);
                    }
                }
                $numFound = !empty($results->response->numFound) ? $results->response->numFound : 0;

                $response = new Command\Search\Response($this->success, $this->statusCode, $this->error, $numFound, $docs);
                break;
            case 'Basho\Riak\Command\Search\Index\Store':
            case 'Basho\Riak\Command\Search\Index\Fetch':
                $index = json_decode($body);
                $response = new Command\Search\Index\Response($this->success, $this->statusCode, $this->error, $index);
                break;

            case 'Basho\Riak\Command\Search\Schema\Store':
            case 'Basho\Riak\Command\Search\Schema\Fetch':
                $response = new Command\Search\Schema\Response(
                    $this->success, $this->statusCode, $this->error, $body, $this->getResponseHeader(static::CONTENT_TYPE_KEY)
                );
                break;

            case 'Basho\Riak\Command\MapReduce\Fetch':
                $results = in_array($this->statusCode, [200,204]) ? json_decode($body) : null;
                $response = new Command\MapReduce\Response($this->success, $this->statusCode, $this->error, $results);
                break;
            case 'Basho\Riak\Command\Indexes\Query':
                $json_object = in_array($this->statusCode, [200,204]) ? json_decode($body, true) : null;
                $results = [];
                $termsReturned = false;
                $continuation = null;
                $done = true;

                if (isset($json_object['keys'])) {
                    $results = $json_object['keys'];
                }

                if (isset($json_object['results'])) {
                    $results = $json_object['results'];
                    $termsReturned = true;
                }

                if (isset($json_object['continuation'])) {
                    $continuation = $json_object['continuation'];
                    $done = false;
                }

                $response = new Command\Indexes\Response(
                    $this->success, $this->statusCode, $this->error, $results, $termsReturned, $continuation, $done, $this->getResponseHeader('Date')
                );
                break;
            case 'Basho\Riak\Command\Stats':
                $response = new Command\Stats\Response($this->success, $this->statusCode, $this->error, json_decode($body, true));
                break;
            case 'Basho\Riak\Command\TimeSeries\Fetch':
                $row = in_array($this->statusCode, ['200','201','204']) ? json_decode($body, true) : [];
                $response = new Command\TimeSeries\Response($this->success, $this->statusCode, $this->error, [$row]);
                break;
            case 'Basho\Riak\Command\TimeSeries\Query\Fetch':
                $results = in_array($this->statusCode, ['200','204']) ? json_decode($body) : [];
                $rows = [];
                if (isset($results->rows)) {
                    foreach ($results->rows as $row) {
                        $cells = [];
                        foreach ($results->columns as $index => $column) {
                            $cells[$column] = $row[$index];
                        }
                        $rows[] = $cells;
                    }
                }
                $response = new Command\TimeSeries\Query\Response($this->success, $this->statusCode, $this->error, $rows);
                break;
            case 'Basho\Riak\Command\TimeSeries\Store':
            case 'Basho\Riak\Command\TimeSeries\Delete':
            case 'Basho\Riak\Command\Object\Delete':
            case 'Basho\Riak\Command\Bucket\Delete':
            case 'Basho\Riak\Command\Search\Index\Delete':
            case 'Basho\Riak\Command\Ping':
            default:
                $response = new Command\Response($this->success, $this->statusCode, $this->error);
                break;
        }

        $this->response = $response;
    }

    protected function getResponseHeader($key, $default = '')
    {
        if (!empty($this->responseHeaders[$key])) {
            return $this->responseHeaders[$key];
        } else {
            return $default;
        }
    }
}


<?php

namespace Basho\Riak;

/**
 * Trait HeadersTrait
 *
 * Offers code reuse between kv objects & crdts since they share several common needs
 *
 * @author Christopher Mancini <cmancini at basho d0t com>
 */
trait HeadersTrait
{
    /**
     * Request / response headers for the object
     *
     * Content type, last modified, etc
     *
     * @var array
     */
    protected $headers = [];

    /**
     * @return array
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * Retrieve the value for a header, null if not set
     *
     * @param $key
     *
     * @return string|null
     */
    protected function getHeader($key)
    {
        return isset($this->headers[$key]) ? $this->headers[$key] : NULL;
    }
}


<?php

namespace Basho\Riak;

use Basho\Riak\Node\Config;
use Basho\Riak\Node\Response;

/**
 * Contains the connection configuration to connect to a Riak node.
 *
 * @author Christopher Mancini <cmancini at basho d0t com>
 */
class Node
{
    /**
     * Configuration
     *
     * Contains configuration needed to connect to a Riak node.
     *
     * @var Config|null
     */
    protected $config = null;

    /**
     * Inactive node
     *
     * This is only set to true if the node has been marked as unreachable.
     *
     * @var bool
     */
    protected $inactive = false;

    /**
     * Node signature
     *
     * This property is used to store a stateless unique identifier for this node.
     *
     * @var string
     */
    protected $signature = '';

    public function __construct(Config $config)
    {
        $this->config = $config;
        $this->setSignature();
    }

    /**
     * This should NEVER be invoked outside of this object.
     */
    private function setSignature()
    {
        $this->signature = md5(json_encode($this->config));
    }

    /**
     * @return boolean
     */
    public function isInactive()
    {
        return $this->inactive;
    }

    /**
     * @param boolean $inactive
     */
    public function setInactive($inactive)
    {
        $this->inactive = $inactive;
    }

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

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

    /**
     * @return Config|null
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * @return int
     */
    public function getPort()
    {
        return $this->config->getPort();
    }

    /**
     * Returns host:port for Node
     *
     * @return string
     */
    public function getUri()
    {
        return sprintf('%s:%s', $this->config->getHost(), $this->config->getPort());
    }

    /**
     * useTls
     *
     * @return bool
     */
    public function useTls()
    {
        return $this->config->isAuth();
    }

    /**
     * getUserName
     *
     * @return string
     */
    public function getUserName()
    {
        return $this->config->getUser();
    }

    /**
     * getPassword
     *
     * @return string
     */
    public function getPassword()
    {
        return $this->config->getPass();
    }

    public function getCaFile()
    {
        return $this->config->getCaFile();
    }

    public function getCaDirectory()
    {
        return $this->config->getCaDirectory();
    }

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

    public function getCertificatePassword()
    {
        return $this->config->getCertificatePassword();
    }

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

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

    /**
     * @return int
     */
    public function getTimeout()
    {
        return $this->config->getConnectionTimeout();
    }

    /**
     * @return int
     */
    public function getStreamTimeout()
    {
        // NB: value represents seconds
        return $this->config->getStreamTimeout();
    }

    /**
     * @param Command $command
     * @param Api $api
     *
     * @return Command\Response
     * @throws Exception
     */
    public function execute(Command $command, Api $api)
    {
        $success = $api->prepare($command, $this)->send();
        if ($success === FALSE) {
            return false;
        }

        return $api->getResponse();
    }
}


<?php
declare(strict_types=1);

namespace AcMailer\Model;

use AcMailer\Exception\InvalidArgumentException;
use DirectoryIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Zend\Mime\Message;
use Zend\Mime\Part;
use Zend\Stdlib\AbstractOptions;
use function is_array;
use function is_dir;
use function is_resource;
use function is_string;

final class Email extends AbstractOptions
{
    public const DEFAULT_CHARSET = 'utf-8';

    /** @var string */
    private $from = '';
    /** @var string */
    private $fromName = '';
    /** @var string */
    private $replyTo = '';
    /** @var string */
    private $replyToName = '';
    /** @var array */
    private $to = [];
    /** @var array */
    private $cc = [];
    /** @var array */
    private $bcc = [];
    /** @var string */
    private $encoding = '';
    /** @var string */
    private $subject = '';
    /** @var string|Part|Message */
    private $body = '';
    /** @var string|null */
    private $template;
    /** @var array */
    private $templateParams = [];
    /** @var array */
    private $attachments = [];
    /** @var array */
    private $attachmentsDir = [];
    /** @var string */
    private $charset = self::DEFAULT_CHARSET;
    /** @var array */
    private $customHeaders = [];

    public function __construct($options = null)
    {
        $this->__strictMode__ = false;
        parent::__construct($options);
    }

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

    /**
     * @param string $from
     * @return $this|self
     */
    public function setFrom(string $from): self
    {
        $this->from = $from;
        return $this;
    }

    /**
     * @return string
     */
    public function getFromName(): string
    {
        return $this->fromName;
    }

    /**
     * @param string $fromName
     * @return $this|self
     */
    public function setFromName(string $fromName): self
    {
        $this->fromName = $fromName;
        return $this;
    }

    /**
     * @return string
     */
    public function getReplyTo(): string
    {
        return $this->replyTo;
    }

    /**
     * @param string $replyTo
     * @return $this|self
     */
    public function setReplyTo(string $replyTo): self
    {
        $this->replyTo = $replyTo;
        return $this;
    }

    /**
     * @return string
     */
    public function getReplyToName(): string
    {
        return $this->replyToName;
    }

    /**
     * @param string $replyToName
     * @return $this|self
     */
    public function setReplyToName(string $replyToName): self
    {
        $this->replyToName = $replyToName;
        return $this;
    }

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

    /**
     * @param array $to
     * @return $this|self
     */
    public function setTo(array $to): self
    {
        $this->to = $to;
        return $this;
    }

    public function addTo(string $to): self
    {
        $this->to[] = $to;
        return $this;
    }

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

    /**
     * @param array $cc
     * @return $this|self
     */
    public function setCc(array $cc): self
    {
        $this->cc = $cc;
        return $this;
    }

    public function addCc(string $cc): self
    {
        $this->cc[] = $cc;
        return $this;
    }

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

    /**
     * @param array $bcc
     * @return $this|self
     */
    public function setBcc(array $bcc): self
    {
        $this->bcc = $bcc;
        return $this;
    }

    public function addBcc(string $bcc): self
    {
        $this->bcc[] = $bcc;
        return $this;
    }

    /**
     * @return string
     */
    public function getEncoding(): string
    {
        return $this->encoding;
    }

    /**
     * @param string $encoding
     * @return $this|self
     */
    public function setEncoding(string $encoding): self
    {
        $this->encoding = $encoding;
        return $this;
    }

    /**
     * @return string
     */
    public function getSubject(): string
    {
        return $this->subject;
    }

    /**
     * @param string $subject
     * @return $this|self
     */
    public function setSubject(string $subject): self
    {
        $this->subject = $subject;
        return $this;
    }

    /**
     * @return string|Part|Message
     */
    public function getBody()
    {
        return $this->body;
    }

    /**
     * @param string|Part|Message $body
     * @return $this|self
     * @throws InvalidArgumentException
     */
    public function setBody($body): self
    {
        if (! is_string($body) && ! $body instanceof Part && ! $body instanceof Message) {
            throw InvalidArgumentException::fromValidTypes(['string', Part::class, Message::class], $body, 'body');
        }

        $this->body = $body;
        return $this;
    }

    /**
     * @param string|resource|array|Part|Attachment $file
     * @param string|null $filename
     * @return $this
     * @throws InvalidArgumentException
     */
    public function addAttachment($file, string $filename = null): self
    {
        if (! is_string($file)
            && ! is_array($file)
            && ! is_resource($file)
            && ! $file instanceof Part
            && ! $file instanceof Attachment
        ) {
            throw InvalidArgumentException::fromValidTypes(
                ['string', 'array', 'resource', Part::class, Attachment::class],
                $file,
                'attachment'
            );
        }

        if ($filename !== null) {
            $this->attachments[$filename] = $file;
        } else {
            $this->attachments[] = $file;
        }
        return $this;
    }

    /**
     * @param string[]|resource[]|array[]|Part[]|Attachment[] $files
     * @return $this
     * @throws InvalidArgumentException
     */
    public function addAttachments(array $files): self
    {
        foreach ($files as $key => $file) {
            $this->addAttachment($file, is_string($key) ? $key : null);
        }

        return $this;
    }

    /**
     * @param string[]|resource[]|array[]|Part[]|Attachment[] $files
     * @return $this
     * @throws InvalidArgumentException
     */
    public function setAttachments(array $files): self
    {
        $this->attachments = [];
        $this->addAttachments($files);

        return $this;
    }

    /**
     * Returns the list of attachments
     * @return string[]|resource[]|array[]|Part[]|Attachment[]
     */
    public function getAttachments(): array
    {
        return $this->attachments;
    }

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

    /**
     * @param array $attachmentsDir
     * @return $this|self
     */
    public function setAttachmentsDir(array $attachmentsDir): self
    {
        $this->attachmentsDir = $attachmentsDir;
        return $this;
    }

    /**
     * @return bool
     */
    public function hasAttachments(): bool
    {
        return ! empty($this->attachments) || ! empty($this->attachmentsDir);
    }

    /**
     * Processes the attachments dir and merges the result with the attachments array, then returns the result
     *
     * @return array
     */
    public function getComputedAttachments(): array
    {
        if (! $this->hasAttachments()) {
            return [];
        }
        $attachments = $this->getAttachments();

        // Process the attachments dir if any, and include the files in that folder
        $dir = $this->getAttachmentsDir();
        $path = $dir['path'] ?? null;
        $recursive = $dir['recursive'] ?? false;

        if (is_string($path) && is_dir($path)) {
            $files = $recursive ? new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
                RecursiveIteratorIterator::CHILD_FIRST
            ) : new DirectoryIterator($path);

            /* @var \SplFileInfo $fileInfo */
            foreach ($files as $fileInfo) {
                if ($fileInfo->isDir()) {
                    continue;
                }
                $attachments[] = $fileInfo->getPathname();
            }
        }

        return $attachments;
    }

    /**
     * @return string|null
     */
    public function getTemplate(): ?string
    {
        return $this->template;
    }

    /**
     * @param string|null $template
     * @return $this|self
     */
    public function setTemplate(string $template = null): self
    {
        $this->template = $template;
        return $this;
    }

    /**
     * @return bool
     */
    public function hasTemplate(): bool
    {
        return $this->template !== null;
    }

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

    /**
     * @param array $templateParams
     * @return $this|self
     */
    public function setTemplateParams(array $templateParams): self
    {
        $this->templateParams = $templateParams;
        return $this;
    }

    /**
     * @return string
     */
    public function getCharset(): string
    {
        return $this->charset;
    }

    /**
     * @param string $charset
     * @return $this|self
     */
    public function setCharset(string $charset): self
    {
        $this->charset = $charset;
        return $this;
    }

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

    /**
     * @param array $customHeaders
     * @return $this|self
     */
    public function setCustomHeaders(array $customHeaders): self
    {
        $this->customHeaders = $customHeaders;
        return $this;
    }

    public function addCustomHeader(string $headerName, string $value): self
    {
        $this->customHeaders[$headerName] = $value;
        return $this;
    }

    public function removeCustomHeader(string $headerName): self
    {
        unset($this->customHeaders[$headerName]);
        return $this;
    }
}


<?php
declare(strict_types=1);

namespace AcMailer\Service\Factory;

use AcMailer\Attachment\AttachmentParserManager;
use AcMailer\Event\MailEvent;
use AcMailer\Event\MailListenerInterface;
use AcMailer\Exception;
use AcMailer\Model\EmailBuilder;
use AcMailer\Service\MailService;
use AcMailer\View\ExpressiveMailViewRenderer;
use AcMailer\View\MailViewRendererInterface;
use AcMailer\View\MvcMailViewRenderer;
use Interop\Container\ContainerInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\EventsCapableInterface;
use Zend\EventManager\Exception\InvalidArgumentException;
use Zend\EventManager\LazyListenerAggregate;
use Zend\Expressive\Template\TemplateRendererInterface;
use Zend\Mail\Transport;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
use Zend\Stdlib\ArrayUtils;
use Zend\View\Renderer\RendererInterface;
use function array_key_exists;
use function array_keys;
use function count;
use function explode;
use function get_class;
use function gettype;
use function implode;
use function in_array;
use function is_array;
use function is_object;
use function is_string;
use function is_subclass_of;
use function sprintf;

class MailServiceAbstractFactory implements AbstractFactoryInterface
{
    private const ACMAILER_PART = 'acmailer';
    private const MAIL_SERVICE_PART = 'mailservice';
    private const TRANSPORT_MAP = [
        'sendmail' => Transport\Sendmail::class,
        'smtp' => Transport\Smtp::class,
        'file' => Transport\File::class,
        'in_memory' => Transport\InMemory::class,
        'null' => Transport\InMemory::class,
    ];

    /**
     * Can the factory create an instance for the service?
     *
     * @param  ContainerInterface $container
     * @param  string $requestedName
     * @return bool
     * @throws ContainerExceptionInterface
     */
    public function canCreate(ContainerInterface $container, $requestedName): bool
    {
        $parts = explode('.', $requestedName);
        if (count($parts) !== 3) {
            return false;
        }

        if ($parts[0] !== self::ACMAILER_PART || $parts[1] !== static::MAIL_SERVICE_PART) {
            return false;
        }

        $specificServiceName = $parts[2];
        $config = $container->get('config')['acmailer_options']['mail_services'] ?? [];
        return array_key_exists($specificServiceName, $config);
    }

    /**
     * Create an object
     *
     * @param  ContainerInterface $container
     * @param  string $requestedName
     * @param  null|array $options
     * @return MailService
     * @throws InvalidArgumentException
     * @throws Exception\InvalidArgumentException
     * @throws Exception\ServiceNotCreatedException
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null): MailService
    {
        $specificServiceName = explode('.', $requestedName)[2] ?? null;
        $mailOptions = $container->get('config')['acmailer_options'] ?? [];
        $specificMailServiceOptions = $mailOptions['mail_services'][$specificServiceName] ?? null;

        if ($specificMailServiceOptions === null) {
            throw new Exception\ServiceNotCreatedException(sprintf(
                'Requested MailService with name "%s" could not be found. Make sure you have registered it with name'
                . ' "%s" under the acmailer_options.mail_services config entry',
                $requestedName,
                $specificServiceName
            ));
        }

        // Recursively extend configuration
        $specificMailServiceOptions = $this->buildConfig($mailOptions, $specificMailServiceOptions);

        // Create the service
        $transport = $this->createTransport($container, $specificMailServiceOptions);
        $renderer = $this->createRenderer($container, $specificMailServiceOptions);
        $mailService = new MailService(
            $transport,
            $renderer,
            $container->get(EmailBuilder::class),
            $container->get(AttachmentParserManager::class)
        );

        // Attach mail listeners
        $this->attachMailListeners($mailService, $container, $specificMailServiceOptions);
        return $mailService;
    }

    /**
     * @param array $mailOptions
     * @param array $specificOptions
     * @return array
     * @throws Exception\InvalidArgumentException
     * @throws Exception\ServiceNotCreatedException
     */
    private function buildConfig(array $mailOptions, array $specificOptions): array
    {
        if (! isset($specificOptions['extends'])) {
            return $specificOptions;
        }

        // Recursively extend
        $mailServices = $mailOptions['mail_services'];
        $processedExtends = [];
        do {
            $serviceToExtend = $specificOptions['extends'] ?? null;
            // Unset the extends value to allow recursive inheritance
            unset($specificOptions['extends']);

            // Prevent an infinite loop by self inheritance
            if (in_array($serviceToExtend, $processedExtends, true)) {
                throw new Exception\ServiceNotCreatedException(
                    'It wasn\'t possible to create a mail service due to circular inheritance. Review "extends".'
                );
            }
            $processedExtends[] = $serviceToExtend;

            // Ensure the service from which we have to extend has been configured
            if (! isset($mailServices[$serviceToExtend])) {
                throw new Exception\InvalidArgumentException(sprintf(
                    'Provided service "%s" to extend from is not configured inside acmailer_options.mail_services',
                    $serviceToExtend
                ));
            }

            $specificOptions = ArrayUtils::merge($mailServices[$serviceToExtend], $specificOptions);
        } while (isset($specificOptions['extends']));

        return $specificOptions;
    }

    /**
     * @param ContainerInterface $container
     * @param array $mailOptions
     * @return Transport\TransportInterface
     * @throws Exception\InvalidArgumentException
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createTransport(ContainerInterface $container, array $mailOptions): Transport\TransportInterface
    {
        $transport = $mailOptions['transport'] ?? Transport\Sendmail::class;
        if (! is_string($transport) && ! $transport instanceof Transport\TransportInterface) {
            // The adapter is not valid. Throw an exception
            throw Exception\InvalidArgumentException::fromValidTypes(
                ['string', Transport\TransportInterface::class],
                $transport,
                'transport'
            );
        }

        // A transport instance can be returned as is
        if ($transport instanceof Transport\TransportInterface) {
            return $transport;
        }

        // Check if the adapter is one of Zend's default adapters
        $transport = self::TRANSPORT_MAP[$transport] ?? $transport;
        if (is_subclass_of($transport, Transport\TransportInterface::class)) {
            return $this->setupStandardTransportFromConfig(new $transport(), $mailOptions);
        }

        // Check if the transport is a service
        if ($container->has($transport)) {
            /** @var Transport\TransportInterface $transport */
            $transportInstance = $container->get($transport);
            if ($transportInstance instanceof Transport\TransportInterface) {
                return $transportInstance;
            }

            throw new Exception\InvalidArgumentException(sprintf(
                'Provided transport service with name "%s" does not return a "%s" instance',
                $transport,
                Transport\TransportInterface::class
            ));
        }

        // The adapter is not valid. Throw an exception
        throw new Exception\InvalidArgumentException(sprintf(
            'Registered transport "%s" is not either one of ["%s"], a "%s" subclass or a registered service.',
            $transport,
            implode('", "', array_keys(self::TRANSPORT_MAP)),
            Transport\TransportInterface::class
        ));
    }

    /**
     * @param Transport\TransportInterface $transport
     * @param array $mailOptions
     * @return Transport\TransportInterface
     */
    private function setupStandardTransportFromConfig(
        Transport\TransportInterface $transport,
        array $mailOptions
    ): Transport\TransportInterface {
        if ($transport instanceof Transport\Smtp) {
            $transport->setOptions(new Transport\SmtpOptions($mailOptions['transport_options'] ?? []));
        } elseif ($transport instanceof Transport\File) {
            $transportOptions = $mailOptions['transport_options'] ?? [];
            $transportOptions['path'] = $transportOptions['path'] ?? 'data/mail/output';
            $transport->setOptions(new Transport\FileOptions($transportOptions));
        }

        return $transport;
    }

    /**
     * @param ContainerInterface $container
     * @param array $mailOptions
     * @return MailViewRendererInterface
     * @throws Exception\InvalidArgumentException
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createRenderer(ContainerInterface $container, array $mailOptions): MailViewRendererInterface
    {
        if (! isset($mailOptions['renderer'])) {
            return $container->get(MailViewRendererInterface::class);
        }

        // Resolve renderer service and ensure it has proper type
        $renderer = $container->get($mailOptions['renderer']);

        if ($renderer instanceof MailViewRendererInterface) {
            return $renderer;
        }

        if ($renderer instanceof TemplateRendererInterface) {
            return new ExpressiveMailViewRenderer($renderer);
        }

        if ($renderer instanceof RendererInterface) {
            return new MvcMailViewRenderer($renderer);
        }

        throw new Exception\InvalidArgumentException(sprintf(
            'Defined renderer of type "%s" is not valid. The renderer must resolve to a instance of ["%s"] types',
            is_object($renderer) ? get_class($renderer) : gettype($renderer),
            implode(
                '", "',
                [MailViewRendererInterface::class, TemplateRendererInterface::class, RendererInterface::class]
            )
        ));
    }

    /**
     * Attaches the preconfigured mail listeners to the mail service
     *
     * @param EventsCapableInterface $service
     * @param ContainerInterface $container
     * @param array $mailOptions
     * @throws InvalidArgumentException
     * @throws Exception\InvalidArgumentException
     * @throws NotFoundExceptionInterface
     */
    private function attachMailListeners(
        EventsCapableInterface $service,
        ContainerInterface $container,
        array $mailOptions
    ): void {
        $listeners = (array) ($mailOptions['mail_listeners'] ?? []);
        if (empty($listeners)) {
            return;
        }

        $definitions = [];
        $eventManager = $service->getEventManager();
        foreach ($listeners as $listener) {
            $this->addDefinitions($definitions, $listener, $eventManager);
        }

        // Attach lazy event listeners if any
        if (! empty($definitions)) {
            (new LazyListenerAggregate($definitions, $container))->attach($eventManager);
        }
    }

    /**
     * @param array $definitions
     * @param array|string|MailListenerInterface $listener
     * @param EventManagerInterface $events
     * @throws Exception\InvalidArgumentException
     */
    private function addDefinitions(array &$definitions, $listener, EventManagerInterface $events): void
    {
        $priority = 1;
        if (is_array($listener) && array_key_exists('listener', $listener)) {
            $listener = $listener['listener'];
            $priority = $listener['priority'] ?? 1;
        }

        // If the listener is already an instance, just register it
        if ($listener instanceof MailListenerInterface) {
            $listener->attach($events, $priority);
            return;
        }

        // Ensure the listener is a string
        if (! is_string($listener)) {
            throw Exception\InvalidArgumentException::fromValidTypes(
                ['string', 'array', MailListenerInterface::class],
                $listener,
                'listener'
            );
        }

        foreach (MailEvent::getEventNames() as $method => $eventName) {
            $definitions[] = [
                'listener' => $listener,
                'method' => $method,
                'event' => $eventName,
                'priority' => $priority,
            ];
        }
    }
}


<?php
declare(strict_types=1);

namespace AcMailer\Service;

use AcMailer\Attachment\AttachmentParserManagerInterface;
use AcMailer\Attachment\Parser\AttachmentParserInterface;
use AcMailer\Event\MailEvent;
use AcMailer\Event\MailListenerAwareInterface;
use AcMailer\Event\MailListenerInterface;
use AcMailer\Exception;
use AcMailer\Mail\MessageFactory;
use AcMailer\Model\Attachment;
use AcMailer\Model\Email;
use AcMailer\Model\EmailBuilderInterface;
use AcMailer\Result\MailResult;
use AcMailer\Result\ResultInterface;
use AcMailer\View\MailViewRendererInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Throwable;
use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\EventsCapableInterface;
use Zend\EventManager\SharedEventManager;
use Zend\Mail\Exception\InvalidArgumentException;
use Zend\Mail\Message;
use Zend\Mail\Transport\TransportInterface;
use Zend\Mime;
use function array_key_exists;
use function array_merge;
use function get_class;
use function gettype;
use function is_array;
use function is_object;
use function is_string;
use function sprintf;
use function strip_tags;

class MailService implements MailServiceInterface, EventsCapableInterface, MailListenerAwareInterface
{
    /** @var TransportInterface */
    private $transport;
    /** @var MailViewRendererInterface */
    private $renderer;
    /** @var EventManagerInterface */
    private $events;
    /** @var EmailBuilderInterface */
    private $emailBuilder;
    /** @var AttachmentParserManagerInterface */
    private $attachmentParserManager;

    /**
     * Creates a new MailService
     * @param TransportInterface $transport
     * @param MailViewRendererInterface $renderer
     * @param EmailBuilderInterface $emailBuilder
     * @param AttachmentParserManagerInterface $attachmentParserManager
     * @param EventManagerInterface|null $events
     */
    public function __construct(
        TransportInterface $transport,
        MailViewRendererInterface $renderer,
        EmailBuilderInterface $emailBuilder,
        AttachmentParserManagerInterface $attachmentParserManager,
        EventManagerInterface $events = null
    ) {
        $this->transport = $transport;
        $this->renderer = $renderer;
        $this->emailBuilder = $emailBuilder;
        $this->attachmentParserManager = $attachmentParserManager;
        $this->events = $this->initEventManager($events);
    }

    private function initEventManager(EventManagerInterface $events = null): EventManagerInterface
    {
        $events = $events ?: new EventManager(new SharedEventManager());
        $events->setIdentifiers([
            __CLASS__,
            static::class,
        ]);
        return $events;
    }

    /**
     * Tries to send the message, returning a MailResult object
     * @param string|array|Email $email
     * @param array $options
     * @return ResultInterface
     * @throws NotFoundExceptionInterface
     * @throws ContainerExceptionInterface
     * @throws Exception\InvalidArgumentException
     * @throws Exception\EmailNotFoundException
     * @throws Exception\MailException
     */
    public function send($email, array $options = []): ResultInterface
    {
        // Try to resolve the email to be sent
        if (is_string($email)) {
            $email = $this->emailBuilder->build($email, $options);
        } elseif (is_array($email)) {
            $email = $this->emailBuilder->build(Email::class, $email);
        } elseif (! $email instanceof Email) {
            throw Exception\InvalidArgumentException::fromValidTypes(
                ['string', 'array', Email::class],
                $email,
                'email'
            );
        }

        // Trigger the pre render event and then render the email's body in case it has to be composed from a template
        $this->events->triggerEvent($this->createMailEvent($email, MailEvent::EVENT_MAIL_PRE_RENDER));
        $this->renderEmailBody($email);

        // Trigger pre send event, and cancel email sending if any listener returned false
        $eventResp = $this->events->triggerEvent($this->createMailEvent($email, MailEvent::EVENT_MAIL_PRE_SEND));
        if ($eventResp->contains(false)) {
            return new MailResult($email, false);
        }

        try {
            // Build the message object to send
            $message = MessageFactory::createMessageFromEmail($email)->setBody(
                $this->buildBody($email->getBody(), $email->getCharset())
            );
            $this->attachFiles($message, $email);
            $this->addCustomHeaders($message, $email);

            // Try to send the message
            $this->transport->send($message);

            // Trigger post send event
            $result = new MailResult($email);
            $this->events->triggerEvent($this->createMailEvent($email, MailEvent::EVENT_MAIL_POST_SEND, $result));
            return $result;
        } catch (Throwable $e) {
            // Trigger error event, notifying listeners of the error
            $this->events->triggerEvent($this->createMailEvent($email, MailEvent::EVENT_MAIL_SEND_ERROR, new MailResult(
                $email,
                false,
                $e
            )));

            throw new Exception\MailException('An error occurred while trying to send the email', $e->getCode(), $e);
        }
    }

    /**
     * Creates a new MailEvent object
     * @param Email $email
     * @param string $name
     * @param ResultInterface $result
     * @return MailEvent
     */
    private function createMailEvent(
        Email $email,
        string $name,
        ResultInterface $result = null
    ): MailEvent {
        $event = new MailEvent($email, $name);
        if ($result !== null) {
            $event->setResult($result);
        }

        return $event;
    }

    /**
     * @param Email $email
     * @throws Exception\InvalidArgumentException
     */
    private function renderEmailBody(Email $email)
    {
        if (! $email->hasTemplate()) {
            return;
        }

        $rawBody = $this->renderer->render(
            $email->getTemplate(),
            $this->injectLayoutParam($email->getTemplateParams())
        );
        $email->setBody($rawBody);
    }

    private function injectLayoutParam(array $original): array
    {
        // When using Zend/View in expressive, a layout could have been globally configured.
        // We have to override it unless explicitly provided. It won't affect other renderer implementations.
        if (! array_key_exists('layout', $original)) {
            $original['layout'] = false;
        }

        return $original;
    }

    /**
     * Sets the message body
     * @param string|Mime\Part|Mime\Message $body
     * @param string $charset
     * @return Mime\Message
     * @throws Mime\Exception\InvalidArgumentException
     */
    private function buildBody($body, string $charset): Mime\Message
    {
        if ($body instanceof Mime\Message) {
            return $body;
        }

        // If the body is a string, wrap it into a Mime\Part
        if (is_string($body)) {
            $mimePart = new Mime\Part($body);
            $mimePart->type = $body !== strip_tags($body) ? Mime\Mime::TYPE_HTML : Mime\Mime::TYPE_TEXT;
            $body = $mimePart;
        }

        $body->charset = $charset;
        $message = new Mime\Message();
        $message->setParts([$body]);
        return $message;
    }

    /**
     * Attaches files to the message if any
     * @param Message $message
     * @param Email $email
     * @throws Exception\InvalidAttachmentException
     * @throws Exception\ServiceNotCreatedException
     * @throws NotFoundExceptionInterface
     * @throws ContainerExceptionInterface
     * @throws InvalidArgumentException
     */
    private function attachFiles(Message $message, Email $email)
    {
        if (! $email->hasAttachments()) {
            return;
        }
        $attachments = $email->getComputedAttachments();

        // Get old message parts
        /** @var Mime\Message $mimeMessage */
        $mimeMessage = $message->getBody();
        $oldParts = $mimeMessage->getParts();

        // Generate a new Mime\Part for each attachment
        $attachmentParts = [];
        $info = null;
        foreach ($attachments as $key => $attachment) {
            // If the attachment is an array with "parser_name" and "value" keys, cast it into an Attachment object
            if (is_array($attachment) && isset($attachment['parser_name'], $attachment['value'])) {
                $attachment = Attachment::fromArray($attachment);
            }

            $parserName = $this->resolveParserNameFromAttachment($attachment);
            if (! $this->attachmentParserManager->has($parserName)) {
                throw new Exception\ServiceNotCreatedException(
                    sprintf('The attachment parser "%s" could not be found', $parserName)
                );
            }

            /** @var AttachmentParserInterface $parser */
            $parser = $this->attachmentParserManager->get($parserName);
            $attachmentValue = $attachment instanceof Attachment ? $attachment->getValue() : $attachment;
            $part = $parser->parse($attachmentValue, is_string($key) ? $key : null);

            $part->charset = $email->getCharset();
            $attachmentParts[] = $part;
        }

        // Create a new body for the message, merging the attachment parts and all the old parts
        $body = new Mime\Message();
        $body->setParts(array_merge($oldParts, $attachmentParts));
        $message->setBody($body);
    }

    /**
     * @param string|resource|array|Mime\Part|Attachment $attachment
     * @return string
     */
    private function resolveParserNameFromAttachment($attachment): string
    {
        if ($attachment instanceof Attachment) {
            return $attachment->getParserName();
        }

        return is_object($attachment) ? get_class($attachment) : gettype($attachment);
    }

    /**
     * Retrieve the event manager
     * Lazy-loads an EventManager instance if none registered.
     * @return EventManagerInterface
     */
    public function getEventManager(): EventManagerInterface
    {
        return $this->events;
    }

    /**
     * Attaches a new MailListenerInterface
     * @param MailListenerInterface $mailListener
     * @param int $priority
     * @return void
     */
    public function attachMailListener(MailListenerInterface $mailListener, $priority = 1)
    {
        $mailListener->attach($this->events, $priority);
    }

    /**
     * Detaches provided MailListener
     * @param MailListenerInterface $mailListener
     * @return void
     */
    public function detachMailListener(MailListenerInterface $mailListener)
    {
        $mailListener->detach($this->events);
    }

    private function addCustomHeaders(Message $message, Email $email): void
    {
        $headers = $message->getHeaders();
        foreach ($email->getCustomHeaders() as $headerName => $value) {
            $headers->addHeaderLine($headerName, $value);
        }
        $message->setHeaders($headers);
    }
}


<?php
declare(strict_types=1);

namespace AcMailer\View;

use Interop\Container\ContainerInterface;
use Interop\Container\ContainerInterface as InteropContainer;
use Interop\Container\Exception\ContainerException;
use Interop\Container\Exception\NotFoundException;
use Psr\Container\ContainerExceptionInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
use Zend\Mvc\Service\ViewHelperManagerFactory;
use Zend\ServiceManager\Config;
use Zend\View\HelperPluginManager;
use Zend\View\Renderer\PhpRenderer;
use Zend\View\Renderer\RendererInterface;
use Zend\View\Resolver\AggregateResolver;
use Zend\View\Resolver\ResolverInterface;
use Zend\View\Resolver\TemplateMapResolver;
use Zend\View\Resolver\TemplatePathStack;
use function array_shift;
use function count;

class MailViewRendererFactory
{
    /** @deprecated Use the MailViewRendererInterface FQCN instead */
    public const SERVICE_NAME = MailViewRendererInterface::class;

    /**
     * @param ContainerInterface $container
     * @return MailViewRendererInterface
     * @throws ContainerExceptionInterface
     * @throws ContainerException
     * @throws NotFoundException
     */
    public function __invoke(ContainerInterface $container): MailViewRendererInterface
    {
        // First, if the TemplateRendererInterface is registered as a service, use that service.
        // This should be true in expressive applications
        if ($container->has(TemplateRendererInterface::class)) {
            return new ExpressiveMailViewRenderer($container->get(TemplateRendererInterface::class));
        }

        // If the mailviewrenderer is registered, wrap it into a ZendViewRenderer
        // This should be true in Zend/MVC apps, run in a HTTP context
        if ($container->has('mailviewrenderer')) {
            return $this->wrapZendView($container->get('mailviewrenderer'));
        }

        // Finally, create a zend/view PhpRenderer and wrap it into a ZendViewRenderer
        // This should be reached only in Zend/MVC apps run in a CLI context
        $vmConfig = $this->getSpecificConfig($container, 'view_manager');
        $renderer = new PhpRenderer();

        // Check what kind of view_manager configuration has been defined
        $resolversStack = [];
        if (isset($vmConfig['template_map'])) {
            // Create a TemplateMapResolver in case only the template_map has been defined
            $resolversStack[] = new TemplateMapResolver($vmConfig['template_map']);
        }
        if (isset($vmConfig['template_path_stack'])) {
            // Create a TemplatePathStack resolver in case only the template_path_stack has been defined
            $pathStackResolver = new TemplatePathStack();
            $pathStackResolver->setPaths($vmConfig['template_path_stack']);
            $resolversStack[] = $pathStackResolver;
        }

        // Create the template resolver for the PhpRenderer
        $resolver = $this->buildTemplateResolverFromStack($resolversStack);
        if ($resolver !== null) {
            $renderer->setResolver($resolver);
        }

        // Create a HelperPluginManager with default view helpers and user defined view helpers
        $renderer->setHelperPluginManager($this->createHelperPluginManager($container));
        return $this->wrapZendView($renderer);
    }

    private function wrapZendView(RendererInterface $renderer): MailViewRendererInterface
    {
        return new MvcMailViewRenderer($renderer);
    }

    /**
     * Creates a view helper manager
     * @param ContainerInterface|InteropContainer $container
     * @return HelperPluginManager
     * @throws ContainerException
     * @throws NotFoundException
     */
    private function createHelperPluginManager(ContainerInterface $container): HelperPluginManager
    {
        $factory = new ViewHelperManagerFactory();
        /** @var HelperPluginManager $helperManager */
        $helperManager = $factory($container, ViewHelperManagerFactory::PLUGIN_MANAGER_CLASS);
        $config = new Config($this->getSpecificConfig($container, 'view_helpers'));
        $config->configureServiceManager($helperManager);
        return $helperManager;
    }

    /**
     * Returns a specific configuration defined by provided key
     * @param ContainerInterface $container
     * @param string $configKey
     * @return array
     * @throws ContainerException
     * @throws NotFoundException
     */
    private function getSpecificConfig(ContainerInterface $container, string $configKey): array
    {
        return $container->get('config')[$configKey] ?? [];
    }

    /**
     * @param array $resolversStack
     * @return ResolverInterface|null
     */
    private function buildTemplateResolverFromStack(array $resolversStack): ?ResolverInterface
    {
        if (count($resolversStack) <= 1) {
            return array_shift($resolversStack);
        }

        // Attach all resolvers to the aggregate, if there's more than one
        $aggregateResolver = new AggregateResolver();
        foreach ($resolversStack as $resolver) {
            $aggregateResolver->attach($resolver);
        }
        return $aggregateResolver;
    }
}


<?php
/*
 * This file is part of JoliCi.
*
* (c) Joel Wurtz <jwurtz@jolicode.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliCi\Log;

use Monolog\Formatter\FormatterInterface;

class SimpleFormatter implements FormatterInterface
{
    private $static = array();

    private $lineNumber = 0;

    /*
     * (non-PHPdoc) @see \Monolog\Formatter\FormatterInterface::format()
     */
    public function format(array $record)
    {
        if (isset($record['context']['clear-static'])) {
            $this->static = array();

            return "";
        }

        $message = $record['message'];

        if (isset($record['context']['static']) && $record['context']['static']) {
            $id      = $record['context']['static-id'];

            if (!isset($this->static[$id])) {
                $this->static[$id] = array(
                    'current_line' => $this->lineNumber,
                    'message'      => $message,
                );

                $message = sprintf("%s\n", $message);
            } else {
                $diff                         = ($this->lineNumber - $this->static[$id]['current_line']);
                $lastMessage                  = $this->static[$id]['message'];
                $this->static[$id]['message'] = $message;

                //Add space to replace old string in message
                if (mb_strlen($lastMessage) > mb_strlen($message)) {
                    $message = str_pad($message, mb_strlen($lastMessage), " ", STR_PAD_RIGHT);
                }

                $message = sprintf("\x0D\x1B[%sA%s\x1B[%sB\x0D", $diff, $message, $diff);
            }
        }

        if (preg_match('#\n#', $message)) {
            $this->lineNumber++;
        }

        return $message;
    }

    /*
     * (non-PHPdoc) @see \Monolog\Formatter\FormatterInterface::formatBatch()
     */
    public function formatBatch(array $records)
    {
        $message = '';
        foreach ($records as $record) {
            $message .= $this->format($record);
        }

        return $message;
    }
}


<?php

namespace Joli\JoliCi;

use Docker\API\Model\BuildInfo;
use Psr\Log\LoggerInterface;

class LoggerCallback
{
    /**
     * @var \Closure
     */
    private $buildCallback;

    /**
     * @var \Closure
     */
    private $runStdoutCallback;

    /**
     * @var \Closure
     */
    private $runStderrCallback;

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $build     = new \ReflectionMethod($this, 'buildCallback');
        $runStdout = new \ReflectionMethod($this, 'runStdoutCallback');
        $runStderr = new \ReflectionMethod($this, 'runStderrCallback');

        $this->buildCallback = $build->getClosure($this);
        $this->runStdoutCallback = $runStdout->getClosure($this);
        $this->runStderrCallback = $runStderr->getClosure($this);
        $this->logger = $logger;
    }

    /**
     * Get the build log callback when building / pulling an image
     *
     * @return callable
     */
    public function getBuildCallback()
    {
        return $this->buildCallback;
    }

    /**
     * Get the run stdout callback for docker
     *
     * @return callable
     */
    public function getRunStdoutCallback()
    {
        return $this->runStdoutCallback;
    }

    /**
     * Get the run stderr callback for docker
     *
     * @return callable
     */
    public function getRunStderrCallback()
    {
        return $this->runStderrCallback;
    }

    /**
     * Clear static log buffer
     */
    public function clearStatic()
    {
        $this->logger->debug("", array('clear-static' => true));
    }

    /**
     * The build callback when creating a image, useful to see what happens during building
     *
     * @param BuildInfo $output An encoded json string from docker daemon
     */
    private function buildCallback(BuildInfo $output)
    {
        $message = "";

        if ($output->getError()) {
            $this->logger->error(sprintf("Error when creating job: %s\n", $output->getError()), array('static' => false, 'static-id' => null));
            return;
        }

        if ($output->getStream()) {
            $message = $output->getStream();
        }

        if ($output->getStatus()) {
            $message = $output->getStatus();

            if ($output->getProgress()) {
                $message .= " " . $output->getProgress();
            }
        }

        // Force new line
        if (!$output->getId() && !preg_match('#\n#', $message)) {
            $message .= "\n";
        }

        $this->logger->debug($message, array(
            'static' => $output->getId() !== null,
            'static-id' => $output->getId(),
        ));
    }

    /**
     * Run callback to catch stdout logs of test running
     *
     * @param string $output Output from run (stdout)
     */
    private function runStdoutCallback($output)
    {
        $this->logger->info($output);
    }

    /**
     * Run callback to catch stderr logs of test running
     *
     * @param string $output Output from run (stderr)
     */
    private function runStderrCallback($output)
    {
        $this->logger->error($output);
    }
}


<?php
/*
 * This file is part of JoliCi.
*
* (c) Joel Wurtz <jwurtz@jolicode.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliCi\BuildStrategy;

use Joli\JoliCi\Job;
use Joli\JoliCi\Filesystem\Filesystem;
use Joli\JoliCi\Naming;
use Symfony\Component\Finder\Finder;

/**
 * JoliCi implementation for build
 *
 * A project must have a .jolici directory, each directory inside this one will be a type of build and must contain a Dockerfile to be executable
 *
 * @author Joel Wurtz <jwurtz@jolicode.com>
 */
class JoliCiBuildStrategy implements BuildStrategyInterface
{
    /**
     * @var string Base path for build
     */
    private $buildPath;

    /**
     * @var Filesystem Filesystem service
     */
    private $filesystem;

    /**
     * @var Naming Use to name the image created
     */
    private $naming;

    /**
     * @param string     $buildPath  Directory where build must be created
     * @param Naming     $naming     Naming service
     * @param Filesystem $filesystem Filesystem service
     */
    public function __construct($buildPath, Naming $naming, Filesystem $filesystem)
    {
        $this->buildPath  = $buildPath;
        $this->naming     = $naming;
        $this->filesystem = $filesystem;
    }

    /**
     * {@inheritdoc}
     */
    public function getJobs($directory)
    {
        $builds = array();
        $finder = new Finder();
        $finder->directories();

        foreach ($finder->in($this->getJoliCiStrategyDirectory($directory)) as $dir) {
            $builds[] = new Job(
                $this->naming->getProjectName($directory),
                $this->getName(),
                $this->naming->getUniqueKey(array('build' => $dir->getFilename())),
                array(
                    'origin' => $directory,
                    'build'  => $dir->getRealPath(),
                ),
                "JoliCi Build: ".$dir->getFilename()
            );
        }

        return $builds;
    }

    /**
     * {@inheritdoc}
     */
    public function prepareJob(Job $job)
    {
        $origin = $job->getParameters()['origin'];
        $target = $this->buildPath.DIRECTORY_SEPARATOR. $job->getDirectory();
        $build  = $job->getParameters()['build'];

        // First mirroring target
        $this->filesystem->mirror($origin, $target, null, array(
            'delete'   => true,
            'override' => true,
        ));

        // Second override target with build dir
        $this->filesystem->mirror($build, $target, null, array(
            'delete'   => false,
            'override' => true,
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return "JoliCi";
    }

    /**
     * {@inheritdoc}
     */
    public function supportProject($directory)
    {
        return file_exists($this->getJoliCiStrategyDirectory($directory)) && is_dir($this->getJoliCiStrategyDirectory($directory));
    }

    /**
     * Return the jolici strategy directory where there must be builds
     *
     * @param  string $projectPath
     * @return string
     */
    protected function getJoliCiStrategyDirectory($projectPath)
    {
        return $projectPath.DIRECTORY_SEPARATOR.'.jolici';
    }
}


<?php

namespace Joli\JoliCi;

use Behat\Transliterator\Transliterator;

class Naming
{
    /**
     * Return a translitared name for a project
     *
     * @param string $projectPath Project directory
     *
     * @return string
     */
    public function getProjectName($projectPath)
    {
        $project = basename(realpath($projectPath));
        $project = Transliterator::transliterate($project, '-');

        return $project;
    }

    /**
     * Generate a unique key for a list of parameters, with same parameters
     * the key must not change (no random stuff) and the order does not matter
     *
     * @param array $parameters
     *
     * @return integer
     */
    public function getUniqueKey($parameters = array())
    {
        // First ordering parameters
        ksort($parameters);

        // Return a hash of the serialzed parameters
        return crc32(serialize($parameters));
    }
}


<?php

namespace Joli\JoliCi;

use Docker\API\Model\ContainerConfig;
use Docker\Container as DockerContainer;
use Docker\Docker;
use Docker\Exception\ImageNotFoundException;
use Docker\Exception\UnexpectedStatusCodeException;
use Docker\Manager\ImageManager;
use Http\Client\Plugin\Exception\ClientErrorException;
use Psr\Log\LoggerInterface;

class ServiceManager
{
    private $docker;

    private $logger;

    public function __construct(Docker $docker, LoggerCallback $logger)
    {
        $this->docker = $docker;
        $this->logger = $logger;
    }

    /**
     * Start services for a Job
     *
     * @param Job $build
     */
    public function start(Job $build)
    {
        foreach ($build->getServices() as $service) {
            try {
                $this->docker->getImageManager()->find(sprintf('%s:%s', $service->getRepository(), $service->getTag()));
            } catch (ClientErrorException $e) {
                if ($e->getResponse()->getStatusCode() == 404) {
                    $buildStream = $this->docker->getImageManager()->create(null, [
                        'fromImage' => sprintf('%s:%s', $service->getRepository(), $service->getTag())
                    ], ImageManager::FETCH_STREAM);

                    $buildStream->onFrame($this->logger->getBuildCallback());
                    $buildStream->wait();
                } else {
                    throw $e;
                }
            }

            $serviceConfig = $service->getConfig();
            $containerConfig = new ContainerConfig();
            $containerConfig->setImage(sprintf('%s:%s', $service->getRepository(), $service->getTag()));
            $containerConfig->setLabels([
                'com.jolici.container=true'
            ]);

            if (isset($serviceConfig['Env'])) {
                $containerConfig->setEnv($serviceConfig['Env']);
            }

            $containerCreateResult = $this->docker->getContainerManager()->create($containerConfig);
            $this->docker->getContainerManager()->start($containerCreateResult->getId());
            $service->setContainer($containerCreateResult->getId());
        }
    }

    /**
     * Stop services for a Job and reinit volumes
     *
     * @param Job $job     The job to stop services
     * @param int $timeout Timeout to wait before killing the service
     */
    public function stop(Job $job, $timeout = 10)
    {
        foreach ($job->getServices() as $service) {
            if ($service->getContainer()) {
                try {
                    $this->docker->getContainerManager()->stop($service->getContainer(), [
                        't' => $timeout
                    ]);
                } catch (ClientErrorException $e) {
                    if ($e->getResponse()->getStatusCode() != 304) {
                        throw $e;
                    }
                }

                $this->docker->getContainerManager()->remove($service->getContainer(), [
                    'v' => true,
                    'force' => true
                ]);

                $service->setContainer(null);
            }
        }
    }
}


<?php
/*
 * This file is part of JoliCi.
 *
 * (c) Joel Wurtz <jwurtz@jolicode.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Joli\JoliCi;

/**
 * Create the Job list by computing all available possibility through dimensions
 */
class Matrix
{
    private $dimensions = array();

    /**
     * Set a dimension for this matrix
     *
     * @param string $name   Name of the dimension
     * @param array  $values Value for this dimension
     */
    public function setDimension($name, array $values)
    {
        if (empty($values)) {
            $values = array(null);
        }

        $this->dimensions[$name] = $values;
    }

    /**
     * Return all possibility for the matrix
     *
     * @return array
     */
    public function compute()
    {
        $dimensions = $this->dimensions;

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

        // Pop first dimension
        $values = reset($dimensions);
        $name   = key($dimensions);
        unset($dimensions[$name]);

        // Create all possiblites for the first dimension
        $posibilities = array();

        foreach ($values as $v) {
            $posibilities[] = array($name => $v);
        }

        // If only one dimension return simple all the possibilites created (break point of recursivity)
        if (empty($dimensions)) {
            return $posibilities;
        }

        // If not create a new matrix with remaining dimension
        $matrix = new Matrix();

        foreach ($dimensions as $name => $values) {
            $matrix->setDimension($name, $values);
        }

        $result    = $matrix->compute();
        $newResult = array();

        foreach ($result as $value) {
            foreach ($posibilities as $possiblity) {
                $newResult[] = $value + $possiblity;
            }
        }

        return $newResult;
    }
}


<?php

namespace Joli\JoliCi\Command;

use Joli\JoliCi\Container;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;

class CleanCommand extends Command
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('clean');
        $this->setDescription('Clean images, containers and/or directories of previous build for this project');
        $this->addOption('project-path', 'p', InputOption::VALUE_OPTIONAL, "Path where you project is (default to current directory)", ".");
        $this->addOption('keep', 'k', InputOption::VALUE_OPTIONAL, "Number of images / containers / directories per build to keep", 1);
        $this->addOption('only-containers', null, InputOption::VALUE_NONE, "Only clean containers (no images or directories)");
        $this->addOption('only-directories', null, InputOption::VALUE_NONE, "Only clean directories (no images or containers)");
        $this->addOption('only-images', null, InputOption::VALUE_NONE, "Only clean images (no containers or directories), be aware that this may fail if containers are still attached to images (you may need to use force option)");
        $this->addOption('force', null, InputOption::VALUE_NONE, "Force removal for images");
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $container = new Container();
        $vacuum = $container->getVacuum();

        if ($input->getOption('only-containers')) {
            $vacuum->cleanContainers($vacuum->getJobsToRemove($input->getOption('project-path'), $input->getOption('keep')));

            return 0;
        }

        if ($input->getOption('only-directories')) {
            $vacuum->cleanDirectories($vacuum->getJobsToRemove($input->getOption('project-path'), $input->getOption('keep')));

            return 0;
        }

        if ($input->getOption('only-images')) {
            $vacuum->cleanImages($vacuum->getJobsToRemove($input->getOption('project-path'), $input->getOption('keep')), $input->getOption('force'));

            return 0;
        }

        $vacuum->clean($input->getOption('project-path'), $input->getOption('keep'), $input->getOption('force'));

        return 0;
    }
}


<?php
/*
 * This file is part of JoliCi.
 *
 * (c) Joel Wurtz <jwurtz@jolicode.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Joli\JoliCi\Filesystem;

use Symfony\Component\Filesystem\Filesystem as BaseFilesystem;

class Filesystem extends BaseFilesystem
{
    /**
     * Add keeping same permissions as origin file
     *
     * @see \Symfony\Component\Filesystem\Filesystem::copy()
     *
     * @param string  $originFile
     * @param string  $targetFile
     * @param Boolean $override
     */
    public function copy($originFile, $targetFile, $override = false)
    {
        parent::copy($originFile, $targetFile, $override);

        $this->chmod($targetFile, fileperms($originFile));
    }
}