<?php

namespace Proengsoft\JsValidation\Javascript;

use Exception;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Facades\View;
use Proengsoft\JsValidation\Exceptions\PropertyNotFoundException;

class JavascriptValidator implements Arrayable
{
    /**
     * Registered validator instance.
     *
     * @var \Proengsoft\JsValidation\Javascript\ValidatorHandler
     */
    protected $validator;

    /**
     * Selector used in javascript generation.
     *
     * @var string
     */
    protected $selector;

    /**
     * View that renders Javascript.
     *
     * @var
     */
    protected $view;

    /**
     * Enable or disable remote validations.
     *
     * @var bool
     */
    protected $remote;

    /**
     * 'ignore' option for jQuery Validation Plugin.
     *
     * @var string
     */
    protected $ignore;

    /**
     * @param \Proengsoft\JsValidation\Javascript\ValidatorHandler $validator
     * @param array $options
     */
    public function __construct(ValidatorHandler $validator, $options = [])
    {
        $this->validator = $validator;
        $this->setDefaults($options);
    }

    /**
     * Set default parameters.
     *
     * @param $options
     * @return void
     */
    protected function setDefaults($options)
    {
        $this->selector = empty($options['selector']) ? 'form' : $options['selector'];
        $this->view = empty($options['view']) ? 'jsvalidation::bootstrap' : $options['view'];
        $this->remote = isset($options['remote']) ? $options['remote'] : true;
    }

    /**
     * Render the specified view with validator data.
     *
     * @param null|\Illuminate\Contracts\View\View|string $view
     * @param null|string $selector
     * @return string
     */
    public function render($view = null, $selector = null)
    {
        $this->view($view);
        $this->selector($selector);

        return View::make($this->view, ['validator' => $this->getViewData()])
            ->render();
    }

    /**
     * Get the view data as an array.
     *
     * @return array
     */
    public function toArray()
    {
        return $this->getViewData();
    }

    /**
     * Get the string resulting of render default view.
     *
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->render();
        } catch (Exception $exception) {
            return trigger_error($exception->__toString(), E_USER_ERROR);
        }
    }

    /**
     * Gets value from view data.
     *
     * @param $name
     * @return string
     *
     * @throws \Proengsoft\JsValidation\Exceptions\PropertyNotFoundException
     */
    public function __get($name)
    {
        $data = $this->getViewData();
        if (! array_key_exists($name, $data)) {
            throw new PropertyNotFoundException($name, get_class());
        }

        return $data[$name];
    }

    /**
     * Gets view data.
     *
     * @return array
     */
    protected function getViewData()
    {
        $this->validator->setRemote($this->remote);
        $data = $this->validator->validationData();
        $data['selector'] = $this->selector;

        if (! is_null($this->ignore)) {
            $data['ignore'] = $this->ignore;
        }

        return $data;
    }

    /**
     * Set the form selector to validate.
     *
     * @param string $selector
     * @deprecated
     */
    public function setSelector($selector)
    {
        $this->selector = $selector;
    }

    /**
     * Set the form selector to validate.
     *
     * @param string $selector
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function selector($selector)
    {
        $this->selector = is_null($selector) ? $this->selector : $selector;

        return $this;
    }

    /**
     * Set the input selector to ignore for validation.
     *
     * @param string $ignore
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function ignore($ignore)
    {
        $this->ignore = $ignore;

        return $this;
    }

    /**
     * Set the view to render Javascript Validations.
     *
     * @param null|\Illuminate\Contracts\View\View|string $view
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function view($view)
    {
        $this->view = is_null($view) ? $this->view : $view;

        return $this;
    }

    /**
     * Enables or disables remote validations.
     *
     * @param null|bool $enabled
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function remote($enabled = true)
    {
        $this->remote = $enabled;

        return $this;
    }

    /**
     * Validate Conditional Validations using Ajax in specified fields.
     *
     * @param string $attribute
     * @param string|array $rules
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function sometimes($attribute, $rules)
    {
        $this->validator->sometimes($attribute, $rules);

        return $this;
    }
}


<?php

namespace Proengsoft\JsValidation\Support;

use Illuminate\Validation\Validator as BaseValidator;

class DelegatedValidator
{
    use AccessProtectedTrait;

    /**
     * The Validator resolved instance.
     *
     * @var \Illuminate\Validation\Validator
     */
    protected $validator;

    /**
     * Validation rule parser instance.
     *
     * @var \Proengsoft\JsValidation\Support\ValidationRuleParserProxy
     */
    protected $ruleParser;

    /**
     * Closure to invoke non accessible Validator methods.
     *
     * @var \Closure
     */
    protected $validatorMethod;

    /**
     * DelegatedValidator constructor.
     *
     * @param \Illuminate\Validation\Validator $validator
     * @param \Proengsoft\JsValidation\Support\ValidationRuleParserProxy $ruleParser
     */
    public function __construct(BaseValidator $validator, ValidationRuleParserProxy $ruleParser)
    {
        $this->validator = $validator;
        $this->ruleParser = $ruleParser;
        $this->validatorMethod = $this->createProtectedCaller($validator);
    }

    /**
     * Call validator method.
     *
     * @param string $method
     * @param array $args
     * @return mixed
     */
    private function callValidator($method, $args = [])
    {
        return $this->callProtected($this->validatorMethod, $method, $args);
    }

    /**
     * Get current \Illuminate\Validation\Validator instance.
     *
     * @return \Illuminate\Validation\Validator
     */
    public function getValidator()
    {
        return $this->validator;
    }

    /**
     * Get the data under validation.
     *
     * @return array
     */
    public function getData()
    {
        return $this->validator->getData();
    }

    /**
     * Set the data under validation.
     *
     * @param array
     */
    public function setData($data)
    {
        $this->validator->setData($data);
    }

    /**
     * Get the validation rules.
     *
     * @return array
     */
    public function getRules()
    {
        return $this->validator->getRules();
    }

    /**
     * Determine if a given rule implies the attribute is required.
     *
     * @param string $rule
     * @return bool
     */
    public function isImplicit($rule)
    {
        return $this->callValidator('isImplicit', [$rule]);
    }

    /**
     * Replace all error message place-holders with actual values.
     *
     * @param string $message
     * @param string $attribute
     * @param string $rule
     * @param array $parameters
     * @return string
     */
    public function makeReplacements($message, $attribute, $rule, $parameters)
    {
        if (is_object($rule)) {
            $rule = get_class($rule);
        }

        return $this->callValidator('makeReplacements', [$message, $attribute, $rule, $parameters]);
    }

    /**
     * Determine if the given attribute has a rule in the given set.
     *
     * @param string $attribute
     * @param string|array $rules
     * @return bool
     */
    public function hasRule($attribute, $rules)
    {
        return $this->callValidator('hasRule', [$attribute, $rules]);
    }

    /**
     * Get the validation message for an attribute and rule.
     *
     * @param string $attribute
     * @param string $rule
     * @return string
     */
    public function getMessage($attribute, $rule)
    {
        if (is_object($rule)) {
            $rule = get_class($rule);
        }

        return $this->callValidator('getMessage', [$attribute, $rule]);
    }

    /**
     * Extract the rule name and parameters from a rule.
     *
     * @param array|string $rules
     * @return array
     */
    public function parseRule($rules)
    {
        return $this->ruleParser->parse($rules);
    }

    /**
     * Explode the rules into an array of rules.
     *
     * @param string|array $rules
     * @return array
     */
    public function explodeRules($rules)
    {
        return $this->callValidator('explodeRules', [$rules]);
    }

    /**
     * Add conditions to a given field based on a Closure.
     *
     * @param string $attribute
     * @param string|array $rules
     * @param callable $callback
     * @return void
     */
    public function sometimes($attribute, $rules, callable $callback)
    {
        $this->validator->sometimes($attribute, $rules, $callback);
    }

    /**
     * Delegate method calls to validator instance.
     *
     * @param $method
     * @param $params
     * @return mixed
     */
    public function __call($method, $params)
    {
        $arrCaller = [$this->validator, $method];

        return call_user_func_array($arrCaller, $params);
    }
}


<?php

namespace Proengsoft\JsValidation\Javascript;

trait JavascriptRulesTrait
{
    /**
     * Handles multidimensional attribute names.
     *
     * @param string $attribute
     * @return string
     */
    abstract protected function getAttributeName($attribute);

    /**
     * Parse named parameters to $key => $value items.
     *
     * @param array $parameters
     * @return array
     */
    abstract public function parseNamedParameters($parameters);

    /**
     * Confirmed rule is applied to confirmed attribute.
     *
     * @param $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleConfirmed($attribute, array $parameters)
    {
        $parameters[0] = $this->getAttributeName($attribute);
        $attribute = "{$attribute}_confirmation";

        return [$attribute, $parameters];
    }

    /**
     * Returns Javascript parameters for After rule.
     *
     * @param $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleAfter($attribute, array $parameters)
    {
        if (! ($date = strtotime($parameters[0]))) {
            $date = $this->getAttributeName($parameters[0]);
        }

        return [$attribute, [$date]];
    }

    /**
     * Returns Javascript parameters for Before rule.
     *
     * @param $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleBefore($attribute, array $parameters)
    {
        return $this->ruleAfter($attribute, $parameters);
    }

    /**
     * Validate that two attributes match.
     *
     * @param string $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleSame($attribute, array $parameters)
    {
        $other = $this->getAttributeName($parameters[0]);

        return [$attribute, [$other]];
    }

    /**
     * Validate that an attribute is different from another attribute.
     *
     * @param string $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleDifferent($attribute, array $parameters)
    {
        return $this->ruleSame($attribute, $parameters);
    }

    /**
     * Validate that an attribute exists when any other attribute exists.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleRequiredWith($attribute, array $parameters)
    {
        $parameters = array_map([$this, 'getAttributeName'], $parameters);

        return [$attribute, $parameters];
    }

    /**
     * Validate that an attribute exists when all other attributes exists.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleRequiredWithAll($attribute, array $parameters)
    {
        return $this->ruleRequiredWith($attribute, $parameters);
    }

    /**
     * Validate that an attribute exists when another attribute does not.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleRequiredWithout($attribute, array $parameters)
    {
        return $this->ruleRequiredWith($attribute, $parameters);
    }

    /**
     * Validate that an attribute exists when all other attributes do not.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleRequiredWithoutAll($attribute, array $parameters)
    {
        return $this->ruleRequiredWith($attribute, $parameters);
    }

    /**
     * Validate that an attribute exists when another attribute has a given value.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleRequiredIf($attribute, array $parameters)
    {
        $parameters[0] = $this->getAttributeName($parameters[0]);

        return [$attribute, $parameters];
    }

    /**
     * Validate that an attribute exists when another attribute does not have a given value.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleRequiredUnless($attribute, array $parameters)
    {
        return $this->ruleRequiredIf($attribute, $parameters);
    }

    /**
     * Validate that the values of an attribute is in another attribute.
     *
     * @param string $attribute
     * @param mixed $parameters
     * @return array
     */
    protected function ruleInArray($attribute, array $parameters)
    {
        return $this->ruleRequiredIf($attribute, $parameters);
    }

