<?php namespace Laraplus\Data;

use Closure;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
use Illuminate\Database\Query\Grammars\SQLiteGrammar;
use Illuminate\Database\Query\Grammars\PostgresGrammar;
use Illuminate\Database\Query\Grammars\SqlServerGrammar;

class QueryBuilder extends Builder
{
    protected $model;

    /**
     * Set a model instance
     *
     * @param \Illuminate\Database\Eloquent\Model $model
     * @return $this
     */
    public function setModel(Eloquent $model)
    {
        $this->model = $model;

        return $this;
    }

    /**
     * Set the columns to be selected.
     *
     * @param  array|mixed  $columns
     * @return $this
     */
    public function select($columns = ['*'])
    {
        parent::select($columns);

        $this->columns = $this->qualifyColumns($this->columns);

        return $this;
    }

    /**
     * Add a new select column to the query.
     *
     * @param  array|mixed  $column
     * @return $this
     */
    public function addSelect($column)
    {
        $column = $this->qualifyColumns(is_array($column) ? $column : func_get_args());

        $this->columns = array_merge((array) $this->columns, $column);

        return $this;
    }

    /**
     * Qualify translated columns
     *
     * @param $columns
     * @return mixed
     */
    protected function qualifyColumns($columns)
    {
        foreach($columns as &$column) {
            if(!in_array($column, $this->model->translatableAttributes())) {
                continue;
            }

            $primary = $this->qualifyTranslationColumn($column);
            $fallback = $this->qualifyTranslationColumn($column, true);

            if($this->model->shouldFallback()) {
                $column = new Expression($this->compileIfNull($primary, $fallback, $column));
            } else {
                $column = $primary;
            }
        }

        return $columns;
    }

    /**
     * Add a where clause to the query.
     *
     * @param  string|\Closure  $column
     * @param  string  $operator
     * @param  mixed   $value
     * @param  string  $boolean
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        // If the column is an array, we will assume it is an array of key-value pairs
        // and can add them each as a where clause. We will maintain the boolean we
        // received when the method was called and pass it into the nested where.
        if (is_array($column)) {
            return $this->addArrayOfWheres($column, $boolean);
        }

        // Then we need to check if we are dealing with a translated column and defer
        // to the "whereTranslated" clause in that case. That way the user doesn't
        // need to worry about translated columns and let us handle the details.
        if(in_array($column, $this->model->translatableAttributes())) {
            return $this->whereTranslated($column, $operator, $value, $boolean);
        }

        return parent::where($column, $operator, $value, $boolean);
    }

    /**
     * Add a where clause to the query and don't modify it for i18n.
     *
     * @param  string|\Closure  $column
     * @param  string  $operator
     * @param  mixed   $value
     * @param  string  $boolean
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function whereOriginal($column, $operator = null, $value = null, $boolean = 'and')
    {
        return parent::where($column, $operator, $value, $boolean);
    }

    /**
     * Add a translation where clause to the query.
     *
     * @param  string|\Closure  $column
     * @param  string  $operator
     * @param  mixed   $value
     * @param  string  $boolean
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function whereTranslated($column, $operator = null, $value = null, $boolean = 'and')
    {
        // Here we will make some assumptions about the operator. If only 2 values are
        // passed to the method, we will assume that the operator is an equals sign
        // and keep going. Otherwise, we'll require the operator to be passed in.
        if (func_num_args() == 2) {
            list($value, $operator) = [$operator, '='];
        } elseif ($this->invalidOperatorAndValue($operator, $value)) {
            throw new InvalidArgumentException('Illegal operator and value combination.');
        }

        // If the given operator is not found in the list of valid operators we will
        // assume that the developer is just short-cutting the '=' operators and
        // we will set the operators to '=' and set the values appropriately.
        if (! in_array(strtolower($operator), $this->operators, true)) {
            list($value, $operator) = [$operator, '='];
        }

        $fallbackColumn = $this->qualifyTranslationColumn($column, true);
        $column = $this->qualifyTranslationColumn($column);

        // Finally we'll check whether we need to consider fallback translations. In
        // that case we need to create a complex "ifnull" clause, otherwise we can
        // just prepend the translation alias and add the where clause normally.
        if (!$this->model->shouldFallback() || $column instanceof Closure) {
            return $this->where($column, $operator, $value, $boolean);
        }

        $condition = $this->compileIfNull($column, $fallbackColumn);

        return $this->whereRaw("$condition $operator ?", [$value], $boolean);
    }

    /**
     * Add a translation or where clause to the query.
     *
     * @param  string|array|\Closure  $column
     * @param  string  $operator
     * @param  mixed   $value
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function orWhereTranslated($column, $operator = null, $value = null)
    {
        return $this->whereTranslated($column, $operator, $value, 'or');
    }

    /**
     * Add a full sub-select to the query.
     *
     * @param string $column
     * @param \Illuminate\Database\Query\Builder $query
     * @param string $boolean
     * @return $this
     */
    public function whereSubQuery($column, $query, $boolean = 'and')
    {
        list($type, $operator) = ['Sub', 'in'];

        $this->wheres[] = compact('type', 'column', 'operator', 'query', 'boolean');

        $this->addBinding($query->getBindings(), 'where');

        return $this;
    }

    /**
     * Add an "order by" clause by translated column to the query.
     *
     * @param  string  $column
     * @param  string  $direction
     * @return $this
     */
    public function orderBy($column, $direction = 'asc')
    {
        if(in_array($column, $this->model->translatableAttributes())) {
            return $this->orderByTranslated($column, $direction);
        }

        return parent::orderBy($column, $direction);
    }

    /**
     * Add an "order by" clause by translated column to the query.
     *
     * @param  string  $column
     * @param  string  $direction
     * @return $this
     */
    public function orderByTranslated($column, $direction = 'asc')
    {
        $fallbackColumn = $this->qualifyTranslationColumn($column, true);
        $column = $this->qualifyTranslationColumn($column);

        if (!$this->model->shouldFallback()) {
            return $this->orderBy($column, $direction);
        }

        $condition = $this->compileIfNull($column, $fallbackColumn);

        return $this->orderByRaw("{$condition} {$direction}");
    }

    /**
     * @param $column
     * @param $fallback
     * @return string
     */
    protected function qualifyTranslationColumn($column, $fallback = false)
    {
        $alias = $this->model->getI18nTable();
        $fallback = $fallback ? '_fallback' : '';

        if(Str::contains($column, '.')) {
            list($table, $field) = explode('.', $column);
            $suffix = $this->model->getTranslationTableSuffix();

            return Str::endsWith($alias, $suffix) ?
                "{$table}{$fallback}.{$field}" :
                "{$table}{$suffix}{$fallback}.{$field}";
        }

        return "{$alias}{$fallback}.{$column}";
    }

    /**
     * @param string $primary
     * @param string $fallback
     * @param string|null $alias
     * @return string
     */
    public function compileIfNull($primary, $fallback, $alias = null)
    {
        if ($this->grammar instanceof SqlServerGrammar) {
            $ifNull = 'isnull';
        } elseif ($this->grammar instanceof MySqlGrammar || $this->grammar instanceof SQLiteGrammar) {
            $ifNull = 'ifnull';
        } elseif ($this->grammar instanceof PostgresGrammar) {
            $ifNull = 'coalesce';
        } else {
            throw new \Exception('Cannot compile IFNULL statement for grammar ' . get_class($this->grammar));
        }

        $primary = $this->grammar->wrap($primary);
        $fallback = $this->grammar->wrap($fallback);
        $alias = $alias ? ' as ' . $this->grammar->wrap($alias) : '';

        return "{$ifNull}($primary, $fallback)" . $alias;
    }

    /**
     * Get a new instance of the query builder.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    public function newQuery()
    {
        $query = parent::newQuery();

        return $query->setModel($this->model);
    }
}


<?php

declare(strict_types=1);

namespace Adrenth\Raindrop;

use Adrenth\Raindrop\Exception\UnableToAcquireAccessToken;
use Adrenth\Raindrop\Exception\ApiRequestFailed;
use Adrenth\Raindrop\Exception\RefreshTokenFailed;
use Adrenth\Raindrop\TokenStorage\TokenStorage;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;

/**
 * Class ApiBase
 *
 * @package Adrenth\Raindrop
 */
abstract class ApiBase
{
    const USER_AGENT = 'adrenth.raindrop-sdk (PHP) version 1.2.0';

    /**
     * Settings which are required to connect to the API.
     *
     * @var ApiSettings
     */
    private $settings;

    /**
     * The storage for the API authentication token.
     *
     * @var TokenStorage
     */
    private $tokenStorage;

    /**
     * HTTP Client for making API calls.
     *
     * @var \GuzzleHttp\Client
     */
    private $httpClient;

    /**
     * @param ApiSettings $settings
     * @param TokenStorage $tokenStorage
     */
    public function __construct(
        ApiSettings $settings,
        TokenStorage $tokenStorage
    ) {
        $this->settings = $settings;
        $this->tokenStorage = $tokenStorage;
        $this->httpClient = new \GuzzleHttp\Client([
            'base_uri' => $this->settings->getEnvironment()->getApiUrl() . '/hydro/v1/',
            'headers' => [
                'Content-Type' => 'application/json',
                'User-Agent' => self::USER_AGENT
            ]
        ]);
    }

    /**
     * @param string $method
     * @param string $uri
     * @param array $options
     * @return ResponseInterface
     * @throws GuzzleException
     * @throws ApiRequestFailed
     */
    protected function callHydroApi(string $method, string $uri, array $options = []): ResponseInterface
    {
        if (!isset($options['handler'])) {
            $options['handler'] = $this->getHandlerStack();
        }

        return $this->httpClient->request($method, $uri, $options);
    }

    /**
     * @return ApiAccessToken
     * @throws RefreshTokenFailed
     */
    protected function refreshToken(): ApiAccessToken
    {
        try {
            $client = new \GuzzleHttp\Client([
                'base_uri' => $this->settings->getEnvironment()->getApiUrl(),
                'headers' => [
                    'Content-Type' => 'application/json',
                    'User-Agent' => self::USER_AGENT,
                    'Authorization' => 'Basic ' . base64_encode(
                        $this->settings->getClientId() . ':' . $this->settings->getClientSecret()
                    )
                ]
            ]);

            $response = $client->post('/authorization/v1/oauth/token?grant_type=client_credentials');
        } catch (RequestException $e) {
            throw new RefreshTokenFailed($e->getMessage());
        }

        try {
            $json = $response->getBody()->getContents();

            $data = \GuzzleHttp\json_decode($json, true);
        } catch (RuntimeException $e) {
            throw new RefreshTokenFailed('Invalid response from server');
        } catch (InvalidArgumentException $e) {
            throw new RefreshTokenFailed('Invalid response from server');
        }

        $accessToken = new ApiAccessToken($data['access_token'], time() + $data['expires_in']);

        $this->tokenStorage->setAccessToken($accessToken);

        return $accessToken;
    }

    /**
     * @return HandlerStack
     */
    private function getHandlerStack(): HandlerStack
    {
        $stack = HandlerStack::create();
        $stack->push(Middleware::mapRequest(function (RequestInterface $request) {
            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
            $token = $this->getAccessToken();

            if ($token) {
                /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                return $request->withHeader('Authorization', "Bearer {$token->getToken()}");
            }

            return $request;
        }), 'add_oauth2_header');

        $stack->push(function (callable $handler) {
            return function (RequestInterface $request, array $options) use ($handler) {
                /** @var PromiseInterface $promise */
                $promise = $handler($request, $options);

                return $promise->then(
                    function (ResponseInterface $response) {
                        $statusCode = $response->getStatusCode();

                        if ($response->getStatusCode() === 500) {
                            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                            throw ApiRequestFailed::withCode(ApiRequestFailed::INTERNAL_SERVER_ERROR, $response);
                        }

                        $contents = null;

                        try {
                            $contents = $response->getBody()->getContents();

                            if (!empty($contents)) {
                                $contents = \GuzzleHttp\json_decode($contents, true);
                            }
                        } catch (RuntimeException $e) {
                            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                            throw ApiRequestFailed::withCode(ApiRequestFailed::INVALID_JSON, $response);
                        } catch (InvalidArgumentException $e) {
                            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                            throw ApiRequestFailed::withCode(ApiRequestFailed::INVALID_JSON, $response);
                        }

                        if (is_array($contents)
                            && array_key_exists('message', $contents)
                            && ($statusCode >= 400 && $statusCode < 500)
                        ) {
                            /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                            throw ApiRequestFailed::withMessage($contents['message'], $response);
                        }

                        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                        $response->getBody()->rewind();
                        return $response;
                    }
                );
            };
        });

        return $stack;
    }

    /**
     * Get the API access token. This method can be used to verify if provided `ApiSettings` are valid.
     *
     * @return ApiAccessToken
     * @throws RefreshTokenFailed
     */
    public function getAccessToken(): ApiAccessToken
    {
        try {
            $accessToken = $this->tokenStorage->getAccessToken();
        } catch (UnableToAcquireAccessToken $e) {
            $accessToken = null;
        }

        if ($accessToken && $accessToken->isExpired()) {
            $this->tokenStorage->unsetAccessToken();
            $accessToken = null;
        }

        return $accessToken ?: $this->refreshToken();
    }
}


<?php namespace Laraplus\Data;

use Illuminate\Support\ServiceProvider;

class TranslatableServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        TranslatableConfig::cacheGetter(function($table) {
            return $this->app['cache']->get('translatable.' . $table);
        });

        TranslatableConfig::cacheSetter(function($table, $fields) {
            return $this->app['cache']->forever('translatable.' . $table, $fields);
        });

        TranslatableConfig::currentLocaleGetter(function() {
            return $this->app->getLocale();
        });

        TranslatableConfig::fallbackLocaleGetter(function() {
            return method_exists($this->app, 'getFallbackLocale')
                ? $this->app->getFallbackLocale()
                : config('app.fallback_locale');
        });
    }

    /**
     * Boot the service provider.
     *
     * @return void
     */
    public function boot()
    {
        $config = dirname(__DIR__) . '/config/translatable.php';

        $this->mergeConfigFrom($config, 'translatable');
        $this->publishes([$config => config_path('translatable.php')], 'config');

        TranslatableConfig::setDbSettings(
            $this->app['config']->get('translatable.db_settings')
        );

        TranslatableConfig::setDefaults(
            $this->app['config']->get('translatable.defaults')
        );
    }
}


<?php namespace Laraplus\Data;

use Illuminate\Database\Eloquent\Relations\HasMany;

trait Translatable
{
    protected $overrideLocale = null;

    protected $overrideFallbackLocale = null;

    protected $overrideOnlyTranslated = null;

    protected $overrideWithFallback = null;

    protected $localeChanged = false;

    /**
     * Translated attributes cache
     *
     * @var array
     */
    protected static $i18nAttributes = [];

    /**
     * Boot the trait.
     */
    public static function bootTranslatable()
    {
        static::addGlobalScope(new TranslatableScope);
    }

    /**
     * Save a new model and return the instance.
     *
     * @param array $attributes
     * @param array|string $translations
     * @return static
     */
    public static function create(array $attributes = [], $translations = [])
    {
        $model = new static($attributes);

        if ($model->save() && is_array($translations)) {
            $model->saveTranslations($translations);
        }

        return $model;
    }

    /**
     * Save a new model in provided locale and return the instance.
     *
     * @param string $locale
     * @param array $attributes
     * @param array|string $translations
     * @return static
     */
    public static function createInLocale($locale, array $attributes = [], $translations = [])
    {
        $model = (new static($attributes))->setLocale($locale);

        if ($model->save() && is_array($translations)) {
            $model->saveTranslations($translations);
        }

        return $model;
    }

    /**
     * Save a new model and return the instance. Allow mass-assignment.
     *
     * @param array $attributes
     * @param array|string $translations
     * @return static
     */
    public static function forceCreate(array $attributes, $translations = [])
    {
        $model = new static;

        return static::unguarded(function () use ($model, $attributes, $translations){
            return $model->create($attributes, $translations);
        });
    }

    /**
     * Save a new model in provided locale and return the instance. Allow mass-assignment.
     *
     * @param array $attributes
     * @param array|string $translations
     * @return static
     */
    public static function forceCreateInLocale($locale, array $attributes, $translations = [])
    {
        $model = new static;

        return static::unguarded(function () use ($locale, $model, $attributes, $translations){
            return $model->createInLocale($locale, $attributes, $translations);
        });
    }

    /**
     * Reload a fresh model instance from the database.
     *
     * @param  array|string $with
     * @return static|null
     */
    public function fresh($with = [])
    {
        if (!$this->exists) {
            return;
        }

        $query = static::newQueryWithoutScopes()
            ->with(is_string($with) ? func_get_args() : $with)
            ->where($this->getKeyName(), $this->getKey());

        (new TranslatableScope())->apply($query, $this);

        return $query->first();
    }

    /**
     * @param array $translations
     * @return bool
     */
    public function saveTranslations(array $translations)
    {
        $success = true;
        $fresh = parent::fresh();

        foreach ($translations as $locale => $attributes) {
            $model = clone $fresh;
            $model->setLocale($locale);
            $model->fill($attributes);

            $success &= $model->save();
        }

        return $success;
    }

    /**
     * @param array $translations
     * @return bool
     */
    public function forceSaveTranslations(array $translations)
    {
        return static::unguarded(function () use ($translations){
            return $this->saveTranslations($translations);
        });
    }

    /**
     * @param $locale
     * @param array $attributes
     * @return bool
     */
    public function saveTranslation($locale, array $attributes)
    {
        return $this->saveTranslations([
            $locale => $attributes
        ]);
    }

    /**
     * @param $locale
     * @param array $attributes
     * @return bool
     */
    public function forceSaveTranslation($locale, array $attributes)
    {
        return static::unguarded(function () use ($locale, $attributes){
            return $this->saveTranslation($locale, $attributes);
        });
    }

    /**
     * @param array $attributes
     * @return $this
     * @throws MassAssignmentException
     */
    public function fill(array $attributes)
    {
        if (!isset(static::$i18nAttributes[$this->getTable()])) {
            $this->initTranslatableAttributes();
        }

        return parent::fill($attributes);
    }

    /**
     * Init translatable attributes.
     */
    protected function initTranslatableAttributes()
    {
        if (property_exists($this, 'translatable')) {
            $attributes = $this->translatable;
        }else {
            $attributes = $this->getTranslatableAttributesFromSchema();
        }

        static::$i18nAttributes[$this->getTable()] = $attributes;
    }

    /**
     * Get an array of translatable attributes from schema.
     *
     * @return array
     */
    protected function getTranslatableAttributesFromSchema()
    {
        if ((!$con = $this->getConnection()) || (!$builder = $con->getSchemaBuilder())) {
            return [];
        }

        if ($columns = TranslatableConfig::cacheGet($this->getI18nTable())) {
            return $columns;
        }

        $columns = $builder->getColumnListing($this->getI18nTable());
        unset($columns[array_search($this->getForeignKey(), $columns)]);

        TranslatableConfig::cacheSet($this->getI18nTable(), $columns);

        return $columns;
    }

    /**
     * Get a collection of translated attributes in provided locale.
     *
     * @param $locale
     * @return \Laraplus\Data\TranslationModel|null
     */
    public function translate($locale)
    {
        $found = $this->translations->where($this->getLocaleKey(), $locale)->first();

        if (!$found && $this->shouldFallback($locale)) {
            return $this->translate($this->getFallbackLocale());
        }

        return $found;
    }

    /**
     * Get a collection of translated attributes in provided locale or create new one.
     *
     * @param $locale
     * @return \Laraplus\Data\TranslationModel
     */
    public function translateOrNew($locale)
    {

        if (is_null($instance = $this->translate($locale))) {
            return $this->newModelInstance();
        }

        return $instance;

    }

    /**
     * Translations relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function translations()
    {
        $localKey = $this->getKeyName();
        $foreignKey = $this->getForeignKey();
        $instance = $this->translationModel();

        return new HasMany($instance->newQuery(), $this, $instance->getTable() . '.' . $foreignKey, $localKey);
    }

    /**
     * Returns the default translation model instance.
     *
     * @return DefaultTranslationModel
     */
    public function translationModel()
    {
        $translation = new TranslationModel();
        $translation->setConnection($this->getI18nConnection());
        $translation->setTable($this->getI18nTable());
        $translation->setKeyName($this->getForeignKey());
        $translation->setLocaleKey($this->getLocaleKey());

        if ($attributes = $this->translatableAttributes()) {
            $translation->fillable(array_intersect($attributes, $this->getFillable()));
        }

        return $translation;
    }

    /**
     * Get an array of translatable attributes.
     *
     * @return array
     */
    public function translatableAttributes()
    {
        if(!isset(static::$i18nAttributes[$this->getTable()])) {
            return [];
        }

        return static::$i18nAttributes[$this->getTable()];
    }