    /**
     * Validate the dimensions of an image matches the given values.
     *
     * @param string $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleDimensions($attribute, $parameters)
    {
        $parameters = $this->parseNamedParameters($parameters);

        return [$attribute, $parameters];
    }

    /**
     * Validate an attribute is unique among other values.
     *
     * @param string $attribute
     * @param array $parameters
     * @return array
     */
    protected function ruleDistinct($attribute, array $parameters)
    {
        $parameters[0] = $attribute;

        return $this->ruleRequiredIf($attribute, $parameters);
    }
}


<?php

namespace Proengsoft\JsValidation\Javascript;

use Proengsoft\JsValidation\Support\DelegatedValidator;
use Proengsoft\JsValidation\Support\UseDelegatedValidatorTrait;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class MessageParser
{
    use UseDelegatedValidatorTrait;

    /**
     * Create a new JsValidation instance.
     *
     * @param \Proengsoft\JsValidation\Support\DelegatedValidator $validator
     */
    public function __construct(DelegatedValidator $validator)
    {
        $this->validator = $validator;
    }

    /**
     *  Replace javascript error message place-holders with actual values.
     *
     * @param string $attribute
     * @param string $rule
     * @param array  $parameters
     * @return mixed
     */
    public function getMessage($attribute, $rule, $parameters)
    {
        $data = $this->fakeValidationData($attribute, $rule, $parameters);

        $message = $this->validator->getMessage($attribute, $rule);
        $message = $this->validator->makeReplacements($message, $attribute, $rule, $parameters);

        $this->validator->setData($data);

        return $message;
    }

    /**
     * Creates fake data needed to parse messages
     * Returns original data.
     *
     * @param string $attribute
     * @param string $rule
     * @param $parameters
     * @return array
     */
    protected function fakeValidationData($attribute, $rule, $parameters)
    {
        $data = $this->validator->getData();

        $this->fakeFileData($data, $attribute);
        $this->fakeRequiredIfData($data, $rule, $parameters);

        return $data;
    }

    /**
     * Generate fake data to get RequiredIf message.
     *
     * @param $data
     * @param $rule
     * @param $parameters
     * @return void
     */
    private function fakeRequiredIfData($data, $rule, $parameters)
    {
        if ($rule !== 'RequiredIf') {
            return;
        }

        $newData = $data;
        $newData[$parameters[0]] = $parameters[1];
        $this->validator->setData($newData);
    }

    /**
     * Generate fake data to get file type messages.
     *
     * @param $data
     * @param $attribute
     * @return void
     */
    private function fakeFileData($data, $attribute)
    {
        if (! $this->validator->hasRule($attribute, ['Mimes', 'Image'])) {
            return;
        }

        $newFiles = $data;
        $newFiles[$attribute] = $this->createUploadedFile();
        $this->validator->setData($newFiles);
    }

    /**
     * Create fake UploadedFile to generate file messages.
     *
     * @return UploadedFile
     */
    protected function createUploadedFile()
    {
        return new UploadedFile('fakefile', 'fakefile', null, null, UPLOAD_ERR_NO_FILE, true);
    }
}


<?php

namespace Proengsoft\JsValidation\Remote;

use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\ValidationRuleParser;
use Illuminate\Validation\Validator as BaseValidator;
use Proengsoft\JsValidation\Support\AccessProtectedTrait;
use Proengsoft\JsValidation\Support\RuleListTrait;

class Validator
{
    use AccessProtectedTrait;
    use RuleListTrait;

    /**
     * Validator extension name.
     */
    const EXTENSION_NAME = 'jsvalidation';

    /**
     * @var \Illuminate\Validation\Validator
     */
    protected $validator;

    /**
     * RemoteValidator constructor.
     *
     * @param \Illuminate\Validation\Validator $validator
     */
    public function __construct(BaseValidator $validator)
    {
        $this->validator = $validator;
    }

    /**
     * Validate request.
     *
     * @param $field
     * @param $parameters
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function validate($field, $parameters = [])
    {
        $attribute = $this->parseAttributeName($field);
        $validationParams = $this->parseParameters($parameters);
        $validationResult = $this->validateJsRemoteRequest($attribute, $validationParams);

        $this->throwValidationException($validationResult, $this->validator);
    }

    /**
     * Throw the failed validation exception.
     *
     * @param mixed $result
     * @param \Illuminate\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException|\Illuminate\Http\Exceptions\HttpResponseException
     */
    protected function throwValidationException($result, $validator)
    {
        $response = new JsonResponse($result, 200);

        if ($result !== true && class_exists(ValidationException::class)) {
            throw new ValidationException($validator, $response);
        }

        throw new HttpResponseException($response);
    }

    /**
     *  Parse Validation input request data.
     *
     * @param $data
     * @return array
     */
    protected function parseAttributeName($data)
    {
        parse_str($data, $attrParts);
        $attrParts = is_null($attrParts) ? [] : $attrParts;
        $newAttr = array_keys(array_dot($attrParts));

        return array_pop($newAttr);
    }

    /**
     *  Parse Validation parameters.
     *
     * @param $parameters
     * @return array
     */
    protected function parseParameters($parameters)
    {
        $newParams = ['validate_all' => false];
        if (isset($parameters[0])) {
            $newParams['validate_all'] = ($parameters[0] === 'true') ? true : false;
        }

        return $newParams;
    }

    /**
     * Validate remote Javascript Validations.
     *
     * @param $attribute
     * @param array $parameters
     * @return array|bool
     */
    protected function validateJsRemoteRequest($attribute, $parameters)
    {
        $this->setRemoteValidation($attribute, $parameters['validate_all']);

        $validator = $this->validator;
        if ($validator->passes()) {
            return true;
        }

        return $validator->messages()->get($attribute);
    }

    /**
     * Sets data for validate remote rules.
     *
     * @param $attribute
     * @param bool $validateAll
     * @return void
     */
    protected function setRemoteValidation($attribute, $validateAll = false)
    {
        $validator = $this->validator;
        $rules = $validator->getRules();
        $rules = isset($rules[$attribute]) ? $rules[$attribute] : [];
        if (in_array('no_js_validation', $rules)) {
            $validator->setRules([$attribute => []]);

            return;
        }
        if (! $validateAll) {
            $rules = $this->purgeNonRemoteRules($rules, $validator);
        }
        $validator->setRules([$attribute => $rules]);
    }

    /**
     * Remove rules that should not be validated remotely.
     *
     * @param $rules
     * @param BaseValidator $validator
     * @return mixed
     */
    protected function purgeNonRemoteRules($rules, $validator)
    {
        $protectedValidator = $this->createProtectedCaller($validator);

        foreach ($rules as $i => $rule) {
            $parsedRule = ValidationRuleParser::parse([$rule]);
            if (! $this->isRemoteRule($parsedRule[0])) {
                unset($rules[$i]);
            }
        }

        return $rules;
    }
}


<?php

namespace Proengsoft\JsValidation\Support;

use Closure;

trait AccessProtectedTrait
{
    /**
     * Create closure to call inaccessible method.
     *
     * @param $instance
     * @return \Closure
     */
    protected function createProtectedCaller($instance)
    {
        $closure = function ($method, $args) {
            $callable = [$this, $method];

            return call_user_func_array($callable, $args);
        };

        return $closure->bindTo($instance, $instance);
    }

    /**
     * Gets inaccessible property.
     *
     * @param $instance
     * @param $property
     * @return \Closure
     */
    protected function getProtected($instance, $property)
    {
        $closure = function ($property) {
            return $this->$property;
        };
        $callback = $closure->bindTo($instance, $instance);

        return $callback($property);
    }

    /**
     * Calls inaccessible method.
     *
     * @param object|\Closure $instance
     * @param $method
     * @param $args
     * @return mixed
     */
    protected function callProtected($instance, $method, $args = [])
    {
        if (! ($instance instanceof Closure)) {
            $instance = $this->createProtectedCaller($instance);
        }

        return call_user_func($instance, $method, $args);
    }
}


<?php

namespace Proengsoft\JsValidation\Javascript;

use Proengsoft\JsValidation\Support\DelegatedValidator;
use Proengsoft\JsValidation\Support\UseDelegatedValidatorTrait;

class ValidatorHandler
{
    use UseDelegatedValidatorTrait;

    /**
     * Rule used to disable validations.
     *
     * @const string
     */
    const JSVALIDATION_DISABLE = 'NoJsValidation';

    /**
     * @var RuleParser
     */
    protected $rules;
    /**
     * @var MessageParser
     */
    protected $messages;

    /**
     * @var bool
     */
    protected $remote = true;

    /**
     * Create a new JsValidation instance.
     *
     * @param RuleParser $rules
     * @param MessageParser $messages
     */
    public function __construct(RuleParser $rules, MessageParser $messages)
    {
        $this->rules = $rules;
        $this->messages = $messages;
        $this->validator = $rules->getDelegatedValidator();
    }

    /**
     * Sets delegated Validator instance.
     *
     * @param \Proengsoft\JsValidation\Support\DelegatedValidator $validator
     * @return void
     */
    public function setDelegatedValidator(DelegatedValidator $validator)
    {
        $this->validator = $validator;
        $this->rules->setDelegatedValidator($validator);
        $this->messages->setDelegatedValidator($validator);
    }

    /**
     * Enable or disables remote validations.
     *
     * @param bool $enabled
     * @return void
     */
    public function setRemote($enabled)
    {
        $this->remote = $enabled;
    }

    /**
     * Generate Javascript Validations.
     *
     * @return array
     */
    protected function generateJavascriptValidations()
    {
        $jsValidations = [];

        foreach ($this->validator->getRules() as $attribute => $rules) {
            if (! $this->jsValidationEnabled($attribute)) {
                continue;
            }

            $newRules = $this->jsConvertRules($attribute, $rules, $this->remote);
            $jsValidations = array_merge($jsValidations, $newRules);
        }

        return $jsValidations;
    }