    /**
     * Get name of the locale key.
     *
     * @return string
     */
    public function getLocaleKey()
    {
        return TranslatableConfig::dbKey();
    }

    /**
     * Get current locale
     *
     * @param $locale
     * @return string
     */
    public function setLocale($locale)
    {
        $this->overrideLocale = $locale;

        $this->localeChanged = true;

        return $this;
    }

    /**
     * Get current locale
     *
     * @return string
     */
    public function getLocale()
    {
        if ($this->overrideLocale) {
            return $this->overrideLocale;
        }

        if (property_exists($this, 'locale')) {
            return $this->locale;
        }

        return TranslatableConfig::currentLocale();
    }

    /**
     * Get current locale
     *
     * @param $locale
     * @return string
     */
    public function setFallbackLocale($locale)
    {
        $this->overrideFallbackLocale = $locale;

        return $this;
    }

    /**
     * Get current locale
     *
     * @return string
     */
    public function getFallbackLocale()
    {
        if ($this->overrideFallbackLocale) {
            return $this->overrideFallbackLocale;
        }

        if (property_exists($this, 'fallbackLocale')) {
            return $this->fallbackLocale;
        }

        return TranslatableConfig::fallbackLocale();
    }

    /**
     * Set if model should select only translated rows
     *
     * @param bool $onlyTranslated
     * @return $this
     */
    public function setOnlyTranslated($onlyTranslated)
    {
        $this->overrideOnlyTranslated = $onlyTranslated;

        return $this;
    }

    /**
     * Get current locale
     *
     * @return bool
     */
    public function getOnlyTranslated()
    {
        if (!is_null($this->overrideOnlyTranslated)) {
            return $this->overrideOnlyTranslated;
        }

        if (property_exists($this, 'onlyTranslated')) {
            return $this->onlyTranslated;
        }

        return TranslatableConfig::onlyTranslated();
    }

    /**
     * Set if model should select only translated rows
     *
     * @param bool $withFallback
     * @return $this
     */
    public function setWithFallback($withFallback)
    {
        $this->overrideWithFallback = $withFallback;

        return $this;
    }

    /**
     * Get current locale
     *
     * @return bool
     */
    public function getWithFallback()
    {
        if (!is_null($this->overrideWithFallback)) {
            return $this->overrideWithFallback;
        }

        if (property_exists($this, 'withFallback')) {
            return $this->withFallback;
        }

        return TranslatableConfig::withFallback();
    }

    /**
     * Get the i18n connection name associated with the model.
     *
     * @return string
     */
    public function getI18nConnection()
    {
        return $this->getConnectionName();
    }

    /**
     * Get the i18n table associated with the model.
     *
     * @return string
     */
    public function getI18nTable()
    {
        return $this->getTable() . $this->getTranslationTableSuffix();
    }

    /**
     * Get the i18n table suffix.
     *
     * @return string
     */
    public function getTranslationTableSuffix()
    {
        return TranslatableConfig::dbSuffix();
    }

    /**
     * Should fallback to a primary translation.
     *
     * @param string|null $locale
     * @return bool
     */
    public function shouldFallback($locale = null)
    {
        if (!$this->getWithFallback() || !$this->getFallbackLocale()) {
            return false;
        }

        $locale = $locale ?: $this->getLocale();

        return $locale != $this->getFallbackLocale();
    }

    /**
     * Create a new Eloquent query builder for the model.
     *
     * @param  \Illuminate\Database\Query\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newEloquentBuilder($query)
    {
        return new Builder($query);
    }

    /**
     * Get a new query builder instance for the connection.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    protected function newBaseQueryBuilder()
    {
        $conn = $this->getConnection();

        $grammar = $conn->getQueryGrammar();

        $builder = new QueryBuilder($conn, $grammar, $conn->getPostProcessor());

        return $builder->setModel($this);
    }

    /**
     * Get the attributes that have been changed since last sync.
     *
     * @return array
     */
    public function getDirty()
    {
        $dirty = parent::getDirty();

        if (!$this->localeChanged) {
            return $dirty;
        }

        foreach ($this->translatableAttributes() as $key) {
            if (isset($this->attributes[$key])) {
                $dirty[$key] = $this->attributes[$key];
            }
        }

        return $dirty;
    }

    /**
     * Sync the original attributes with the current.
     *
     * @return $this
     */
    public function syncOriginal()
    {
        $this->localeChanged = false;

        return parent::syncOriginal();
    }
}


<?php

declare(strict_types=1);

namespace Adrenth\Raindrop\TokenStorage;

use Adrenth\Raindrop\ApiAccessToken;
use Adrenth\Raindrop\Exception\UnableToAcquireAccessToken;

/**
 * Class FileTokenStorage
 *
 * @package Adrenth\Raindrop\TokenStorage
 */
class FileTokenStorage implements TokenStorage
{
    /**
     * @var string
     */
    private $filename;

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

    /**
     * {@inheritdoc}
     */
    public function getAccessToken(): ApiAccessToken
    {
        if (!is_readable($this->filename)) {
            throw new UnableToAcquireAccessToken('Access Token is not found in the storage.');
        }

        $data = file_get_contents($this->filename);

        if (!empty($data) && substr_count($data, '|') === 1) {
            $data = explode('|', $data);
            return ApiAccessToken::create($data[0] ?? '', (int) ($data[1] ?? 0));
        }

        throw new UnableToAcquireAccessToken('Access Token is not found in the storage.');
    }

    /**
     * {@inheritdoc}
     */
    public function setAccessToken(ApiAccessToken $token)
    {
        file_put_contents($this->filename, $token->getToken() . '|'. $token->getExpiresAt());
    }

    /**
     * {@inheritdoc}
     */
    public function unsetAccessToken()
    {
        if (file_exists($this->filename)) {
            unlink($this->filename);
        }
    }
}


<?php

namespace SilverStripe\SelectUpload;

use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Security\Permission;
use SilverStripe\ORM\SS_List;
use SilverStripe\View\Requirements;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\File;
use SilverStripe\AssetAdmin\Controller\AssetAdmin;
use SilverStripe\Forms\Form;

/**
 * A composite form field which allows users to select a folder into which files may be uploaded
 */
class SelectUploadField extends UploadField
{

    private static $url_handlers = [
        'folder/tree/$ID' => 'tree'
    ];

    /**
     * @config
     * @var array
     */
    private static $allowed_actions = [
        'upload',
        'tree',
        'changeFolder'
    ];
    /**
     * Folder selector field
     *
     * @var FolderDropdownField
     */
    protected $selectField;

    /**
     * @var bool|string
     */
    protected $canSelectFolder = true;

    public function __construct($name, $title = null, SS_List $items = null)
    {
        parent::__construct($name, $title, $items);
        $this->selectField = FolderDropdownField::create("{$name}/folder");

        // If we haven't uploaded to a folder yet, set to the default foldername
        if (!$this->selectField->Value()) {
            $folderID = $this->folderIDFromPath($this->getDefaultFolderName());
            if ($folderID) {
                $this->selectField->setValue($folderID);
            }
        }
    }

    /**
     * @param array $properties
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
     */
    public function Field($properties = [])
    {
        $field = parent::Field($properties);
        $folderLink = $this->Link('changeFolder');
        // Extra requirements
        Requirements::customScript("const folderURL = '$folderLink'");
        Requirements::javascript("silverstripe/selectupload:/js/SelectUploadField.js");
        Requirements::css("silverstripe/selectupload:/css/SelectUploadField.css");
        return $field;
    }

    /**
     * Get the folder selector field
     *
     * @return FolderDropdownField
     */
    public function FolderSelector()
    {
        return $this->selectField;
    }

    /**
     * Return the subtree for a selected folder id
     *
     * @param HTTPRequest $request
     * @return string HTTP Response
     */
    public function tree(HTTPRequest $request)
    {
        return $this->FolderSelector()->tree($request);
    }

    /**
     * process HTTP request to change the upload folder
     *
     * @param HTTPRequest $request
     * @throws HTTPResponse_Exception
     */
    public function changeFolder(HTTPRequest $request)
    {
        // CSRF check
        $token = $this->getForm()->getSecurityToken();
        if (!$token->checkRequest($request)) {
            return $this->httpError(400);
        }
        $folderID = $request->postVar('FolderID');
        if ($folderID) {
            $this->FolderSelector()->setLastFolderID($folderID);
        }
    }

    /**
     * @param Form $form
     * @return UploadField
     */
    public function setForm($form)
    {
        $this->selectField->setForm($form);
        return parent::setForm($form);
    }

    /**
     * @return string
     */
    public function Type()
    {
        return 'selectupload entwine-uploadfield uploadfield';
    }

    /**
     * Given a request, ensure that the current field is aware of the selected folder
     *
     * @param HTTPRequest $request
     */
    protected function updateFolderName(HTTPRequest $request)
    {
        // check if allowed to select folder
        if (!$this->getCanSelectFolder()) {
            return;
        }

        // Get path from upload
        $folderID = $request->requestVar("{$this->Name}/folder");
        $path = $this->folderPathFromID($folderID);
        if ($path !== false) {
            $this->setFolderName($path);
            $this->selectField->setValue($folderID);
        }
    }

    /**
     * Get path of a folder relative to /assets/ by id.
     * This will be a format appropriate for setting setFolderName to
     *
     * @param int $folderID
     * @return string|bool Relative path to the assets directory, or false if not found
     */
    protected function folderPathFromID($folderID)
    {
        if (empty($folderID)) {
            return false;
        }
        $folder = Folder::get()->byID($folderID);
        if (!$folder) {
            return false;
        }

        // Translate path
        $path = $folder->getFilename();
        if (stripos($path, ASSETS_DIR) === 0) {
            $path = substr($path, strlen(ASSETS_DIR) + 1);
        }
        return $path;
    }

    /**
     * Gets the ID of a folder given a path relative to /assets/.
     *
     * @param string $path
     * @return int Folder ID
     */
    protected function folderIDFromPath($path)
    {
        $folder = File::find($path);
        if ($folder) {
            return $folder->ID;
        }
    }

    /**
     * @param HTTPRequest $request
     * @return array|HTTPResponse|\SilverStripe\Control\RequestHandler|string
     */
    public function handleRequest(HTTPRequest $request)
    {
        $this->updateFolderName($request);
        return parent::handleRequest($request);
    }