    /**
     * Make Laravel Validations compatible with JQuery Validation Plugin.
     *
     * @param $attribute
     * @param $rules
     * @param bool $includeRemote
     * @return array
     */
    protected function jsConvertRules($attribute, $rules, $includeRemote)
    {
        $jsRules = [];
        foreach ($rules as $rawRule) {
            list($rule, $parameters) = $this->validator->parseRule($rawRule);
            list($jsAttribute, $jsRule, $jsParams) = $this->rules->getRule($attribute, $rule, $parameters, $rawRule);
            if ($this->isValidatable($jsRule, $includeRemote)) {
                $jsRules[$jsAttribute][$jsRule][] = [$rule, $jsParams,
                    $this->messages->getMessage($attribute, $rule, $parameters),
                    $this->validator->isImplicit($rule),
                ];
            }
        }

        return $jsRules;
    }

    /**
     * Check if rule should be validated with javascript.
     *
     * @param $jsRule
     * @param bool $includeRemote
     * @return bool
     */
    protected function isValidatable($jsRule, $includeRemote)
    {
        return $jsRule && ($includeRemote || $jsRule !== RuleParser::REMOTE_RULE);
    }

    /**
     * Check if JS Validation is disabled for attribute.
     *
     * @param $attribute
     * @return bool
     */
    public function jsValidationEnabled($attribute)
    {
        return ! $this->validator->hasRule($attribute, self::JSVALIDATION_DISABLE);
    }

    /**
     * Returns view data to render javascript.
     *
     * @return array
     */
    public function validationData()
    {
        $jsMessages = [];
        $jsValidations = $this->generateJavascriptValidations();

        return [
            'rules' => $jsValidations,
            'messages' => $jsMessages,
        ];
    }

    /**
     * Validate Conditional Validations using Ajax in specified fields.
     *
     * @param string $attribute
     * @param string|array $rules
     * @return void
     */
    public function sometimes($attribute, $rules = [])
    {
        $callback = function () {
            return true;
        };
        $this->validator->sometimes($attribute, $rules, $callback);
        $this->rules->addConditionalRules($attribute, (array) $rules);
    }
}


<?php

namespace Proengsoft\JsValidation;

use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;
use Proengsoft\JsValidation\Javascript\JavascriptValidator;
use Proengsoft\JsValidation\Javascript\MessageParser;
use Proengsoft\JsValidation\Javascript\RuleParser;
use Proengsoft\JsValidation\Javascript\ValidatorHandler;
use Proengsoft\JsValidation\Support\DelegatedValidator;
use Proengsoft\JsValidation\Support\ValidationRuleParserProxy;

class JsValidatorFactory
{
    /**
     * The application instance.
     *
     * @var \Illuminate\Container\Container
     */
    protected $app;

    /**
     * Configuration options.
     *
     * @var array
     */
    protected $options;

    /**
     * Create a new Validator factory instance.
     *
     * @param \Illuminate\Container\Container $app
     * @param array $options
     */
    public function __construct($app, array $options = [])
    {
        $this->app = $app;
        $this->setOptions($options);
    }

    /**
     * @param $options
     * @return void
     */
    protected function setOptions($options)
    {
        $options['disable_remote_validation'] = empty($options['disable_remote_validation']) ? false : $options['disable_remote_validation'];
        $options['view'] = empty($options['view']) ? 'jsvalidation:bootstrap' : $options['view'];
        $options['form_selector'] = empty($options['form_selector']) ? 'form' : $options['form_selector'];

        $this->options = $options;
    }

    /**
     * Creates JsValidator instance based on rules and message arrays.
     *
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @param null|string $selector
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function make(array $rules, array $messages = [], array $customAttributes = [], $selector = null)
    {
        $validator = $this->getValidatorInstance($rules, $messages, $customAttributes);

        return $this->validator($validator, $selector);
    }

    /**
     * Get the validator instance for the request.
     *
     * @param array $rules
     * @param array $messages
     * @param array $customAttributes
     * @return \Illuminate\Validation\Validator
     */
    protected function getValidatorInstance(array $rules, array $messages = [], array $customAttributes = [])
    {
        $factory = $this->app->make(ValidationFactory::class);

        $data = $this->getValidationData($rules, $customAttributes);
        $validator = $factory->make($data, $rules, $messages, $customAttributes);
        $validator->addCustomAttributes($customAttributes);

        return $validator;
    }

    /**
     * Gets fake data when validator has wildcard rules.
     *
     * @param array $rules
     * @return array
     */
    protected function getValidationData(array $rules, array $customAttributes = [])
    {
        $attributes = array_filter(array_keys($rules), function ($attribute) {
            return $attribute !== '' && mb_strpos($attribute, '*') !== false;
        });

        $attributes = array_merge(array_keys($customAttributes), $attributes);
        $data = array_reduce($attributes, function ($data, $attribute) {
            Arr::set($data, $attribute, true);

            return $data;
        }, []);

        return $data;
    }

    /**
     * Creates JsValidator instance based on FormRequest.
     *
     * @param $formRequest
     * @param null $selector
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function formRequest($formRequest, $selector = null)
    {
        if (! is_object($formRequest)) {
            $formRequest = $this->createFormRequest($formRequest);
        }

        $rules = method_exists($formRequest, 'rules') ? $formRequest->rules() : [];

        $validator = $this->getValidatorInstance($rules, $formRequest->messages(), $formRequest->attributes());

        return $this->validator($validator, $selector);
    }

    /**
     * @param string|array $class
     * @return array
     */
    protected function parseFormRequestName($class)
    {
        $params = [];
        if (is_array($class)) {
            $params = empty($class[1]) ? $params : $class[1];
            $class = $class[0];
        }

        return [$class, $params];
    }

    /**
     * Creates and initializes an Form Request instance.
     *
     * @param string $class
     * @return \Illuminate\Foundation\Http\FormRequest
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function createFormRequest($class)
    {
        /*
         * @var $formRequest \Illuminate\Foundation\Http\FormRequest
         * @var $request Request
         */
        list($class, $params) = $this->parseFormRequestName($class);

        $request = $this->app->__get('request');
        $formRequest = $this->app->build($class, $params);

        if ($session = $request->getSession()) {
            $formRequest->setLaravelSession($session);
        }
        $formRequest->setUserResolver($request->getUserResolver());
        $formRequest->setRouteResolver($request->getRouteResolver());
        $formRequest->setContainer($this->app);
        $formRequest->query = $request->query;

        return $formRequest;
    }

    /**
     * Creates JsValidator instance based on Validator.
     *
     * @param \Illuminate\Validation\Validator $validator
     * @param null|string $selector
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    public function validator(Validator $validator, $selector = null)
    {
        return $this->jsValidator($validator, $selector);
    }

    /**
     * Creates JsValidator instance based on Validator.
     *
     * @param \Illuminate\Validation\Validator $validator
     * @param null|string $selector
     * @return \Proengsoft\JsValidation\Javascript\JavascriptValidator
     */
    protected function jsValidator(Validator $validator, $selector = null)
    {
        $remote = ! $this->options['disable_remote_validation'];
        $view = $this->options['view'];
        $selector = is_null($selector) ? $this->options['form_selector'] : $selector;

        $delegated = new DelegatedValidator($validator, new ValidationRuleParserProxy());
        $rules = new RuleParser($delegated, $this->getSessionToken());
        $messages = new MessageParser($delegated);

        $jsValidator = new ValidatorHandler($rules, $messages);

        $manager = new JavascriptValidator($jsValidator, compact('view', 'selector', 'remote'));

        return $manager;
    }

    /**
     * Get and encrypt token from session store.
     *
     * @return null|string
     */
    protected function getSessionToken()
    {
        $token = null;
        if ($session = $this->app->__get('session')) {
            $token = $session->token();
        }

        if ($encrypter = $this->app->__get('encrypter')) {
            $token = $encrypter->encrypt($token);
        }

        return $token;
    }
}


<?php

namespace Proengsoft\JsValidation;

use Closure;
use Illuminate\Contracts\Config\Repository as Config;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Http\Request;
use Proengsoft\JsValidation\Remote\Resolver;
use Proengsoft\JsValidation\Remote\Validator as RemoteValidator;

class RemoteValidationMiddleware
{
    /**
     * Validator factory instance to wrap.
     *
     * @var \Illuminate\Contracts\Validation\Factory
     */
    protected $factory;

    /**
     * Field used to detect Javascript validation.
     *
     * @var mixed
     */
    protected $field;

    /**
     * RemoteValidationMiddleware constructor.
     *
     * @param \Illuminate\Contracts\Validation\Factory $validator
     * @param \Illuminate\Contracts\Config\Repository $config
     */
    public function __construct(ValidationFactory $validator, Config $config)
    {
        $this->factory = $validator;
        $this->field = $config->get('jsvalidation.remote_validation_field');
    }

    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        if ($request->has($this->field)) {
            $this->wrapValidator();
        }

        return $next($request);
    }

    /**
     * Wraps Validator resolver with RemoteValidator resolver.
     *
     * @return void
     */
    protected function wrapValidator()
    {
        $resolver = new Resolver($this->factory);
        $this->factory->resolver($resolver->resolver($this->field));
        $this->factory->extend(RemoteValidator::EXTENSION_NAME, $resolver->validatorClosure());
    }
}


<?php

namespace Proengsoft\JsValidation\Remote;

use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator as BaseValidator;
use Proengsoft\JsValidation\Support\AccessProtectedTrait;

class Resolver
{
    use AccessProtectedTrait;

    /**
     * @var \Closure
     */
    protected $resolver;

    /**
     * @var \Illuminate\Contracts\Validation\Factory
     */
    protected $factory;

    /**
     * RemoteValidator constructor.
     *
     * @param \Illuminate\Contracts\Validation\Factory $factory
     */
    public function __construct(ValidationFactory $factory)
    {
        $this->factory = $factory;
        $this->resolver = $this->getProtected($factory, 'resolver');
    }

    /**
     * Closure used to resolve Validator instance.
     *
     * @param $field
     * @return \Closure
     */
    public function resolver($field)
    {
        return function ($translator, $data, $rules, $messages, $customAttributes) use ($field) {
            return $this->resolve($translator, $data, $rules, $messages, $customAttributes, $field);
        };
    }