    /**
     * Specify whether the user can select an upload folder.
     * String values will be treated as required permission codes
     *
     * @param boolean|string $canSelectFolder Either a boolean flag, or a required
     * permission code
     * @return $this
     */
    public function setCanSelectFolder($canSelectFolder)
    {
        $this->canSelectFolder = $canSelectFolder;
        return $this;
    }

    /**
     * Determine if the current member is allowed to change the folder
     *
     * @return boolean
     */
    public function getCanSelectFolder()
    {
        if (!$this->isActive()) {
            return false;
        }
        if ($this->template && in_array($this->template, self::config()->disable_for_templates)) {
            return false;
        }
        // Check config
        $can = $this->canSelectFolder;
        return (is_bool($can)) ? $can : Permission::check($can);
    }

    /**
     * @return string
     */
    public function getFolderName()
    {
        // Ensure that, if this member is allowed, the persistant folder overrides any default set
        if ($this->getCanSelectFolder()) {
            $path = $this->folderPathFromID($this->selectField->Value());
            if ($path !== false) {
                return $path;
            }
        }
        return $this->getDefaultFolderName();
    }

    /**
     * Get the 'default' folder name
     *
     * @return string
     */
    public function getDefaultFolderName()
    {
        return parent::getFolderName();
    }

    /**
     * Improve readability of folder name and directory structure.
     * add a space before and after each /
     * 'foo/bar' will be displayed as 'foo / bar'
     *
     * @return null|string|string[]
     */
    public function getDisplayFolderName()
    {
        $name = $this->getFolderName();
        return preg_replace('/\s*\\/\s*/', ' / ', trim($name, '/'));
    }

    /**
     * Returns true if the field is neither readonly nor disabled
     *
     * @return boolean
     */
    public function isActive()
    {
        return !$this->isDisabled() && !$this->isReadonly();
    }
}


<?php

declare(strict_types=1);

namespace Adrenth\Raindrop;

use Adrenth\Raindrop\Exception\AddressWhitelistingFailed;
use Adrenth\Raindrop\Exception\ApiRequestFailed;
use Adrenth\Raindrop\Exception\AuthenticationFailed;
use Adrenth\Raindrop\Exception\ChallengeFailed;
use Adrenth\Raindrop\Response\AuthenticationResponse;
use Adrenth\Raindrop\Response\ChallengeResponse;
use Adrenth\Raindrop\Response\WhitelistResponse;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;

/**
 * Class Server
 *
 * Server-side Raindrop is meant to secure access to large systems, databases, and APIs. Server-side Raindrop consists
 * of a transaction performed via a smart contract that publicly validates access to a private system.
 *
 * @package Adrenth\Raindrop
 */
class Server extends ApiBase
{
    /**
     * All users who want to access to your Raindrop-enabled system will have to be whitelisted.
     *
     * @param string $address Hydro Etherium Address
     * @return WhitelistResponse
     * @throws AddressWhitelistingFailed
     */
    public function whitelist(string $address): WhitelistResponse
    {
        try {
            $response = $this->callHydroApi(
                'post',
                'whitelist',
                [
                    'timeout' => 60,
                    'json' => [
                        'address' => $address
                    ]
                ]
            );

            $data = \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
        } catch (RuntimeException $e) {
            throw AddressWhitelistingFailed::forAddress($address, $e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw AddressWhitelistingFailed::forAddress($address, $e->getMessage());
        } catch (GuzzleException $e) {
            throw AddressWhitelistingFailed::forAddress($address, $e->getMessage());
        } catch (ApiRequestFailed $e) {
            throw AddressWhitelistingFailed::forAddress($address, $e->getMessage());
        }

        return new WhitelistResponse(
            $data['hydro_address_id'],
            $data['transaction_hash']
        );
    }

    /**
     * After being whitelisted, each user must authenticate through the Server-side Raindrop process once every 24 hours
     * to retain access to the protected system.
     *
     * @param string $hydroAddressId
     * @return ChallengeResponse
     * @throws ChallengeFailed
     */
    public function challenge(string $hydroAddressId): ChallengeResponse
    {
        try {
            $response = $this->callHydroApi(
                'post',
                'challenge',
                [
                    'json' => [
                        'hydro_address_id' => $hydroAddressId
                    ]
                ]
            );

            $data = \GuzzleHttp\json_decode($response->getBody()->getContents(), true, 512, JSON_BIGINT_AS_STRING);
        } catch (RuntimeException $e) {
            throw ChallengeFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw ChallengeFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        } catch (GuzzleException $e) {
            throw ChallengeFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        } catch (ApiRequestFailed $e) {
            throw ChallengeFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        }

        return new ChallengeResponse(
            $data['amount'],
            (int) $data['challenge'],
            (int) $data['partner_id'],
            $data['transaction_hash']
        );
    }

    /**
     * @param string $hydroAddressId
     * @return AuthenticationResponse
     * @throws AuthenticationFailed
     */
    public function authenticate(string $hydroAddressId): AuthenticationResponse
    {
        try {
            $response = $this->callHydroApi(
                'get',
                'authenticate?hydro_address_id=' . $hydroAddressId
            );

            $data = \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
        } catch (RuntimeException $e) {
            throw AuthenticationFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw AuthenticationFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        } catch (GuzzleException $e) {
            throw AuthenticationFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        } catch (ApiRequestFailed $e) {
            throw AuthenticationFailed::forHydroAddressId($hydroAddressId, $e->getMessage());
        }

        if ($response->getStatusCode() !== 200) {
            throw AuthenticationFailed::forHydroAddressId($hydroAddressId, 'Unexpected response code.');
        }

        return new AuthenticationResponse(
            $data['authentication_id'],
            strtotime($data['timestamp'])
        );
    }
}


<?php namespace Laraplus\Data;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;

class Builder extends EloquentBuilder
{
    /**
     * Update a record in the database.
     *
     * @param  array  $values
     * @return int
     */
    public function update(array $values)
    {
        $updated = 0;

        $modelKey = $this->getModel()->getKey();
        $modelKeyName = $this->model->getKeyName();

        $values = $this->addUpdatedAtColumn($values);
        list($values, $i18nValues) = $this->filterValues($values);

        $ids = $modelKey ? [$modelKey] : $this->pluck($modelKeyName)->all();

        if($values) {
            $updated += $this->updateBase($values, $ids);
        }

        if($i18nValues) {
            $updated += $this->updateI18n($i18nValues, $ids);
        }

        return $updated;
    }

    /**
     * Increment a column's value by a given amount.
     *
     * @param  string  $column
     * @param  int  $amount
     * @param  array  $extra
     * @return int
     */
    public function increment($column, $amount = 1, array $extra = [])
    {
        $extra = $this->addUpdatedAtColumn($extra);

        return $this->noTranslationsQuery()->increment($column, $amount, $extra);
    }

    /**
     * Decrement a column's value by a given amount.
     *
     * @param  string  $column
     * @param  int  $amount
     * @param  array  $extra
     * @return int
     */
    public function decrement($column, $amount = 1, array $extra = [])
    {
        $extra = $this->addUpdatedAtColumn($extra);

        return $this->noTranslationsQuery()->decrement($column, $amount, $extra);
    }

    /**
     * Insert a new record into the database.
     *
     * @param  array  $values
     * @return bool
     */
    public function insert(array $values)
    {
        list($values, $i18nValues) = $this->filterValues($values);

        if($this->query->insert($values)) {
            return $this->insertI18n($i18nValues, $values[$this->model->getKeyName()]);
        }
    }

    /**
     * Insert a new record and get the value of the primary key.
     *
     * @param  array   $values
     * @param  string  $sequence
     * @return int
     */
    public function insertGetId(array $values, $sequence = null)
    {
        list($values, $i18nValues) = $this->filterValues($values);

        if($id = $this->query->insertGetId($values, $sequence)) {
            if($this->insertI18n($i18nValues, $id)) {
                return $id;
            }
        }

        return false;
    }

    /**
     * Delete a record from the database.
     *
     * @return mixed
     */
    public function delete()
    {
        if (isset($this->onDelete)) {
            return call_user_func($this->onDelete, $this);
        }

        return $this->i18nDeleteQuery()->delete() | $this->toBase()->delete();
    }

    /**
     * Run the default delete function on the builder.
     *
     * @return mixed
     */
    public function forceDelete()
    {
        return $this->i18nDeleteQuery(false)->delete() && $this->query->delete();
    }

    /**
     * Filters translatable values from non-translatable.
     *
     * @param array $values
     * @return array
     */
    protected function filterValues(array $values)
    {
        $attributes = $this->model->translatableAttributes();

        $translatable = [];

        foreach($attributes as $key) {
            if(array_key_exists($key, $values)) {
                $translatable[$key] = $values[$key];

                unset($values[$key]);
            }
        }

        return [$values, $translatable];
    }

    /**
     * @param array $values
     * @param mixed $key
     * @return bool
     */
    protected function insertI18n(array $values, $key)
    {
        if(count($values) == 0) {
            return true;
        }

        $values[$this->model->getForeignKey()] = $key;
        $values[$this->model->getLocaleKey()] = $this->model->getLocale();

        return $this->i18nQuery()->insert($values);
    }

    /**
     * Update values in base table
     *
     * @param array $values
     * @param $ids
     * @return mixed
     */
    private function updateBase(array $values, array $ids)
    {
        $query = $this->model->newQuery()
            ->whereIn($this->model->getKeyName(), $ids)
            ->getQuery();

        return $query->update($values);
    }

    /**
     * @param array $values
     * @param array $ids
     * @return bool
     */
    protected function updateI18n(array $values, array $ids)
    {
        if(count($values) == 0) {
            return true;
        }

        $updated = 0;

        foreach($ids as $id) {
            $query = $this->i18nQuery()
                ->whereOriginal($this->model->getForeignKey(), $id)
                ->whereOriginal($this->model->getLocaleKey(), $this->model->getLocale());

            if($query->exists()) {
                unset($values[$this->model->getLocaleKey()]);
                $updated += $query->update($values);
            } else {
                $updated += $this->insertI18n($values, $id);
            }
        }

        return $updated;
    }

    /**
     * Get the query builder instance for translation table.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    public function i18nQuery()
    {
        $query = $this->getModel()->newQueryWithoutScopes()->getQuery();

        $query->from($this->model->getI18nTable());

        return $query;
    }

    /**
     * Get the delete query instance for translation table.
     *
     * @param bool $withGlobalScopes
     * @return \Illuminate\Database\Query\Builder
     */
    protected function i18nDeleteQuery($withGlobalScopes = true)
    {
        $subQuery = $withGlobalScopes ? $this->toBase() : $this->getQuery();
        $subQuery->select($this->model->getQualifiedKeyName());

        return $this->i18nQuery()->whereIn(
            $this->model->getForeignKey(), $subQuery->pluck($this->model->getKeyName())
        );
    }

    /**
     * Get the base query without translations
     *
     * @return \Illuminate\Database\Query\Builder
     */
    protected function noTranslationsQuery()
    {
        return $this->withoutGlobalScope(TranslatableScope::class)->toBase();
    }
}


<?php namespace Laraplus\Data;

use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;

class TranslationModel extends Eloquent
{
    /**
     * Translation model does not include timestamps by default
     *
     * @var boolean
     */
    public $timestamps = false;

    /**
     * Name of the table (will be set dynamically)
     *
     * @var string
     */
    protected $table = null;

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

    /**
     * @var string
     */
    protected $localeKey = 'locale';

    /**
     * Set the keys for a save update query.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    protected function setKeysForSaveQuery(EloquentBuilder $query)
    {
        $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());
        $query->where($this->localeKey, '=', $this->{$this->localeKey});

        return $query;
    }

    /**
     * @param $localeKey
     * @return $this
     */
    public function setLocaleKey($localeKey) {
        $this->localeKey = $localeKey;

        return $this;
    }
}


<?php

namespace Iber\Phkey\Environment\Unix;

use Iber\Phkey\Contracts\MatchableInterface;

/**
 * Class Matcher
 *
 * Responsible for mapping keys to terminal input
 *
 * @package  Iber\Phkey\Environment\Unix
 */
class Matcher implements MatchableInterface
{

    /**
     * Most unicode controls begin with \u001b
     * which is the escape key as well
     */
    protected $escape = '\u001b';

    /**
     * Plain text char map
     *
     * @type array
     */
    protected $text = [
        'enter' => "\n",
        'space' => ' ',
        'tab'   => "\t"
    ];

    /**
     * Ascii char map
     *
     * @type array
     */
    protected $ascii = [
        'backspace' => 127 //\u0008 doesn't work
    ];

    /**
     * Unicode char map
     *
     * @type array
     */
    protected $unicode = [
        // main keys
        'escape'   => '\u001b',
        'delete'   => '\u001b[3~',

        // arrow keys
        'up'       => '\u001b[A',
        'down'     => '\u001b[B',
        'right'    => '\u001b[C',
        'left'     => '\u001b[D',

        // function keys
        'f1'       => '\u001bOP',
        'f2'       => '\u001bOQ',
        'f3'       => '\u001bOR',
        'f4'       => '\u001bOS',
        'f5'       => '\u001b[15~',
        'f6'       => '\u001b[17~',
        'f7'       => '\u001b[18~',
        'f8'       => '\u001b[19~',
        'f9'       => '\u001b[20~',
        'f10'      => '\u001b[21~',
        'f11'      => '\u001b[23~',
        'f12'      => '\u001b[24~',
        'f13'      => '\u001b[25~',
        'f14'      => '\u001b[26~',
        'f15'      => '\u001b[28~',
        'f16'      => '\u001b[29~',
        'f17'      => '\u001b[31~',
        'f18'      => '\u001b[32~',
        'f19'      => '\u001b[33~',
        'ff20'     => '\u001b[34~',

        // other keys
        'do'       => '\u001b[29~',
        'find'     => '\u001b[1~',
        'help'     => '\u001b[28~',
        'insert'   => '\u001b[2~',
        'end'      => '\u001b[F',
        'home'     => '\u001b[H',
        'next'     => '\u001b[6~',
        'previous' => '\u001b[5~',
        'select'   => '\u001b[44~',
    ];

    /**
     * Fetches a human readable key representation
     * based on a unicode/ascii/plain text charset
     *
     * @param string $input unicode string
     *
     * @return bool|int|string
     */
    public function getKey($input)
    {
        // try matching the key with plain text
        foreach ($this->text as $key => $code) {
            if ($code === $input) {
                return $key;
            }
        }

        // try matching the key with ascii
        foreach ($this->ascii as $key => $code) {
            if (chr($code) === $input) {
                return $key;
            }
        }

        // try matching the key with unicode
        foreach ($this->unicode as $key => $code) {
            if (unicode_to_string($code) === $input) {
                return $key;
            }
        }

        return false;
    }

    /**
     * Checks whether a string has only latin characters
     *
     * @param $input
     * @return int
     */
    public function isBasicLatin($input)
    {
        return preg_match('/[^\x00-\x7F]+/', $input, $matches);
    }

    /**
     * Get the encoded escape key
     *
     * @return string
     */
    public function getEscapeKey()
    {
        return unicode_to_string($this->escape);
    }
}


<?php

namespace Iber\Phkey\Environment\Unix;

use Iber\Phkey\Contracts\ListenableInterface;
use Iber\Phkey\Contracts\StreamableInterface;
use Iber\Phkey\Events\KeyPressEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
 * Class Listener
 *
 * Allows listening for keyboard keys and bind events to those keys
 *
 * @package  Iber\Phkey\Environment\Unix
 */
class Listener implements ListenableInterface
{

    /**
     * This is the amount of time we stop waiting for the rest
     * of the key code sequence characters and mark the character
     * as the escape key
     *
     * The timespan between characters when a special char is
     * pressed is around 0.00001
     *
     * In order to capture the escape key, we need to make sure
     * that there aren't any characters following the escape key
     */
    const ESCAPE_TIMEOUT = 0.001;

    /**
     * Object responsible for key matching
     *
     * @type \Iber\Phkey\Contracts\MatchableInterface
     */
    protected $matcher;

    /**
     * Used to keep the listener active
     *
     * @var boolean
     */
    protected $isListening = true;

    /**
     * Event dispacher
     *
     * @var \Symfony\Component\EventDispatcher\EventDispatcher
     */
    protected $eventDispatcher;

    /**
     * @var \Iber\Phkey\Contracts\StreamableInterface
     */
    protected $stream;

    /**
     * Marks special char sequence
     *
     * Most unicode special characters that come through the terminal
     * are symbol sequences that need to be concatenated to get
     * the full key code
     *
     * @var bool
     */
    protected $charSequenceEnabled = false;

    /**
     * Used for marking when the escape key was pressed
     *
     * @var null|number
     */
    protected $escapePressedAt = null;

    /**
     * Stores a key code that is currently being parsed
     *
     * @var null
     */
    protected $currentKey = null;

    /**
     * Initializes the listener
     *
     * @param Matcher $matcher
     * @param EventDispatcher $eventDispatcher
     * @param StreamableInterface $stream
     */
    public function __construct(Matcher $matcher, EventDispatcher $eventDispatcher, StreamableInterface $stream)
    {
        $this->matcher = $matcher;
        $this->eventDispatcher = $eventDispatcher;
        $this->stream = $stream;
    }

    /**
     * @return EventDispatcher
     */
    public function getEventDispatcher()
    {
        return $this->eventDispatcher;
    }

    /**
     * Initiates the listener
     */
    public function start()
    {
        $this->eventDispatcher->addListener(
            'key:stop:listening',
            function () {
                $this->isListening = false;
            }
        );

        $this->resetListener()
            ->overrideReadlineHandler()
            ->runLoop()
            ->restoreReadlineHandler();
    }

    /**
     * Starts capturing keys
     *
     * Reads every single character in STDIN one by one
     * and figures out which keys were pressed
     *
     * Supports special keys such as arrows, function keys etc.
     * by combining separate characters into unicode characters
     *
     * @todo add multi-language support
     *       or not since even js doesn't support it(?)
     *       '/[\x00-\x7F]/' basic latin range including special keys
     *
     * @return $this
     */
    protected function runLoop()
    {
        while ($this->isListening) {
            $this->stream->select();

            // special key sequence has started
            if (true === $this->charSequenceEnabled && null !== $this->escapePressedAt) {
                if ($this->escapeKeyHasExpired()) {
                    $this->disableKeySequence();

                    $this->setCurrentKey($this->matcher->getKey($this->matcher->getEscapeKey()));

                    // run key listeners, if one of the key callbacks returns true
                    // break the loop and restore default terminal functionality
                    $this->dispatchKeyPressEvents($this->getCurrentKey());
                }
            }

            // proceed if the stream isn't empty
            if ($this->stream->isAvailable()) {
                $char = $this->stream->getChar();

                // start of the special key sequence
                // mark the sequence start and setup the timer for the escape key
                if ($this->matcher->getEscapeKey() === $char) {
                    $this->enableKeySequence();
                    $this->setCurrentKey($char);
                } else {
                    $this->escapePressedAt = null;

                    if ($this->charSequenceEnabled) {
                        // if special key was pressed, concatenate the current
                        // escape character with the next characters that come
                        // in through the stream
                        $this->concatCurrentKey($char);

                        $mapped = $this->matcher->getKey($this->getCurrentKey());
                        // check and get which arrow key was pressed
                        if ($mapped) {
                            $this->setCurrentKey($mapped);
                            $this->disableKeySequence();
                        } else {
                            // we skip this iteration because the sequence isn't
                            // finished yet and we don't need to run the key
                            // listeners
                            continue;
                        }
                    } else {
                        // something out of basic latin charset so we ignore it
                        if ($this->matcher->isBasicLatin($char)) {
                            continue;
                        } else {
                            // normal keyboard key was pressed
                            $this->setCurrentKey($this->matcher->getKey($char) ?: $char);
                        }
                    }

                    // run key listeners, if one of the key callbacks returns true
                    // break the loop and restore default terminal functionality
                    $this->dispatchKeyPressEvents($this->getCurrentKey());
                }
            }
        }

        return $this;
    }

    /**
     * @return bool
     */
    protected function escapeKeyHasExpired()
    {
        return (microtime(true) - $this->escapePressedAt) > self::ESCAPE_TIMEOUT;
    }

    /**
     * @return null
     */
    protected function getCurrentKey()
    {
        return $this->currentKey;
    }

    /**
     * @param $key
     * @return $this
     */
    protected function setCurrentKey($key)
    {
        $this->currentKey = $key;

        return $this;
    }

    /**
     * @param $char
     * @return $this
     */
    protected function concatCurrentKey($char)
    {
        $this->currentKey .= $char;

        return $this;
    }

    /**
     * Resets the listener properties
     */
    protected function resetListener()
    {
        $this->currentKey = null;
        $this->escapePressedAt = null;
        $this->charSequenceEnabled = false;
        $this->isListening = true;

        return $this;
    }