    /**
     * Resolves Validator instance.
     *
     * @param $translator
     * @param $data
     * @param $rules
     * @param $messages
     * @param $customAttributes
     * @param $field
     * @return \Illuminate\Validation\Validator
     */
    protected function resolve($translator, $data, $rules, $messages, $customAttributes, $field)
    {
        $validateAll = Arr::get($data, $field.'_validate_all', false);
        $validationRule = 'bail|'.Validator::EXTENSION_NAME.':'.$validateAll;
        $rules = [$field => $validationRule] + $rules;
        $validator = $this->createValidator($translator, $data, $rules, $messages, $customAttributes);

        return $validator;
    }

    /**
     * Create new validator instance.
     *
     * @param $translator
     * @param $data
     * @param $rules
     * @param $messages
     * @param $customAttributes
     * @return \Illuminate\Validation\Validator
     */
    protected function createValidator($translator, $data, $rules, $messages, $customAttributes)
    {
        if (is_null($this->resolver)) {
            return new BaseValidator($translator, $data, $rules, $messages, $customAttributes);
        }

        return call_user_func($this->resolver, $translator, $data, $rules, $messages, $customAttributes);
    }

    /**
     * Closure used to trigger JsValidations.
     *
     * @return \Closure
     */
    public function validatorClosure()
    {
        return function ($attribute, $value, $parameters, BaseValidator $validator) {
            $remoteValidator = new Validator($validator);
            $remoteValidator->validate($value, $parameters);

            return $attribute;
        };
    }
}


<?php

namespace Proengsoft\JsValidation\Support;

trait RuleListTrait
{
    /**
     *  Rules validated with Javascript.
     *
     * @var array
     */
    protected $clientRules = ['Accepted', 'After', 'Alpha', 'AlphaDash',
        'AlphaNum', 'Array', 'Bail', 'Before', 'Between', 'Boolean', 'Confirmed', 'Date', 'Dimensions',
        'DateFormat', 'Different', 'Digits', 'DigitsBetween', 'Distinct', 'Email', 'File', 'Filled', 'Image',
        'In', 'InArray', 'Integer', 'Ip', 'Json', 'Max', 'Mimes', 'Mimetypes', 'Min', 'NotIn', 'Nullable',
        'Numeric', 'Regex', 'Required', 'RequiredIf', 'RequiredUnless', 'RequiredWith', 'RequiredWithAll',
        'RequiredWithout', 'RequiredWithoutAll', 'Same', 'Size', 'Sometimes',
        'String', 'Timezone', 'Url', ];

    /**
     * Rules validated in Server-Side.
     *
     * @var array
     */
    protected $serverRules = ['ActiveUrl', 'Exists', 'Unique'];

    /**
     * Rules applyed to files.
     *
     * @var array
     */
    protected $fileRules = ['File', 'Image', 'Mimes', 'Mimetypes'];

    /**
     * Rule used to disable validations.
     *
     * @var string
     */
    private $disableJsValidationRule = 'NoJsValidation';

    /**
     * Returns if rule is validated using Javascript.
     *
     * @param $rule
     * @return bool
     */
    protected function isImplemented($rule)
    {
        return in_array($rule, $this->clientRules) || in_array($rule, $this->serverRules);
    }

    /**
     * Check if rule must be validated in server-side.
     *
     * @param $rule
     * @return bool
     */
    protected function isRemoteRule($rule)
    {
        return in_array($rule, $this->serverRules) ||
            ! in_array($rule, $this->clientRules);
    }

    /**
     * Check if rule disables rule processing.
     *
     * @param $rule
     * @return bool
     */
    protected function isDisableRule($rule)
    {
        return $rule === $this->disableJsValidationRule;
    }

    /**
     * Check if rules should be validated.
     *
     * @param $rules
     * @return bool
     */
    protected function validationDisabled($rules)
    {
        $rules = (array) $rules;

        return in_array($this->disableJsValidationRule, $rules);
    }

    /**
     * Check if rules is for input file type.
     *
     * @param $rule
     * @return bool
     */
    protected function isFileRule($rule)
    {
        return in_array($rule, $this->fileRules);
    }
}


<?php

namespace Mews\Purifier;

/**
 * Laravel 5 HTMLPurifier package
 *
 * @copyright Copyright (c) 2015 MeWebStudio
 * @version   2.0.0
 * @author    Muharrem ERİN
 * @contact me@mewebstudio.com
 * @web http://www.mewebstudio.com
 * @date      2014-04-02
 * @license   MIT
 */

use Exception;
use HTMLPurifier;
use HTMLPurifier_Config;
use HTMLPurifier_HTMLDefinition;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Filesystem\Filesystem;

class Purifier
{

    /**
     * @var Filesystem
     */
    protected $files;

    /**
     * @var Repository
     */
    protected $config;

    /**
     * @var HTMLPurifier
     */
    protected $purifier;

    /**
     * Constructor
     *
     * @param Filesystem $files
     * @param Repository $config
     * @throws Exception
     */
    public function __construct(Filesystem $files, Repository $config)
    {
        $this->files = $files;
        $this->config = $config;

        $this->setUp();
    }

    /**
     * Setup
     *
     * @throws Exception
     */
    private function setUp()
    {
        if (!$this->config->has('purifier')) {
            throw new Exception('Configuration parameters not loaded!');
        }

        $this->checkCacheDirectory();

        // Create a new configuration object
        $config = $this->getConfig();

        // Create HTMLPurifier object
        $this->purifier = new HTMLPurifier($config);
    }

    /**
     * Add a custom definition
     *
     * @see http://htmlpurifier.org/docs/enduser-customize.html
     * @param array $definitionConfig
     * @param HTMLPurifier_Config $configObject Defaults to using default config
     *
     * @return HTMLPurifier_Config $configObject
     */
    private function addCustomDefinition(array $definitionConfig, HTMLPurifier_Config $configObject = null)
    {
        if (!$configObject) {
            $configObject = HTMLPurifier_Config::createDefault();
            $configObject->loadArray($this->getConfig());
        }

        // Setup the custom definition
        $configObject->set('HTML.DefinitionID', $definitionConfig['id']);
        $configObject->set('HTML.DefinitionRev', $definitionConfig['rev']);

        // Enable debug mode
        if (!isset($definitionConfig['debug']) || $definitionConfig['debug']) {
            $configObject->set('Cache.DefinitionImpl', null);
        }

        // Start configuring the definition
        if ($def = $configObject->maybeGetRawHTMLDefinition()) {
            // Create the definition attributes
            if (!empty($definitionConfig['attributes'])) {
                $this->addCustomAttributes($definitionConfig['attributes'], $def);
            }

            // Create the definition elements
            if (!empty($definitionConfig['elements'])) {
                $this->addCustomElements($definitionConfig['elements'], $def);
            }
        }

        return $configObject;
    }

    /**
     * Add provided attributes to the provided definition
     *
     * @param array $attributes
     * @param HTMLPurifier_HTMLDefinition $definition
     *
     * @return HTMLPurifier_HTMLDefinition $definition
     */
    private function addCustomAttributes(array $attributes, HTMLPurifier_HTMLDefinition $definition)
    {
        foreach ($attributes as $attribute) {
            // Get configuration of attribute
            $required = !empty($attribute[3]) ? true : false;
            $onElement = $attribute[0];
            $attrName = $required ? $attribute[1] . '*' : $attribute[1];
            $validValues = $attribute[2];

            $definition->addAttribute($onElement, $attrName, $validValues);
        }

        return $definition;
    }

    /**
     * Add provided elements to the provided definition
     *
     * @param array $elements
     * @param HTMLPurifier_HTMLDefinition $definition
     *
     * @return HTMLPurifier_HTMLDefinition $definition
     */
    private function addCustomElements(array $elements, HTMLPurifier_HTMLDefinition $definition)
    {
        foreach ($elements as $element) {
            // Get configuration of element
            $name = $element[0];
            $contentSet = $element[1];
            $allowedChildren = $element[2];
            $attributeCollection = $element[3];
            $attributes = isset($element[4]) ? $element[4] : null;

            if (!empty($attributes)) {
                $definition->addElement($name, $contentSet, $allowedChildren, $attributeCollection, $attributes);
            } else {
                $definition->addElement($name, $contentSet, $allowedChildren, $attributeCollection);
            }
        }
    }

    /**
     * Check/Create cache directory
     */
    private function checkCacheDirectory()
    {
        $cachePath = $this->config->get('purifier.cachePath');

        if ($cachePath) {
            if (!$this->files->isDirectory($cachePath)) {
                $this->files->makeDirectory($cachePath, $this->config->get('purifier.cacheFileMode', 0755),true);
            }
        }
    }

    /**
     * @param null $config
     *
     * @return mixed|null
     */
    protected function getConfig($config = null)
    {
        // Create a new configuration object
        $configObject = HTMLPurifier_Config::createDefault();

        // Allow configuration to be modified
        if (! $this->config->get('purifier.finalize')) {
            $configObject->autoFinalize = false;
        }

        // Set default config
        $defaultConfig = [];
        $defaultConfig['Core.Encoding'] = $this->config->get('purifier.encoding');
        $defaultConfig['Cache.SerializerPath'] = $this->config->get('purifier.cachePath');
        $defaultConfig['Cache.SerializerPermissions'] = $this->config->get('purifier.cacheFileMode', 0755);

        if (! $config) {
            $config = $this->config->get('purifier.settings.default');
        } elseif (is_string($config)) {
            $config = $this->config->get('purifier.settings.' . $config);
        }

        if (! is_array($config)) {
            $config = [];
        }

        // Merge configurations
        $config = $defaultConfig + $config;

        // Load to Purifier config
        $configObject->loadArray($config);

        // Load custom definition if set
        if ($definitionConfig = $this->config->get('purifier.settings.custom_definition')) {
            $this->addCustomDefinition($definitionConfig, $configObject);
        }

        // Load custom elements if set
        if ($elements = $this->config->get('purifier.settings.custom_elements')) {
            if ($def = $configObject->maybeGetRawHTMLDefinition()) {
                $this->addCustomElements($elements, $def);
            }
        }

        // Load custom attributes if set
        if ($attributes = $this->config->get('purifier.settings.custom_attributes')) {
            if ($def = $configObject->maybeGetRawHTMLDefinition()) {
                $this->addCustomAttributes($attributes, $def);
            }
        }

        return $configObject;
    }