    /**
     * @return $this
     */
    protected function enableKeySequence()
    {
        $this->charSequenceEnabled = true;
        $this->escapePressedAt = microtime(true);

        return $this;
    }

    /**
     * @return $this
     */
    protected function disableKeySequence()
    {
        $this->escapePressedAt = null;
        $this->charSequenceEnabled = false;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function dispatchKeyPressEvents($key)
    {
        $event = new KeyPressEvent($key);
        $this->eventDispatcher->dispatch('key:press', $event);
        $this->eventDispatcher->dispatch('key:' . $key, $event);

        return $this;
    }

    /**
     * Overrides the default readline handler
     *
     * By default STDIN is read when the Enter key is pressed
     * we can override it by registering an empty closure as
     * the readline handler
     *
     * @return $this
     */
    protected function overrideReadlineHandler()
    {
        readline_callback_handler_install(
            '',
            function () {

            }
        );

        return $this;
    }

    /**
     * Restore the default readline handler
     *
     * @return $this
     */
    protected function restoreReadlineHandler()
    {
        readline_callback_handler_remove();

        return $this;
    }

}


<?php

namespace Iber\Phkey\Environment;

use Iber\Phkey\Contracts\StreamableInterface;
use Iber\Phkey\IO\Stream;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
 * Class Detector
 *
 * Detects the environment the library is ran upon and allows
 * automatic creation of the key listener based on different
 * environment implementations
 *
 * @package  Iber\Phkey\Environment
 */
class Detector
{

    /**
     * @var array
     */
    protected $implemented = [
        'unix'
    ];

    /**
     * @var string
     */
    protected $environment = 'unix';

    /**
     * @param null $environment
     */
    public function __construct($environment = null)
    {
        if (null === $environment) {
            $environment = PHP_OS;
        }

        $this->setEnvironment($environment);
    }

    /**
     * Sets the environment
     *
     * Falls back to Unix if the environment is not implemented
     *
     * @param $environment
     * @return $this
     *
     * @throws \UnexpectedValueException
     */
    public function setEnvironment($environment)
    {
        $environment = strtolower($environment);

        // list of implemented environments
        if (in_array($environment, $this->implemented)) {
            $this->environment = $environment;
            return $this;
        }

        // Linux, Unix and OSX don't differ much, therefore
        // we can use the same implementation
        if (preg_match('/(Linux|Unix|Darwin)/i', $environment, $match)) {
            $this->environment = 'unix';
            return $this;
        }

        // if the OS name begins with Win then
        // it's most likely Windows
        if ('WIN' === strtoupper(substr($environment, 0, 3))) {
            throw new \UnexpectedValueException("Windows isn't supported.");
        }

        // fallback to Unix
        $this->environment = 'unix';

        return $this;
    }

    /**
     * Environment getter
     *
     * @return string
     */
    public function getEnvironment()
    {
        return ucfirst($this->environment);
    }

    /**
     * Automatically instantiates the listener object with dependencies
     * based on environment
     *
     * @param EventDispatcher|null $eventDispatcher
     * @param StreamableInterface|null $stream
     *
     * @return \Iber\Phkey\Contracts\ListenableInterface
     */
    public function getListenerInstance(EventDispatcher $eventDispatcher = null, StreamableInterface $stream = null)
    {
        //instantiate the key matcher object
        $reflection = new \ReflectionClass('\\Iber\\Phkey\\Environment\\' . $this->getEnvironment() . '\\Matcher');
        $matcher = $reflection->newInstance();

        $reflection = new \ReflectionClass('\\Iber\\Phkey\\Environment\\' . $this->getEnvironment() . '\\Listener');

        if (null === $eventDispatcher) {
            $eventDispatcher = new EventDispatcher();
        }

        if (null === $stream) {
            $stream = new Stream();
        }

        $listener = $reflection->newInstance($matcher, $eventDispatcher, $stream);

        return $listener;
    }
}


<?php

namespace Iber\Phkey\IO;

use Iber\Phkey\Contracts\StreamableInterface;

/**
 * Class Stream
 *
 * Allows watching a stream and getting contents from it
 *
 * @package  Iber\Phkey\IO
 */
class Stream implements StreamableInterface
{
    /**
     * Selected stream
     *
     * @var resource
     */
    protected $stream;

    /**
     * Used for stream watching timeout
     *
     * @var integer
     */
    protected $timeout;

    /**
     * Selected stream
     *
     * @var
     */
    protected $selected = false;

    /**
     * @param $stream
     * @param int $timeout
     */
    public function __construct($stream = STDIN, $timeout = 35000)
    {
        $this->setStream($stream)
            ->setTimeout($timeout);
    }

    /**
     * Sets an active stream
     *
     * @param $stream
     * @throws \InvalidArgumentException
     * @return $this
     */
    public function setStream($stream)
    {
        if ('stream' !== get_resource_type($stream)) {
            throw new \InvalidArgumentException(
                'The passed value isn\'t a valid stream.'
            );
        }

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

    /**
     * Sets the stream_select() timeout
     *
     * @param $timeout
     * @return $this
     */
    public function setTimeout($timeout)
    {
        $this->timeout = $timeout;
        return $this;
    }

    /**
     * Starts watching the stream
     *
     * The timeout interval is required to
     * stop blocking the thread when there's no data
     */
    public function select()
    {
        $read = [$this->stream];
        $write = null;
        $except = null;

        // if the process gets a system signal before the timeout is ran
        // a warning is thrown therefore we need to suppress the warning
        $this->selected = @stream_select(
            $read,
            $write,
            $except,
            0,
            $this->timeout
        );

        return $this;
    }

    /**
     * Checks if the selected stream watcher has any content
     *
     * @return bool
     */
    public function isAvailable()
    {
        return false !== $this->selected && $this->selected > 0;
    }

    /**
     * Fetches a single character from the stream
     *
     * @return string
     */
    public function getChar()
    {
        return stream_get_contents($this->stream, 1);
    }
}


<?php

namespace TheCodingMachine\Yaco\Definition;

/**
 * This class represents a parameter.
 */
class ParameterDefinition implements DumpableInterface
{
    /**
     * The identifier of the instance in the container.
     *
     * @var string|null
     */
    private $identifier;

    /**
     * The value of the parameter.
     * It is expected to be a scalar or an array (or more generally anything that can be `var_export`ed).
     *
     * @var mixed
     */
    private $value;

    /**
     * Constructs an instance definition.
     *
     * @param string|null $identifier The identifier of the entry in the container. Can be null if the entry is anonymous (declared inline in other instances)
     * @param mixed      $value      The value of the parameter. It is expected to be a scalar or an array (or more generally anything that can be `var_export`ed)
     */
    public function __construct(?string $identifier, $value)
    {
        $this->identifier = $identifier;
        $this->value = $value;
    }

    /**
     * Returns the identifier of the instance.
     *
     * @return string
     */
    public function getIdentifier(): ?string
    {
        return $this->identifier;
    }

    /**
     * Returns the value of the parameter.
     *
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * Returns an InlineEntryInterface object representing the PHP code necessary to generate
     * the container entry.
     *
     * @param string $containerVariable The name of the variable that allows access to the container instance. For instance: "$container", or "$this->container"
     * @param array  $usedVariables     An array of variables that are already used and that should not be used when generating this code.
     *
     * @return InlineEntryInterface
     */
    public function toPhpCode(string $containerVariable, array $usedVariables = array()): InlineEntryInterface
    {
        return ValueUtils::dumpValue($this->value, $containerVariable, $usedVariables);
    }
}


<?php

namespace TheCodingMachine\Yaco\Definition;

/**
 * This class represents an instance declared using the "new" keyword followed by an optional list of
 * method calls and properties assignations.
 */
class ObjectDefinition implements DumpableInterface
{
    /**
     * The identifier of the instance in the container.
     *
     * @var string|null
     */
    private $identifier;

    /**
     * The fully qualified class name of this instance.
     *
     * @var string
     */
    private $className;

    /**
     * A list of arguments passed to the constructor.
     *
     * @var array Array of scalars or ReferenceInterface, or array mixing scalars, arrays, and ReferenceInterface
     */
    private $constructorArguments = array();

    /**
     * A list of actions to be executed (can be either a method call or a public property assignation).
     *
     * @var ActionInterface[]
     */
    private $actions = array();

    /**
     * Constructs an instance definition.
     *
     * @param string|null $identifier           The identifier of the instance in the container. Can be null if the instance is anonymous (declared inline of other instances)
     * @param string      $className            The fully qualified class name of this instance.
     * @param array       $constructorArguments A list of constructor arguments.
     */
    public function __construct(?string $identifier, string $className, array $constructorArguments = array())
    {
        $this->identifier = $identifier;
        $this->className = '\\'.ltrim($className, '\\');
        $this->constructorArguments = $constructorArguments;
    }

    /**
     * Returns the identifier of the instance.
     *
     * @return string
     */
    public function getIdentifier(): ?string
    {
        return $this->identifier;
    }

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

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

    /**
     * Adds an argument to the list of arguments to be passed to the constructor.
     *
     * @param mixed $argument
     *
     * @return self
     */
    public function addConstructorArgument($argument)
    {
        $this->constructorArguments[] = $argument;

        return $this;
    }

    /**
     * Adds a method call.
     *
     * @param string $methodName
     * @param array  $arguments
     *
     * @return MethodCall
     */
    public function addMethodCall($methodName, array $arguments = array())
    {
        $this->actions[] = $methodCall = new MethodCall($methodName, $arguments);

        return $methodCall;
    }

    /**
     * Adds a method call.
     *
     * @param string $propertyName
     * @param mixed  $value
     *
     * @return self
     */
    public function setProperty($propertyName, $value)
    {
        $this->actions[] = new PropertyAssignment($propertyName, $value);

        return $this;
    }