    /**
     * @param      $dirty
     * @param null $config
     *
     * @return mixed
     */
    public function clean($dirty, $config = null)
    {
        if (is_array($dirty)) {
            return array_map(function ($item) use ($config) {
                return $this->clean($item, $config);
            }, $dirty);
        }

        return $this->purifier->purify($dirty, $config ? $this->getConfig($config) : null);
    }

    /**
     * Get HTMLPurifier instance.
     *
     * @return \HTMLPurifier
     */
    public function getInstance()
    {
        return $this->purifier;
    }
}


<?php namespace Mews\Purifier;

use Illuminate\Container\Container;
use Illuminate\Foundation\Application as LaravelApplication;
use Illuminate\Support\ServiceProvider;
use Laravel\Lumen\Application as LumenApplication;

class PurifierServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * Boot the service provider.
     *
     * @return null
     */
    public function boot()
    {
        if ($this->app instanceof LaravelApplication) {
            $this->publishes([$this->getConfigSource() => config_path('purifier.php')]);
        } elseif ($this->app instanceof LumenApplication) {
            $this->app->configure('purifier');
        }
    }

    /**
     * Get the config source.
     *
     * @return string
     */
    protected function getConfigSource()
    {
        return realpath(__DIR__.'/../config/purifier.php');
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->mergeConfigFrom($this->getConfigSource(), 'purifier');

        $this->app->singleton('purifier', function (Container $app) {
            return new Purifier($app['files'], $app['config']);
        });

        $this->app->alias('purifier', Purifier::class);
    }

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


<?php

namespace Knp\Menu\Matcher\Voter;

use Knp\Menu\ItemInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Voter based on the route
 */
class RouteVoter implements VoterInterface
{
    /**
     * @var RequestStack|null
     */
    private $requestStack;

    /**
     * @var Request|null
     */
    private $request;

    public function __construct($requestStack = null)
    {
        if ($requestStack instanceof RequestStack) {
            $this->requestStack = $requestStack;
        } elseif ($requestStack instanceof Request) {
            @trigger_error(sprintf('Passing a Request as the first argument for "%s" constructor is deprecated since version 2.3 and wwon\'t be possible in 3.0. Pass a RequestStack instead.', __CLASS__), E_USER_DEPRECATED);

            // BC layer for the old API of the class
            $this->request = $requestStack;
        } elseif (null !== $requestStack) {
            throw new \InvalidArgumentException('The first argument of %s must be null, a RequestStack or a Request. %s given', __CLASS__, is_object($requestStack) ? get_class($requestStack) :  gettype($requestStack));
        } else {
            @trigger_error(sprintf('Not passing a RequestStack as the first argument for "%s" constructor is deprecated since version 2.3 and won\'t be possible in 3.0.', __CLASS__), E_USER_DEPRECATED);
        }
    }

    /**
     * Sets the request against which the menu should be matched.
     *
     * This Request is ignored in case a RequestStack is passed in the constructor.
     *
     * @deprecated since version 2.3. Pass a RequestStack to the constructor instead.
     *
     * @param Request $request
     */
    public function setRequest(Request $request)
    {
        @trigger_error(sprintf('The %s() method is deprecated since version 2.3 and will be removed in 3.0. Pass a RequestStack in the constructor instead.', __METHOD__), E_USER_DEPRECATED);

        $this->request = $request;
    }

    public function matchItem(ItemInterface $item)
    {
        if (null !== $this->requestStack) {
            $request = $this->requestStack->getMasterRequest();
        } else {
            $request = $this->request;
        }

        if (null === $request) {
            return null;
        }

        $route = $request->attributes->get('_route');
        if (null === $route) {
            return null;
        }

        $routes = (array) $item->getExtra('routes', array());

        foreach ($routes as $testedRoute) {
            if (is_string($testedRoute)) {
                $testedRoute = array('route' => $testedRoute);
            }

            if (!is_array($testedRoute)) {
                throw new \InvalidArgumentException('Routes extra items must be strings or arrays.');
            }

            if ($this->isMatchingRoute($request, $testedRoute)) {
                return true;
            }
        }

        return null;
    }

    private function isMatchingRoute(Request $request, array $testedRoute)
    {
        $route = $request->attributes->get('_route');

        if (isset($testedRoute['route'])) {
            if ($route !== $testedRoute['route']) {
                return false;
            }
        } elseif (!empty($testedRoute['pattern'])) {
            if (!preg_match($testedRoute['pattern'], $route)) {
                return false;
            }
        } else {
            throw new \InvalidArgumentException('Routes extra items must have a "route" or "pattern" key.');
        }

        if (!isset($testedRoute['parameters'])) {
            return true;
        }

        $routeParameters = $request->attributes->get('_route_params', array());

        foreach ($testedRoute['parameters'] as $name => $value) {
            // cast both to string so that we handle integer and other non-string parameters, but don't stumble on 0 == 'abc'.
            if (!isset($routeParameters[$name]) || (string) $routeParameters[$name] !== (string) $value) {
                return false;
            }
        }

        return true;
    }
}


<?php

namespace Knp\Menu;

use Knp\Menu\Factory\CoreExtension;
use Knp\Menu\Factory\ExtensionInterface;

/**
 * Factory to create a menu from a tree
 */
class MenuFactory implements FactoryInterface
{
    /**
     * @var array[]
     */
    private $extensions = array();

    /**
     * @var ExtensionInterface[]
     */
    private $sorted;

    public function __construct()
    {
        $this->addExtension(new CoreExtension(), -10);
    }

    public function createItem($name, array $options = array())
    {
        foreach ($this->getExtensions() as $extension) {
            $options = $extension->buildOptions($options);
        }

        $item = new MenuItem($name, $this);

        foreach ($this->getExtensions() as $extension) {
            $extension->buildItem($item, $options);
        }

        return $item;
    }

    /**
     * Adds a factory extension
     *
     * @param ExtensionInterface $extension
     * @param integer            $priority
     */
    public function addExtension(ExtensionInterface $extension, $priority = 0)
    {
        $this->extensions[$priority][] = $extension;
        $this->sorted = null;
    }

    /**
     * Sorts the internal list of extensions by priority.
     *
     * @return ExtensionInterface[]
     */
    private function getExtensions()
    {
        if (null === $this->sorted) {
            krsort($this->extensions);
            $this->sorted = !empty($this->extensions) ? call_user_func_array('array_merge', $this->extensions) : array();
        }

        return $this->sorted;
    }
}


<?php

namespace Knp\Menu\Renderer;

if (!defined('ENT_SUBSTITUTE')) {
    define('ENT_SUBSTITUTE', 8);
}

abstract class Renderer
{
    protected $charset = 'UTF-8';

    /**
     * @param string $charset
     */
    public function __construct($charset = null)
    {
        if (null !== $charset) {
            $this->charset = (string) $charset;
        }
    }

    /**
     * Renders a HTML attribute
     *
     * @param string $name
     * @param string $value
     *
     * @return string
     */
    protected function renderHtmlAttribute($name, $value)
    {
        if (true === $value) {
            return sprintf('%s="%s"', $name, $this->escape($name));
        }

        return sprintf('%s="%s"', $name, $this->escape($value));
    }

    /**
     * Renders HTML attributes
     *
     * @param array $attributes
     *
     * @return string
     */
    protected function renderHtmlAttributes(array $attributes)
    {
        return implode('', array_map(array($this, 'htmlAttributesCallback'), array_keys($attributes), array_values($attributes)));
    }

    /**
     * Prepares an attribute key and value for HTML representation.
     *
     * It removes empty attributes.
     *
     * @param string $name  The attribute name
     * @param string $value The attribute value
     *
     * @return string The HTML representation of the HTML key attribute pair.
     */
    private function htmlAttributesCallback($name, $value)
    {
        if (false === $value || null === $value) {
            return '';
        }

        return ' '.$this->renderHtmlAttribute($name, $value);
    }

    /**
     * Escapes an HTML value
     *
     * @param string $value
     *
     * @return string
     */
    protected function escape($value)
    {
        return $this->fixDoubleEscape(htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, $this->charset));
    }

    /**
     * Fixes double escaped strings.
     *
     * @param string $escaped string to fix
     *
     * @return string A single escaped string
     */
    protected function fixDoubleEscape($escaped)
    {
        return preg_replace('/&([a-z]+|(#\d+)|(#x[\da-f]+));/i', '&$1;', $escaped);
    }

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

    /**
     * Set the HTML charset
     *
     * @param string $charset
     */
    public function setCharset($charset)
    {
        $this->charset = (string) $charset;
    }
}


<?php

namespace Knp\Menu\Matcher;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;

/**
 * A MatcherInterface implementation using a voter system
 */
class Matcher implements MatcherInterface
{
    private $cache;
    private $voters;

    /**
     * @param VoterInterface[]|iterable $voters
     */
    public function __construct($voters = array())
    {
        $this->voters = $voters;
        $this->cache = new \SplObjectStorage();
    }

    /**
     * Adds a voter in the matcher.
     *
     * If an iterator was used to provide voters in the constructor, it will be
     * converted to array when using this method, breaking any potential lazy-loading.
     *
     * @deprecated since 2.3. Pass voters in the constructor instead.
     *
     * @param VoterInterface $voter
     */
    public function addVoter(VoterInterface $voter)
    {
        @trigger_error(sprintf('The %s() method is deprecated since version 2.3 and will be removed in 3.0. Pass voters in the constructor instead.', __METHOD__), E_USER_DEPRECATED);

        if ($this->voters instanceof \Traversable) {
            $this->voters = iterator_to_array($this->voters);
        }

        $this->voters[] = $voter;
    }

    public function isCurrent(ItemInterface $item)
    {
        $current = $item->isCurrent();
        if (null !== $current) {
            return $current;
        }

        if ($this->cache->contains($item)) {
            return $this->cache[$item];
        }

        foreach ($this->voters as $voter) {
            $current = $voter->matchItem($item);
            if (null !== $current) {
                break;
            }
        }

        $current = (boolean) $current;
        $this->cache[$item] = $current;

        return $current;
    }

    public function isAncestor(ItemInterface $item, $depth = null)
    {
        if (0 === $depth) {
            return false;
        }

        $childDepth = null === $depth ? null : $depth - 1;
        foreach ($item->getChildren() as $child) {
            if ($this->isCurrent($child) || $this->isAncestor($child, $childDepth)) {
                return true;
            }
        }

        return false;
    }

    public function clear()
    {
        $this->cache = new \SplObjectStorage();
    }
}