    /**
     * Returns an InlineEntryInterface object representing the PHP code necessary to generate
     * the container entry.
     *
     * @param string $containerVariable The name of the variable that allows access to the container instance. For instance: "$container", or "$this->container"
     * @param array  $usedVariables     An array of variables that are already used and that should not be used when generating this code.
     *
     * @return InlineEntryInterface
     */
    public function toPhpCode(string $containerVariable, array $usedVariables = array()): InlineEntryInterface
    {
        if ($this->identifier !== null) {
            $variableName = $this->getIdentifier();
        } else {
            $variableName = $this->className;
        }
        $variableName = VariableUtils::getNextAvailableVariableName(lcfirst($variableName), $usedVariables);

        $usedVariables[] = $variableName;
        $dumpedArguments = ValueUtils::dumpArguments($this->constructorArguments, $containerVariable, $usedVariables);
        $prependedCode = $dumpedArguments->getStatements();
        $code = sprintf("%s = new %s(%s);\n", $variableName, $this->className, $dumpedArguments->getExpression());
        foreach ($this->actions as $action) {
            $inlineCode = $action->toPhpCode($variableName, $containerVariable, $usedVariables);
            $code .= $inlineCode->getStatements()."\n";
            $usedVariables = $inlineCode->getUsedVariables();
        }

        return new InlineEntry($variableName, $prependedCode.$code, $usedVariables);
    }
}


<?php

declare (strict_types = 1);

namespace TheCodingMachine\Yaco\ServiceProvider;

use Interop\Container\Definition\DefinitionInterface;
use Interop\Container\ServiceProviderInterface;
use TheCodingMachine\ServiceProvider\Registry;
use TheCodingMachine\Yaco\Compiler;
use TheCodingMachine\Yaco\CompilerException;
use TheCodingMachine\Yaco\Definition\AliasDefinition;
use TheCodingMachine\Yaco\Definition\DumpableInterface;
use TheCodingMachine\Yaco\Definition\FactoryCallDefinition;
use TheCodingMachine\Yaco\Definition\Reference;
use TheCodingMachine\Yaco\DefinitionConverterInterface;

class ServiceProviderLoader
{
    /**
     * @var Compiler
     */
    private $compiler;

    /**
     * @var DefinitionConverterInterface
     */
    private $converter;

    /**
     * @param Compiler $compiler
     */
    public function __construct(Compiler $compiler, DefinitionConverterInterface $converter)
    {
        $this->compiler = $compiler;
        $this->converter = $converter;
    }

    /**
     * Loads the registry into the container.
     *
     * @param Registry $registry
     */
    public function loadFromRegistry(Registry $registry)
    {
        foreach ($registry as $key => $serviceProvider) {
            $this->loadServiceProviderFactories($serviceProvider, $key);
        }
        foreach ($registry as $key => $serviceProvider) {
            $this->loadServiceProviderExtensions($serviceProvider, $key);
        }
    }

    /**
     * @param ServiceProviderInterface $serviceProvider
     * @param int             $serviceProviderKey
     */
    private function loadServiceProviderFactories(ServiceProviderInterface $serviceProvider, int $serviceProviderKey)
    {
        $serviceFactories = $serviceProvider->getFactories();

        foreach ($serviceFactories as $serviceName => $callable) {
            $this->registerService($serviceName, $serviceProviderKey, $callable);
        }
    }

    /**
     * @param ServiceProviderInterface $serviceProvider
     * @param int             $serviceProviderKey
     */
    private function loadServiceProviderExtensions(ServiceProviderInterface $serviceProvider, int $serviceProviderKey)
    {
        $serviceExtensions = $serviceProvider->getExtensions();

        foreach ($serviceExtensions as $serviceName => $callable) {
            $this->extendService($serviceName, $serviceProviderKey, $callable);
        }
    }

    /**
     * @param string $serviceName
     * @param int $serviceProviderKey
     * @param callable $callable
     *
     */
    private function registerService(string $serviceName, int $serviceProviderKey, callable $callable)
    {
        $definition = $this->getCreateServiceDefinitionFromCallable($serviceName, $serviceName, $serviceProviderKey, $callable, new ContainerDefinition());

        $this->compiler->addDumpableDefinition($definition);
    }

    /**
     * @param string $serviceName
     * @param int $serviceProviderKey
     * @param callable $callable
     *
     * @throws CompilerException
     */
    private function extendService(string $serviceName, int $serviceProviderKey, callable $callable)
    {
        // TODO: check if $callable as a nullable previous argument!

        if (!$this->compiler->has($serviceName)) {
            // TODO: if $callable as NOT a nullable previous argument, throw an exception.
        }

        // The new service will be created under the name 'xxx_decorated_y'
        // The old service will be moved to the name 'xxx_decorated_y.inner'
        // This old service will be accessible through a callback represented by 'xxx_decorated_y.callbackwrapper'
        // The $servicename becomes an alias pointing to 'xxx_decorated_y'

        $previousDefinition = $this->compiler->getDumpableDefinition($serviceName);
        /*while ($previousDefinition instanceof Reference) {
            $previousDefinition = $this->compiler->getDumpableDefinition($previousDefinition->getAlias());
        }*/

        while ($previousDefinition instanceof AliasDefinition) {
            $previousDefinition = $this->compiler->getDumpableDefinition($previousDefinition->getAlias());
        }

        $oldServiceName = $serviceName;
        $decoratedServiceName = $this->getDecoratedServiceName($serviceName);
        //$innerName = $decoratedServiceName.'.inner';
        //$callbackWrapperName = $decoratedServiceName.'.callbackwrapper';

        // TODO: it would be way easier if we could simply rename a definition!!!
        if ($previousDefinition instanceof FactoryCallDefinition) {
            $innerDefinition = $previousDefinition->cloneWithoutIdentifier();
        } elseif ($previousDefinition instanceof CreateServiceFromRegistryDefinition) {
            $innerDefinition = $previousDefinition->cloneWithoutIdentifier();
        } elseif ($previousDefinition instanceof ExtendServiceFromRegistryDefinition) {
            $innerDefinition = $previousDefinition->cloneWithoutIdentifier();
        } else {
            // @codeCoverageIgnoreStart
            throw new CompilerException('Unable to rename definition from class '.get_class($previousDefinition));
            // @codeCoverageIgnoreEnd
        }

        $definition = $this->getExtendServiceDefinitionFromCallable($decoratedServiceName, $serviceName, $serviceProviderKey, $callable, new ContainerDefinition(), $innerDefinition);

        $this->compiler->addDumpableDefinition($definition);
        //$this->compiler->addDumpableDefinition($innerDefinition);
        //$this->compiler->addDumpableDefinition($callbackWrapperDefinition);
        $this->compiler->addDumpableDefinition(new AliasDefinition($oldServiceName, $decoratedServiceName));
    }

    /**
     * @param $serviceName
     * @param int                            $serviceProviderKey
     * @param callable                       $callable
     * @param ContainerDefinition            $containerDefinition
     *
     * @return DumpableInterface
     */
    private function getCreateServiceDefinitionFromCallable($decoratedServiceName, $serviceName, $serviceProviderKey, callable $callable, ContainerDefinition $containerDefinition): DumpableInterface
    {
        if ($callable instanceof DefinitionInterface) {
            return $this->converter->convert($decoratedServiceName, $callable);
        }
        if (is_array($callable) && is_string($callable[0])) {
            return new FactoryCallDefinition($decoratedServiceName, $callable[0], $callable[1], [$containerDefinition]);
        } elseif (is_string($callable) && strpos($callable, '::') !== false) {
            $pos = strpos($callable, '::');
            $className = substr($callable, 0, $pos);
            $methodName = substr($callable, $pos + 2);

            return new FactoryCallDefinition($decoratedServiceName, $className, $methodName, [$containerDefinition]);
        }

        // This is an object or a callback... we need to call the getServices method of the service provider at runtime.
        return new CreateServiceFromRegistryDefinition($decoratedServiceName, $serviceName, $serviceProviderKey);
    }

    /**
     * @param $serviceName
     * @param int                            $serviceProviderKey
     * @param callable                       $callable
     * @param ContainerDefinition            $containerDefinition
     * @param CallbackWrapperDefinition|null $previousDefinition
     *
     * @return DumpableInterface
     */
    private function getExtendServiceDefinitionFromCallable($decoratedServiceName, $serviceName, $serviceProviderKey, callable $callable, ContainerDefinition $containerDefinition, DumpableInterface $previousDefinition = null): DumpableInterface
    {
        if ($callable instanceof DefinitionInterface) {
            return $this->converter->convert($decoratedServiceName, $callable);
        }
        if (is_array($callable) && is_string($callable[0])) {
            return new FactoryCallDefinition($decoratedServiceName, $callable[0], $callable[1], [$containerDefinition, $previousDefinition]);
        } elseif (is_string($callable) && strpos($callable, '::') !== false) {
            $pos = strpos($callable, '::');
            $className = substr($callable, 0, $pos);
            $methodName = substr($callable, $pos + 2);

            return new FactoryCallDefinition($decoratedServiceName, $className, $methodName, [$containerDefinition, $previousDefinition]);
        }

        // This is an object or a callback... we need to call the getServices method of the service provider at runtime.
        return new ExtendServiceFromRegistryDefinition($decoratedServiceName, $serviceName, $serviceProviderKey, $previousDefinition);
    }

    /**
     * @param string $serviceName
     *
     * @return string
     */
    private function getDecoratedServiceName(string $serviceName): string
    {
        $counter = 1;
        while ($this->compiler->has($serviceName.'_decorated_'.$counter)) {
            ++$counter;
        }

        return $serviceName.'_decorated_'.$counter;
    }
}


<?php

namespace TheCodingMachine\Yaco\Definition;

class ValueUtils
{
    /**
     * Dumps values.
     *
     * @param mixed  $value
     * @param string $containerVariable
     * @param array  $usedVariables
     *
     * @return InlineEntryInterface
     */
    public static function dumpValue($value, $containerVariable, array $usedVariables): InlineEntryInterface
    {
        if (is_array($value)) {
            return self::dumpArray($value, $containerVariable, $usedVariables);
        } elseif ($value instanceof ReferenceInterface) {
            return self::dumpReference($value, $containerVariable, $usedVariables);
        } elseif ($value instanceof DumpableInterface) {
            return self::dumpDefinition($value, $containerVariable, $usedVariables);
        } elseif (is_object($value) || is_resource($value)) {
            throw new \RuntimeException('Unable to dump a container if a parameter is an object or a resource.');
        } else {
            return new InlineEntry(var_export($value, true), null, $usedVariables, false);
        }
    }

    public static function dumpArguments($argumentsValues, $containerVariable, array $usedVariables): InlineEntryInterface
    {
        $arguments = [];
        $prependedCode = [];
        foreach ($argumentsValues as $argument) {
            $inlineEntry = self::dumpValue($argument, $containerVariable, $usedVariables);
            $usedVariables = $inlineEntry->getUsedVariables();
            $arguments[] = $inlineEntry->getExpression();
            if (!empty($inlineEntry->getStatements())) {
                $prependedCode[] = $inlineEntry->getStatements();
            }
        }
        $argumentsCode = implode(', ', $arguments);
        $prependedCodeString = implode("\n", $prependedCode);

        return new InlineEntry($argumentsCode, $prependedCodeString, $usedVariables);
    }