<?php

namespace Knp\Menu\Twig;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Util\MenuManipulator;
use Knp\Menu\Renderer\RendererProviderInterface;
use Knp\Menu\Provider\MenuProviderInterface;

/**
 * Helper class containing logic to retrieve and render menus from templating engines
 *
 */
class Helper
{
    private $rendererProvider;
    private $menuProvider;
    private $menuManipulator;
    private $matcher;

    /**
     * @param RendererProviderInterface  $rendererProvider
     * @param MenuProviderInterface|null $menuProvider
     * @param MenuManipulator|null       $menuManipulator
     * @param MatcherInterface|null      $matcher
     */
    public function __construct(RendererProviderInterface $rendererProvider, MenuProviderInterface $menuProvider = null, MenuManipulator $menuManipulator = null, MatcherInterface $matcher = null)
    {
        $this->rendererProvider = $rendererProvider;
        $this->menuProvider = $menuProvider;
        $this->menuManipulator = $menuManipulator;
        $this->matcher = $matcher;
    }

    /**
     * Retrieves item in the menu, eventually using the menu provider.
     *
     * @param ItemInterface|string $menu
     * @param array                $path
     * @param array                $options
     *
     * @return ItemInterface
     *
     * @throws \BadMethodCallException   when there is no menu provider and the menu is given by name
     * @throws \LogicException
     * @throws \InvalidArgumentException when the path is invalid
     */
    public function get($menu, array $path = array(), array $options = array())
    {
        if (!$menu instanceof ItemInterface) {
            if (null === $this->menuProvider) {
                throw new \BadMethodCallException('A menu provider must be set to retrieve a menu');
            }

            $menuName = $menu;
            $menu = $this->menuProvider->get($menuName, $options);

            if (!$menu instanceof ItemInterface) {
                throw new \LogicException(sprintf('The menu "%s" exists, but is not a valid menu item object. Check where you created the menu to be sure it returns an ItemInterface object.', $menuName));
            }
        }

        foreach ($path as $child) {
            $menu = $menu->getChild($child);
            if (null === $menu) {
                throw new \InvalidArgumentException(sprintf('The menu has no child named "%s"', $child));
            }
        }

        return $menu;
    }

    /**
     * Renders a menu with the specified renderer.
     *
     * If the argument is an array, it will follow the path in the tree to
     * get the needed item. The first element of the array is the whole menu.
     * If the menu is a string instead of an ItemInterface, the provider
     * will be used.
     *
     * @param ItemInterface|string|array $menu
     * @param array                      $options
     * @param string                     $renderer
     *
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    public function render($menu, array $options = array(), $renderer =  null)
    {
        $menu = $this->castMenu($menu);

        return $this->rendererProvider->get($renderer)->render($menu, $options);
    }

    /**
     * Renders an array ready to be used for breadcrumbs.
     *
     * Each element in the array will be an array with 3 keys:
     * - `label` containing the label of the item
     * - `url` containing the url of the item (may be `null`)
     * - `item` containing the original item (may be `null` for the extra items)
     *
     * The subItem can be one of the following forms
     *   * 'subItem'
     *   * ItemInterface object
     *   * array('subItem' => '@homepage')
     *   * array('subItem1', 'subItem2')
     *   * array(array('label' => 'subItem1', 'url' => '@homepage'), array('label' => 'subItem2'))
     *
     * @param mixed $item
     * @param mixed $subItem A string or array to append onto the end of the array
     *
     * @return array
     */
    public function getBreadcrumbsArray($menu, $subItem = null)
    {
        if (null === $this->menuManipulator) {
            throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array');
        }

        $menu = $this->castMenu($menu);

        return $this->menuManipulator->getBreadcrumbsArray($menu, $subItem);
    }

    /**
     * Returns the current item of a menu.
     *
     * @param ItemInterface|array|string $menu
     *
     * @return ItemInterface|null
     */
    public function getCurrentItem($menu)
    {
        if (null === $this->matcher) {
            throw new \BadMethodCallException('The matcher must be set to get the current item of a menu');
        }

        $menu = $this->castMenu($menu);

        return $this->retrieveCurrentItem($menu);
    }

    /**
     * @param ItemInterface|array|string $menu
     *
     * @return ItemInterface
     */
    private function castMenu($menu)
    {
        if (!$menu instanceof ItemInterface) {
            $path = array();
            if (is_array($menu)) {
                if (empty($menu)) {
                    throw new \InvalidArgumentException('The array cannot be empty');
                }
                $path = $menu;
                $menu = array_shift($path);
            }

            return $this->get($menu, $path);
        }

        return $menu;
    }

    /**
     * @param ItemInterface $item
     *
     * @return ItemInterface|null
     */
    private function retrieveCurrentItem(ItemInterface $item)
    {
        if ($this->matcher->isCurrent($item)) {
            return $item;
        }

        if ($this->matcher->isAncestor($item)) {
            foreach ($item->getChildren() as $child) {
                $currentItem = $this->retrieveCurrentItem($child);

                if (null !== $currentItem) {
                    return $currentItem;
                }
            }
        }

        return null;
    }
}


<?php

namespace Knp\Menu\Twig;

use Knp\Menu\ItemInterface;
use Knp\Menu\Util\MenuManipulator;
use Knp\Menu\Matcher\MatcherInterface;

class MenuExtension extends \Twig_Extension
{
    private $helper;
    private $matcher;
    private $menuManipulator;

    /**
     * @param Helper $helper
     */
    public function __construct(Helper $helper, MatcherInterface $matcher = null, MenuManipulator $menuManipulator = null)
    {
        $this->helper = $helper;
        $this->matcher = $matcher;
        $this->menuManipulator = $menuManipulator;
    }

    public function getFunctions()
    {
        return array(
             new \Twig_SimpleFunction('knp_menu_get', array($this, 'get')),
             new \Twig_SimpleFunction('knp_menu_render', array($this, 'render'), array('is_safe' => array('html'))),
             new \Twig_SimpleFunction('knp_menu_get_breadcrumbs_array', array($this, 'getBreadcrumbsArray')),
             new \Twig_SimpleFunction('knp_menu_get_current_item', array($this, 'getCurrentItem')),
        );
    }

    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter('knp_menu_as_string', array($this, 'pathAsString')),
        );
    }

    public function getTests()
    {
        return array(
            new \Twig_SimpleTest('knp_menu_current', array($this, 'isCurrent')),
            new \Twig_SimpleTest('knp_menu_ancestor', array($this, 'isAncestor')),
        );
    }

    /**
     * Retrieves an item following a path in the tree.
     *
     * @param ItemInterface|string $menu
     * @param array                $path
     * @param array                $options
     *
     * @return ItemInterface
     */
    public function get($menu, array $path = array(), array $options = array())
    {
        return $this->helper->get($menu, $path, $options);
    }

    /**
     * Renders a menu with the specified renderer.
     *
     * @param ItemInterface|string|array $menu
     * @param array                      $options
     * @param string                     $renderer
     *
     * @return string
     */
    public function render($menu, array $options = array(), $renderer = null)
    {
        return $this->helper->render($menu, $options, $renderer);
    }

    /**
     * Returns an array ready to be used for breadcrumbs.
     *
     * @param ItemInterface|array|string $item
     * @param string|array|null          $subItem
     *
     * @return array
     */
    public function getBreadcrumbsArray($menu, $subItem = null)
    {
        return $this->helper->getBreadcrumbsArray($menu, $subItem);
    }

    /**
     * Returns the current item of a menu.
     *
     * @param ItemInterface|string $menu
     *
     * @return ItemInterface
     */
    public function getCurrentItem($menu)
    {
        $rootItem = $this->get($menu);

        $currentItem = $this->helper->getCurrentItem($rootItem);

        if (null === $currentItem) {
            $currentItem = $rootItem;
        }

        return $currentItem;
    }

    /**
     * A string representation of this menu item
     *
     * e.g. Top Level > Second Level > This menu
     *
     * @param ItemInterface $item
     * @param string        $separator
     *
     * @return string
     */
    public function pathAsString(ItemInterface $menu, $separator = ' > ')
    {
        if (null === $this->menuManipulator) {
            throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array');
        }

        return $this->menuManipulator->getPathAsString($menu, $separator);
    }

    /**
     * Checks whether an item is current.
     *
     * @param ItemInterface $item
     *
     * @return boolean
     */
    public function isCurrent(ItemInterface $item)
    {
        if (null === $this->matcher) {
            throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
        }

        return $this->matcher->isCurrent($item);
    }

    /**
     * Checks whether an item is the ancestor of a current item.
     *
     * @param ItemInterface $item
     * @param integer       $depth The max depth to look for the item
     *
     * @return boolean
     */
    public function isAncestor(ItemInterface $item, $depth = null)
    {
        if (null === $this->matcher) {
            throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array');
        }

        return $this->matcher->isAncestor($item, $depth);
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'knp_menu';
    }
}


<?php

namespace Knp\Menu\Factory;

use Knp\Menu\ItemInterface;

/**
 * core factory extension with the main logic
 */
class CoreExtension implements ExtensionInterface
{
    /**
     * Builds the full option array used to configure the item.
     *
     * @param array $options
     *
     * @return array
     */
    public function buildOptions(array $options)
    {
        return array_merge(
            array(
                'uri' => null,
                'label' => null,
                'attributes' => array(),
                'linkAttributes' => array(),
                'childrenAttributes' => array(),
                'labelAttributes' => array(),
                'extras' => array(),
                'current' => null,
                'display' => true,
                'displayChildren' => true,
            ),
            $options
        );
    }

    /**
     * Configures the newly created item with the passed options
     *
     * @param ItemInterface $item
     * @param array         $options
     */
    public function buildItem(ItemInterface $item, array $options)
    {
        $item
            ->setUri($options['uri'])
            ->setLabel($options['label'])
            ->setAttributes($options['attributes'])
            ->setLinkAttributes($options['linkAttributes'])
            ->setChildrenAttributes($options['childrenAttributes'])
            ->setLabelAttributes($options['labelAttributes'])
            ->setCurrent($options['current'])
            ->setDisplay($options['display'])
            ->setDisplayChildren($options['displayChildren'])
        ;

        $this->buildExtras($item, $options);
    }