    private static function dumpArray(array $value, $containerVariable, array $usedVariables): InlineEntryInterface
    {
        $code = array();
        $prependCode = array();
        foreach ($value as $k => $v) {
            $value = self::dumpValue($v, $containerVariable, $usedVariables);

            if ($value->getStatements()) {
                $prependCode[] = $value->getStatements();
            }
            $usedVariables = $value->getUsedVariables();

            $code[] = sprintf('%s => %s', var_export($k, true), $value->getExpression());
        }

        return new InlineEntry(sprintf('array(%s)', implode(', ', $code)), implode("\n", $prependCode), $usedVariables);
    }

    private static function dumpDefinition(DumpableInterface $definition, $containerVariable, array $usedVariables): InlineEntryInterface
    {
        // If the identifier is null, we must inline the definition.
        if ($definition->getIdentifier() === null) {
            return $definition->toPhpCode($containerVariable, $usedVariables);
        } else {
            return self::dumpReference(new Reference($definition->getIdentifier()), $containerVariable, $usedVariables);
        }
    }

    private static function dumpReference(ReferenceInterface $reference, $containerVariable, array $usedVariables): InlineEntryInterface
    {
        return $reference->toPhpCode($containerVariable, $usedVariables);
    }
}


<?php

namespace TheCodingMachine\Yaco\Definition;

/**
 * Represents an assignment of a property.
 */
class PropertyAssignment implements ActionInterface
{
    /**
     * The name of the property.
     *
     * @var string
     */
    private $propertyName;

    /**
     * The value to assign to the property.
     *
     * @var mixed
     */
    private $value;

    /**
     * @param string $propertyName
     * @param mixed  $value
     */
    public function __construct($propertyName, $value)
    {
        $this->propertyName = $propertyName;
        $this->value = $value;
    }

    /**
     * Generates PHP code for the line.
     *
     * @param string $variableName
     * @param string $containerVariable
     * @param array  $usedVariables
     *
     * @return InlineEntryInterface
     */
    public function toPhpCode($variableName, $containerVariable, array $usedVariables): InlineEntryInterface
    {
        $inlineEntry = ValueUtils::dumpValue($this->value, $containerVariable, $usedVariables);
        $codeLine = sprintf('%s->%s = %s;', $variableName, $this->propertyName, $inlineEntry->getExpression());

        return new InlineEntry('', $inlineEntry->getStatements().$codeLine, $inlineEntry->getUsedVariables());
    }
}


<?php

namespace TheCodingMachine\Yaco\ServiceProvider;

use TheCodingMachine\Yaco\Definition\DumpableInterface;
use TheCodingMachine\Yaco\Definition\InlineEntry;
use TheCodingMachine\Yaco\Definition\InlineEntryInterface;

/**
 * Fetches a service from the service-providers registry.
 */
class ExtendServiceFromRegistryDefinition implements DumpableInterface
{
    /**
     * The identifier of the instance in the container.
     *
     * @var string|null
     */
    private $identifier;

    /**
     * The key of the service provider in the registry.
     *
     * @var int
     */
    private $serviceProviderKey;

    /**
     * @var DumpableInterface|null
     */
    private $previousDefinition;

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

    /**
     * @param string|null                    $identifier
     * @param string                         $serviceName
     * @param int                            $serviceProviderKey
     * @param DumpableInterface|null $previousDefinition
     */
    public function __construct(?string $identifier, string $serviceName, int $serviceProviderKey, DumpableInterface $previousDefinition = null)
    {
        $this->identifier = $identifier;
        $this->serviceName = $serviceName;
        $this->serviceProviderKey = $serviceProviderKey;
        $this->previousDefinition = $previousDefinition;
    }

    /**
     * Returns the identifier for this object in the container.
     * If null, classes consuming this definition should assume the definition must be inlined.
     *
     * @return string|null
     */
    public function getIdentifier(): ?string
    {
        return $this->identifier;
    }

    /**
     * Returns an InlineEntryInterface object representing the PHP code necessary to generate
     * the container entry.
     *
     * @param string $containerVariable The name of the variable that allows access to the container instance. For instance: "$container", or "$this->container"
     * @param array  $usedVariables     An array of variables that are already used and that should not be used when generating this code.
     *
     * @return InlineEntryInterface
     */
    public function toPhpCode(string $containerVariable, array $usedVariables = array()): InlineEntryInterface
    {
        $previousCode = '';
        if ($this->previousDefinition) {
            $previousCode = ', '.$this->previousDefinition->toPhpCode($containerVariable, $usedVariables)->getExpression();
        } else {
            $previousCode = ', null';
        }
        $code = sprintf('$this->registry->extendService(%s, %s, $this->delegateLookupContainer%s)', var_export($this->serviceProviderKey, true),
            var_export($this->serviceName, true), $previousCode);

        return new InlineEntry($code, null, $usedVariables);
    }

    public function cloneWithoutIdentifier(): self
    {
        return new self(null, $this->serviceName, $this->serviceProviderKey, $this->previousDefinition);
    }
}


<?php
/**
 * This file is part of potomoco.
 *
 * (c) TrekkSoft AG (www.trekksoft.com)
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @author Philippe Gerber <philippe@bigwhoop.ch>
 */
namespace TrekkSoft\Potomoco;

/**
 * Parses a .PO file into an array of messages
 *
 * @link http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
 */
class Parser implements ParserInterface
{
    const TOKEN_MSGID         = 'msgid';
    const TOKEN_MSGID_PLURAL  = 'msgid_plural';
    const TOKEN_MSGSTR        = 'msgstr';
    const TOKEN_MSGSTR_PLURAL = 'msgstr[';
    const TOKEN_MULTILINE_STR = '"';
    const TOKEN_CONTEXT       = 'msgctxt';
    const TOKEN_FLAG          = '#,';
    const TOKEN_REFERENCE     = '#:';

    /**
     * @var array
     */
    static private $tokenParsingMap = array(
        self::TOKEN_MSGID_PLURAL  => 'parseTranslatedStringPlural',
        self::TOKEN_MSGID         => 'parseTranslatedString',
        self::TOKEN_MSGSTR_PLURAL => 'parseUntranslatedStringPlural',
        self::TOKEN_MSGSTR        => 'parseUntranslatedString',
        self::TOKEN_MULTILINE_STR => 'parseMultiLineString',
        //self::TOKEN_CONTEXT       => 'parseContext',
        //self::TOKEN_FLAG          => 'parseFlag',
        //self::TOKEN_REFERENCE     => 'parseReference',
    );

    /**
     * @var Message
     */
    private $currentMessage;

    /**
     * @var array
     */
    private $messages = array();

    /**
     * @var string
     */
    private $multiLineState = '';

    /**
     * @param string $poPath
     * @return array        Array of Message objects
     * @throws \InvalidArgumentException
     */
    public function parse($poPath)
    {
        if (!file_exists($poPath)) {
            throw new \InvalidArgumentException("No .po file at '$poPath'.");
        }

        $contents = file_get_contents($poPath);

        // Normalize newlines
        $contents = str_replace(array("\r\n", "\r"), array("\n", "\n"), $contents);

        // Make sure the kitchen is clean :)
        $this->reset();

        // Parse each line
        foreach (explode("\n", $contents) as $line) {
            $this->parseLine($line);
        }

        // Make sure we add the last message (if any is available)
        $this->addCurrentMessage();

        return $this->messages;
    }

    /**
     * @return Parser
     */
    private function reset()
    {
        $this->messages = array();
        $this->currentMessage = new Message();

        return $this;
    }

    /**
     * @param string $line
     */
    private function parseLine($line)
    {
        $line = trim($line);

        foreach (self::$tokenParsingMap as $token => $methodName) {
            if (0 === strpos($line, $token)) {
                $this->$methodName($line);
                break;
            }
        }
    }

    /**
     * Checks whether the current message can be added to the message stack
     */
    private function addCurrentMessage()
    {
        if ($this->currentMessage->hasTranslation()) {
            $this->messages[] = $this->currentMessage;
            $this->currentMessage = new Message();
        }
    }

    /**
     * @param string $line
     */
    private function parseTranslatedString($line)
    {
        $this->addCurrentMessage();

        $chunks = explode(' ', $line, 2);

        if (isset($chunks[1])) {
            $this->currentMessage->msgId = $this->normalizeString($chunks[1]);
            $this->multiLineState = self::TOKEN_MSGID;
        }
    }

    /**
     * @param string $line
     */
    private function parseTranslatedStringPlural($line)
    {
        $chunks = explode(' ', $line, 2);

        if (isset($chunks[1])) {
            $this->currentMessage->msgIdPlural = $this->normalizeString($chunks[1]);
        }
    }

    /**
     * @param string $line
     */
    private function parseUntranslatedString($line)
    {
        $chunks = explode(' ', $line, 2);

        if (isset($chunks[1])) {
            $this->currentMessage->msgStr = $this->normalizeString($chunks[1]);
            $this->multiLineState = self::TOKEN_MSGSTR;
        }
    }

    /**
     * @param string $line
     */
    private function parseUntranslatedStringPlural($line)
    {
        $chunks = explode(' ', $line, 2);

        if (count($chunks) != 2) {
            return;
        }

        list($token, $data) = $chunks;

        $matches = array();
        if (preg_match('|msgstr\[([0-9]+)\]|i', $token, $matches)) {
            $idx = $matches[1];
            $this->currentMessage->msgStrPlural[$idx] = $this->normalizeString($data);
        }
    }

    /**
     * @param string $line
     */
    private function parseMultiLineString($line)
    {
        $line = $this->normalizeString($line);

        switch ($this->multiLineState) {
            case self::TOKEN_MSGID  :   $this->currentMessage->msgId  .= $line;   break;
            case self::TOKEN_MSGSTR :   $this->currentMessage->msgStr .= $line;   break;
        }
    }

    /**
     * @param string $string
     * @return string
     */
    private function normalizeString($string)
    {
        if (empty($string)) {
            return '';
        }

        // Strip leading "
        if ($string[0] === '"') {
            $string = mb_substr($string, 1);
        }

        // Strip trailing "
        if ('"' === mb_substr($string, -1, 1)) {
            $string = mb_substr($string, 0, -1);
        }

        $string = stripslashes($string);

        return $string;
    }
}