    /**
     * Configures the newly created item's extras
     * Extras are processed one by one in order not to reset values set by other extensions
     *
     * @param ItemInterface $item
     * @param array         $options
     */
    private function buildExtras(ItemInterface $item, array $options)
    {
        if (!empty($options['extras'])) {
            foreach ($options['extras'] as $key => $value) {
                $item->setExtra($key, $value);
            }
        }
    }
}


<?php

namespace Knp\Menu\Loader;

use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;

/**
 * Loader importing a menu tree from an array.
 *
 * The array should match the output of MenuManipulator::toArray
 */
class ArrayLoader implements LoaderInterface
{
    private $factory;

    public function __construct(FactoryInterface $factory)
    {
        $this->factory = $factory;
    }

    public function load($data)
    {
        if (!$this->supports($data)) {
            throw new \InvalidArgumentException(sprintf('Unsupported data. Expected an array but got ', is_object($data) ? get_class($data) : gettype($data)));
        }

        return $this->fromArray($data);
    }

    public function supports($data)
    {
        return is_array($data);
    }

    /**
     * @param array       $data
     * @param string|null $name (the name of the item, used only if there is no name in the data themselves)
     *
     * @return ItemInterface
     */
    private function fromArray(array $data, $name = null)
    {
        $name = isset($data['name']) ? $data['name'] : $name;

        if (isset($data['children'])) {
            $children = $data['children'];
            unset($data['children']);
        } else {
            $children = array();
        }

        $item = $this->factory->createItem($name, $data);

        foreach ($children as $name => $child) {
            $item->addChild($this->fromArray($child, $name));
        }

        return $item;
    }
}


<?php

namespace Knp\Menu\Renderer;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;

/**
 * Renders MenuItem tree as unordered list
 */
class ListRenderer extends Renderer implements RendererInterface
{
    protected $matcher;
    protected $defaultOptions;

    /**
     * @param MatcherInterface $matcher
     * @param array            $defaultOptions
     * @param string           $charset
     */
    public function __construct(MatcherInterface $matcher, array $defaultOptions = array(), $charset = null)
    {
        $this->matcher = $matcher;
        $this->defaultOptions = array_merge(array(
            'depth' => null,
            'matchingDepth' => null,
            'currentAsLink' => true,
            'currentClass' => 'current',
            'ancestorClass' => 'current_ancestor',
            'firstClass' => 'first',
            'lastClass' => 'last',
            'compressed' => false,
            'allow_safe_labels' => false,
            'clear_matcher' => true,
            'leaf_class' => null,
            'branch_class' => null,
        ), $defaultOptions);

        parent::__construct($charset);
    }

    public function render(ItemInterface $item, array $options = array())
    {
        $options = array_merge($this->defaultOptions, $options);

        $html = $this->renderList($item, $item->getChildrenAttributes(), $options);

        if ($options['clear_matcher']) {
            $this->matcher->clear();
        }

        return $html;
    }

    protected function renderList(ItemInterface $item, array $attributes, array $options)
    {
        /**
         * Return an empty string if any of the following are true:
         *   a) The menu has no children eligible to be displayed
         *   b) The depth is 0
         *   c) This menu item has been explicitly set to hide its children
         */
        if (!$item->hasChildren() || 0 === $options['depth'] || !$item->getDisplayChildren()) {
            return '';
        }

        $html = $this->format('
renderHtmlAttributes($attributes).'>', 'ul', $item->getLevel(), $options);
        $html .= $this->renderChildren($item, $options);
        $html .= $this->format('
', 'ul', $item->getLevel(), $options);

        return $html;
    }

    /**
     * Renders all of the children of this menu.
     *
     * This calls ->renderItem() on each menu item, which instructs each
     * menu item to render themselves as an
 tag (with nested ul if it
     * has children).
     * This method updates the depth for the children.
     *
     * @param ItemInterface $item
     * @param array         $options The options to render the item.
     *
     * @return string
     */
    protected function renderChildren(ItemInterface $item, array $options)
    {
        // render children with a depth - 1
        if (null !== $options['depth']) {
            $options['depth'] = $options['depth'] - 1;
        }

        if (null !== $options['matchingDepth'] && $options['matchingDepth'] > 0) {
            $options['matchingDepth'] = $options['matchingDepth'] - 1;
        }

        $html = '';
        foreach ($item->getChildren() as $child) {
            $html .= $this->renderItem($child, $options);
        }

        return $html;
    }

    /**
     * Called by the parent menu item to render this menu.
     *
     * This renders the li tag to fit into the parent ul as well as its
     * own nested ul tag if this menu item has children
     *
     * @param ItemInterface $item
     * @param array         $options The options to render the item
     *
     * @return string
     */
    protected function renderItem(ItemInterface $item, array $options)
    {
        // if we don't have access or this item is marked to not be shown
        if (!$item->isDisplayed()) {
            return '';
        }

        // create an array than can be imploded as a class list
        $class = (array) $item->getAttribute('class');

        if ($this->matcher->isCurrent($item)) {
            $class[] = $options['currentClass'];
        } elseif ($this->matcher->isAncestor($item, $options['matchingDepth'])) {
            $class[] = $options['ancestorClass'];
        }

        if ($item->actsLikeFirst()) {
            $class[] = $options['firstClass'];
        }
        if ($item->actsLikeLast()) {
            $class[] = $options['lastClass'];
        }

        if ($item->hasChildren() && $options['depth'] !== 0) {
            if (null !== $options['branch_class'] && $item->getDisplayChildren()) {
                $class[] = $options['branch_class'];
            }
        } elseif (null !== $options['leaf_class']) {
            $class[] = $options['leaf_class'];
        }

        // retrieve the attributes and put the final class string back on it
        $attributes = $item->getAttributes();
        if (!empty($class)) {
            $attributes['class'] = implode(' ', $class);
        }

        // opening li tag
        $html = $this->format('
renderHtmlAttributes($attributes).'>', 'li', $item->getLevel(), $options);

        // render the text/link inside the li tag
        //$html .= $this->format($item->getUri() ? $item->renderLink() : $item->renderLabel(), 'link', $item->getLevel());
        $html .= $this->renderLink($item, $options);

        // renders the embedded ul
        $childrenClass = (array) $item->getChildrenAttribute('class');
        $childrenClass[] = 'menu_level_'.$item->getLevel();

        $childrenAttributes = $item->getChildrenAttributes();
        $childrenAttributes['class'] = implode(' ', $childrenClass);

        $html .= $this->renderList($item, $childrenAttributes, $options);

        // closing li tag
        $html .= $this->format('
', 'li', $item->getLevel(), $options);

        return $html;
    }

    /**
     * Renders the link in a a tag with link attributes or
     * the label in a span tag with label attributes
     *
     * Tests if item has a an uri and if not tests if it's
     * the current item and if the text has to be rendered
     * as a link or not.
     *
     * @param ItemInterface $item    The item to render the link or label for
     * @param array         $options The options to render the item
     *
     * @return string
     */
    protected function renderLink(ItemInterface $item, array $options = array())
    {
        if ($item->getUri() && (!$item->isCurrent() || $options['currentAsLink'])) {
            $text = $this->renderLinkElement($item, $options);
        } else {
            $text = $this->renderSpanElement($item, $options);
        }

        return $this->format($text, 'link', $item->getLevel(), $options);
    }

    protected function renderLinkElement(ItemInterface $item, array $options)
    {
        return sprintf('%s', $this->escape($item->getUri()), $this->renderHtmlAttributes($item->getLinkAttributes()), $this->renderLabel($item, $options));
    }

    protected function renderSpanElement(ItemInterface $item, array $options)
    {
        return sprintf('%s', $this->renderHtmlAttributes($item->getLabelAttributes()), $this->renderLabel($item, $options));
    }

    protected function renderLabel(ItemInterface $item, array $options)
    {
        if ($options['allow_safe_labels'] && $item->getExtra('safe_label', false)) {
            return $item->getLabel();
        }

        return $this->escape($item->getLabel());
    }

    /**
     * If $this->renderCompressed is on, this will apply the necessary
     * spacing and line-breaking so that the particular thing being rendered
     * makes up its part in a fully-rendered and spaced menu.
     *
     * @param string  $html    The html to render in an (un)formatted way
     * @param string  $type    The type [ul,link,li] of thing being rendered
     * @param integer $level
     * @param array   $options
     *
     * @return string
     */
    protected function format($html, $type, $level, array $options)
    {
        if ($options['compressed']) {
            return $html;
        }

        switch ($type) {
            case 'ul':
            case 'link':
                $spacing = $level * 4;
                break;

            case 'li':
                $spacing = $level * 4 - 2;
                break;
        }

        return str_repeat(' ', $spacing).$html."\n";
    }
}


<?php

namespace Knp\Menu\Util;

use Knp\Menu\ItemInterface;

class MenuManipulator
{
    /**
     * Moves item to specified position. Rearrange siblings accordingly.
     *
     * @param ItemInterface $item
     * @param integer       $position Position to move child to.
     */
    public function moveToPosition(ItemInterface $item, $position)
    {
        $this->moveChildToPosition($item->getParent(), $item, $position);
    }

    /**
     * Moves child to specified position. Rearrange other children accordingly.
     *
     * @param ItemInterface $item
     * @param ItemInterface $child    Child to move.
     * @param integer       $position Position to move child to.
     */
    public function moveChildToPosition(ItemInterface $item, ItemInterface $child, $position)
    {
        $name = $child->getName();
        $order = array_keys($item->getChildren());

        $oldPosition = array_search($name, $order);
        unset($order[$oldPosition]);

        $order = array_values($order);

        array_splice($order, $position, 0, $name);
        $item->reorderChildren($order);
    }

    /**
     * Moves item to first position. Rearrange siblings accordingly.
     *
     * @param ItemInterface $item
     */
    public function moveToFirstPosition(ItemInterface $item)
    {
        $this->moveToPosition($item, 0);
    }

    /**
     * Moves item to last position. Rearrange siblings accordingly.
     *
     * @param ItemInterface $item
     */
    public function moveToLastPosition(ItemInterface $item)
    {
        $this->moveToPosition($item, $item->getParent()->count());
    }

    /**
     * Get slice of menu as another menu.
     *
     * If offset and/or length are numeric, it works like in array_slice function:
     *
     *   If offset is non-negative, slice will start at the offset.
     *   If offset is negative, slice will start that far from the end.
     *
     *   If length is null, slice will have all elements.
     *   If length is positive, slice will have that many elements.
     *   If length is negative, slice will stop that far from the end.
     *
     * It's possible to mix names/object/numeric, for example:
     *   slice("child1", 2);
     *   slice(3, $child5);
     * Note: when using a child as limit, it will not be included in the returned menu.
     * the slice is done before this menu.
     *
     * @param ItemInterface $item
     * @param mixed         $offset Name of child, child object, or numeric offset.
     * @param mixed         $length Name of child, child object, or numeric length.
     *
     * @return ItemInterface
     */
    public function slice(ItemInterface $item, $offset, $length = null)
    {
        $names = array_keys($item->getChildren());
        if ($offset instanceof ItemInterface) {
            $offset = $offset->getName();
        }
        if (!is_numeric($offset)) {
            $offset = array_search($offset, $names);
        }

        if (null !== $length) {
            if ($length instanceof ItemInterface) {
                $length = $length->getName();
            }
            if (!is_numeric($length)) {
                $index = array_search($length, $names);
                $length = ($index < $offset) ? 0 : $index - $offset;
            }
        }

        $slicedItem = $item->copy();
        $children = array_slice($slicedItem->getChildren(), $offset, $length);
        $slicedItem->setChildren($children);

        return $slicedItem;
    }

    /**
     * Split menu into two distinct menus.
     *
     * @param ItemInterface $item
     * @param mixed         $length Name of child, child object, or numeric length.
     *
     * @return array Array with two menus, with "primary" and "secondary" key
     */
    public function split(ItemInterface $item, $length)
    {
        return array(
            'primary' => $this->slice($item, 0, $length),
            'secondary' => $this->slice($item, $length),
        );
    }

    /**
     * Calls a method recursively on all of the children of this item
     *
     * @example
     * $menu->callRecursively('setShowChildren', array(false));
     *
     * @param ItemInterface $item
     * @param string        $method
     * @param array         $arguments
     */
    public function callRecursively(ItemInterface $item, $method, $arguments = array())
    {
        call_user_func_array(array($item, $method), $arguments);

        foreach ($item->getChildren() as $child) {
            $this->callRecursively($child, $method, $arguments);
        }
    }

    /**
     * A string representation of this menu item
     *
     * e.g. Top Level > Second Level > This menu
     *
     * @param ItemInterface $item
     * @param string        $separator
     *
     * @return string
     */
    public function getPathAsString(ItemInterface $item, $separator = ' > ')
    {
        $children = array();
        $obj = $item;

        do {
            $children[] = $obj->getLabel();
        } while ($obj = $obj->getParent());

        return implode($separator, array_reverse($children));
    }

    /**
     * @param ItemInterface $item
     * @param integer|null  $depth the depth until which children should be exported (null means unlimited)
     *
     * @return array
     */
    public function toArray(ItemInterface $item, $depth = null)
    {
        $array = array(
            'name' => $item->getName(),
            'label' => $item->getLabel(),
            'uri' => $item->getUri(),
            'attributes' => $item->getAttributes(),
            'labelAttributes' => $item->getLabelAttributes(),
            'linkAttributes' => $item->getLinkAttributes(),
            'childrenAttributes' => $item->getChildrenAttributes(),
            'extras' => $item->getExtras(),
            'display' => $item->isDisplayed(),
            'displayChildren' => $item->getDisplayChildren(),
            'current' => $item->isCurrent(),
        );

        // export the children as well, unless explicitly disabled
        if (0 !== $depth) {
            $childDepth = null === $depth ? null : $depth - 1;
            $array['children'] = array();
            foreach ($item->getChildren() as $key => $child) {
                $array['children'][$key] = $this->toArray($child, $childDepth);
            }
        }

        return $array;
    }

    /**
     * Renders an array ready to be used for breadcrumbs.
     *
     * Each element in the array will be an array with 3 keys:
     * - `label` containing the label of the item
     * - `url` containing the url of the item (may be `null`)
     * - `item` containing the original item (may be `null` for the extra items)
     *
     * The subItem can be one of the following forms
     *   * 'subItem'
     *   * ItemInterface object
     *   * array('subItem' => '@homepage')
     *   * array('subItem1', 'subItem2')
     *   * array(array('label' => 'subItem1', 'url' => '@homepage'), array('label' => 'subItem2'))
     *
     * @param ItemInterface $item
     * @param mixed         $subItem A string or array to append onto the end of the array
     *
     * @return array
     *
     * @throws \InvalidArgumentException if an element of the subItem is invalid
     */
    public function getBreadcrumbsArray(ItemInterface $item, $subItem = null)
    {
        $breadcrumbs = $this->buildBreadcrumbsArray($item);

        if (null === $subItem) {
            return $breadcrumbs;
        }

        if ($subItem instanceof ItemInterface) {
            $breadcrumbs[] = $this->getBreadcrumbsItem($subItem);

            return $breadcrumbs;
        }

        if (!is_array($subItem) && !$subItem instanceof \Traversable) {
            $subItem = array($subItem);
        }

        foreach ($subItem as $key => $value) {
            switch (true) {
                case $value instanceof ItemInterface:
                    $value = $this->getBreadcrumbsItem($value);
                    break;

                case is_array($value):
                    // Assume we already have the appropriate array format for the element
                    break;

                case is_integer($key) && is_string($value):
                    $value = array(
                        'label' => (string) $value,
                        'uri' => null,
                        'item' => null,
                    );
                    break;

                case is_scalar($value):
                    $value = array(
                        'label' => (string) $key,
                        'uri' => (string) $value,
                        'item' => null,
                    );
                    break;

                case null === $value:
                    $value = array(
                        'label' => (string) $key,
                        'uri' => null,
                        'item' => null,
                    );
                    break;

                default:
                    throw new \InvalidArgumentException(sprintf('Invalid value supplied for the key "%s". It should be an item, an array or a scalar', $key));
            }

            $breadcrumbs[] = $value;
        }

        return $breadcrumbs;
    }

    private function buildBreadcrumbsArray(ItemInterface $item)
    {
        $breadcrumb = array();

        do {
            $breadcrumb[] = $this->getBreadcrumbsItem($item);
        } while ($item = $item->getParent());

        return array_reverse($breadcrumb);
    }

    private function getBreadcrumbsItem(ItemInterface $item)
    {
        return array(
            'label' => $item->getLabel(),
            'uri' => $item->getUri(),
            'item' => $item,
        );
    }
}


<?php
namespace DTS\eBaySDK\Merchandising\Services;

/**
 * Base class for the Merchandising service.
 */
class MerchandisingBaseService extends \DTS\eBaySDK\Services\BaseService
{
    /**
     * HTTP header constant. The API version your application supports.
     */
    const HDR_API_VERSION = 'X-EBAY-SOA-SERVICE-VERSION';

    /**
     * HTTP header constant. Your application ID.
     */
    const HDR_APP_ID = 'EBAY-SOA-CONSUMER-ID';

    /**
     * HTTP header constant. The global ID of the eBay site the request is for.
     */
    const HDR_GLOBAL_ID = 'X-EBAY-SOA-GLOBAL-ID';

    /**
     * HTTP header constant. The name of the operation you are calling.
     */
    const HDR_OPERATION_NAME = 'X-EBAY-SOA-OPERATION-NAME';

    /**
     * @param array $config Configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct('https://svcs.ebay.com/MerchandisingService', 'https://svcs.sandbox.ebay.com/MerchandisingService', $config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Merchandising\Services\MerchandisingService::API_VERSION
            ],
            'globalId' => [
                'valid' => ['string']
            ]
        ];
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @param string $operationName The name of the operation been called.
     *
     * @return array An associative array of eBay http headers.
     */
    protected function getEbayHeaders($operationName)
    {
        $headers = [];

        // Add required headers first.
        $headers[self::HDR_APP_ID] = $this->getConfig('credentials')->getAppId();
        $headers[self::HDR_OPERATION_NAME] = $operationName;

        // Add optional headers.
        if ($this->getConfig('apiVersion')) {
            $headers[self::HDR_API_VERSION] = $this->getConfig('apiVersion');
        }

        if ($this->getConfig('globalId')) {
            $headers[self::HDR_GLOBAL_ID] = $this->getConfig('globalId');
        }

        return $headers;
    }
}


<?php
namespace DTS\eBaySDK\Product\Services;

/**
 * Base class for the Product service.
 */
class ProductBaseService extends \DTS\eBaySDK\Services\BaseService
{
    /**
     * HTTP header constant. The API version your application supports.
     */
    const HDR_API_VERSION = 'X-EBAY-SOA-SERVICE-VERSION';

    /**
     * HTTP header constant. Your application ID.
     */
    const HDR_APP_ID = 'X-EBAY-SOA-SECURITY-APPNAME';

    /**
     * HTTP header constant. The global ID of the eBay site the request is for.
     */
    const HDR_GLOBAL_ID = 'X-EBAY-SOA-GLOBAL-ID';

    /**
     * HTTP header constant. The name of the operation you are calling.
     */
    const HDR_OPERATION_NAME = 'X-EBAY-SOA-OPERATION-NAME';

    /**
     * @param array $config Configuration option values.
     */
    public function __construct(array $config)
    {
        parent::__construct('https://svcs.ebay.com/services/marketplacecatalog/ProductService/v1', 'https://svcs.sandbox.ebay.com/services/marketplacecatalog/ProductService/v1', $config);
    }

    /**
     * Returns definitions for each configuration option that is supported.
     *
     * @return array An associative array of configuration definitions.
     */
    public static function getConfigDefinitions()
    {
        $definitions = parent::getConfigDefinitions();

        return $definitions + [
            'apiVersion' => [
                'valid' => ['string'],
                'default' => \DTS\eBaySDK\Product\Services\ProductService::API_VERSION
            ],
            'globalId' => [
                'valid' => ['string']
            ]
        ];
    }

    /**
     * Builds the needed eBay HTTP headers.
     *
     * @param string $operationName The name of the operation been called.
     *
     * @return array An associative array of eBay http headers.
     */
    protected function getEbayHeaders($operationName)
    {
        $headers = [];

        // Add required headers first.
        $headers[self::HDR_APP_ID] = $this->getConfig('credentials')->getAppId();
        $headers[self::HDR_OPERATION_NAME] = $operationName;

        // Add optional headers.
        if ($this->getConfig('apiVersion')) {
            $headers[self::HDR_API_VERSION] = $this->getConfig('apiVersion');
        }

        if ($this->getConfig('globalId')) {
            $headers[self::HDR_GLOBAL_ID] = $this->getConfig('globalId');
        }

        return $headers;
    }
}