<?php

declare(strict_types=1);

namespace Mcfedr\AwsPushBundle\Message;

use Mcfedr\AwsPushBundle\Exception\MessageTooLongException;

class Message implements \JsonSerializable
{
    /**
     * @deprecated use NO_COLLAPSE
     * @see NO_COLLAPSE
     */
    const GCM_NO_COLLAPSE = 'do_not_collapse';

    const NO_COLLAPSE = 'do_not_collapse';

    const APNS_MAX_LENGTH = 2048;
    const GCM_MAX_LENGTH = 4096;
    const ADM_MAX_LENGTH = 6144;

    const PLATFORM_GCM = 'gcm';
    const PLATFORM_APNS = 'apns';
    const PLATFORM_ADM = 'adm';

    const PRIORITY_HIGH = 'high';
    const PRIORITY_NORMAL = 'normal';

    /**
     * The text is automatically trimmed when sending to APNS and GCM
     * The text will be sent to GCM and ADM as 'message' in the data field.
     *
     * @var ?string
     */
    private $text;

    /**
     * This is notification priority for GCM should be 'high' or 'normal'. High priority is default.
     *
     * @var string
     */
    private $priority = self::PRIORITY_HIGH;

    /**
     * The key of a localized string that will form the message displayed.
     *
     * @var ?string
     */
    private $localizedKey;

    /**
     * Arguments for the localized message
     * If you are using iOS these should be strings only, plural localization doesn't work!
     *
     * @var ?array
     */
    private $localizedArguments;

    /**
     * If set then the text content of the message will be trimmed where necessary to fit in the length limits of each
     * platform.
     *
     * @var bool
     *
     * @see Message::APNS_MAX_LENGTH
     * @see Message::GCM_MAX_LENGTH
     */
    private $allowTrimming = true;

    /**
     * APNS only.
     *
     * @var ?int
     */
    private $badge;

    /**
     * APNS only
     * Including this key means that when your app is launched in the background or resumed.
     *
     * @var ?bool
     */
    private $contentAvailable;

    /**
     * APNS only.
     *
     * @var ?string
     */
    private $sound;

    /**
     * This is the data to send to all services, it will be deep merged with the other data.
     *
     * @var array
     */
    private $custom = [];

    /**
     * If set, will be sent to GCM, deep merged with ['message' => $text] in the data field.
     *
     * @var ?array
     */
    private $gcmData;

    /**
     * If set, will be sent to ADM, deep merged with ['message' => $text] in the data field.
     *
     * @var ?array
     */
    private $admData;

    /**
     * If set, will be sent to APNS, deep merged as the top level.
     *
     * @var ?array
     */
    private $apnsData;

    /**
     * GCM and ADM only.
     *
     * @var string
     */
    private $collapseKey = self::NO_COLLAPSE;

    /**
     * GCM and ADM only.
     *
     * GCM: default is 4 weeks
     * ADM: default is 1 week
     *
     * @var ?int number of seconds that the server should retain the message
     */
    private $ttl;

    /**
     * GCM only.
     *
     * @var bool
     */
    private $delayWhileIdle = false;

    /**
     * Platforms that this message will create JSON for, and throw errors for.
     *
     * @var array
     *
     * @see Message::PLATFORM_GCM
     * @see Message::PLATFORM_APNS
     * @see Message::PLATFORM_ADM
     */
    private $platforms = [self::PLATFORM_GCM, self::PLATFORM_APNS, self::PLATFORM_ADM];

    public function __construct(?string $text = null)
    {
        $this->text = $text;
    }

    public function getBadge(): ?int
    {
        return $this->badge;
    }

    /**
     * Set the number on displayed badge
     * APNS only.
     */
    public function setBadge(?int $badge): self
    {
        $this->badge = $badge;

        return $this;
    }

    public function isContentAvailable(): ?bool
    {
        return $this->contentAvailable;
    }

    /**
     * Including this key means that when your app is launched in the background or resumed
     * APNS only.
     */
    public function setContentAvailable(?bool $contentAvailable): self
    {
        $this->contentAvailable = $contentAvailable;

        return $this;
    }

    public function getCustom(): array
    {
        return $this->custom;
    }

    /**
     * This is the data to send to all services, it will be deep merged with the other data.
     */
    public function setCustom(array $custom): self
    {
        $this->custom = $custom;

        return $this;
    }

    public function getSound(): ?string
    {
        return $this->sound;
    }

    /**
     * Name of sound file to use
     * APNS only.
     */
    public function setSound(?string $sound): self
    {
        $this->sound = $sound;

        return $this;
    }

    public function getText(): ?string
    {
        return $this->text;
    }

    /**
     * The text is automatically trimmed when sending to APNS and GCM
     * The text will be sent to GCM and ADM as 'message' in the data field.
     */
    public function setText(?string $text): self
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Get Priority.
     */
    public function getPriority(): string
    {
        return $this->priority;
    }

    /**
     * Set Priority.
     */
    public function setPriority(string $priority): self
    {
        $this->priority = $priority;

        return $this;
    }

    public function getLocalizedKey(): ?string
    {
        return $this->localizedKey;
    }

    /**
     * The key of a localized string that will form the message displayed.
     */
    public function setLocalizedKey(?string $localizedKey): self
    {
        $this->localizedKey = $localizedKey;

        return $this;
    }

    public function getLocalizedArguments(): ?array
    {
        return $this->localizedArguments;
    }

    /**
     * Arguments for the localized message
     * If you are using iOS these should be strings only, plural localization doesn't work!
     */
    public function setLocalizedArguments(?array $localizedArguments = null): self
    {
        $this->localizedArguments = $localizedArguments;

        return $this;
    }

    /**
     * Convenience to set localized text.
     */
    public function setLocalizedText(?string $key, ?array $arguments = null): self
    {
        $this->setLocalizedKey($key);
        $this->setLocalizedArguments($arguments);

        return $this;
    }

    public function getAllowTrimming(): bool
    {
        return $this->allowTrimming;
    }

    /**
     * If set then the text content of the message will be trimmed where necessary to fit in the length limits of each
     * platform.
     */
    public function setAllowTrimming(bool $allowTrimming): self
    {
        $this->allowTrimming = $allowTrimming;

        return $this;
    }

    public function getCollapseKey(): string
    {
        return $this->collapseKey;
    }

    /**
     * GCM and ADM only.
     */
    public function setCollapseKey(?string $collapseKey): self
    {
        $this->collapseKey = $collapseKey ?: static::NO_COLLAPSE;

        return $this;
    }

    public function getDelayWhileIdle(): bool
    {
        return $this->delayWhileIdle;
    }

    /**
     * GCM only.
     */
    public function setDelayWhileIdle(bool $delayWhileIdle): self
    {
        $this->delayWhileIdle = $delayWhileIdle;

        return $this;
    }

    public function getTtl(): ?int
    {
        return $this->ttl;
    }

    /**
     * number of seconds that the server should retain the message.
     *
     * GCM and ADM only
     */
    public function setTtl(?int $ttl): self
    {
        $this->ttl = $ttl;

        return $this;
    }

    public function getGcmData(): ?array
    {
        return $this->gcmData;
    }

    /**
     * If set, will be sent to GCM, deep merged with ['message' => $text] in the data field.
     */
    public function setGcmData(?array $gcmData): self
    {
        $this->gcmData = $gcmData;

        return $this;
    }

    public function getAdmData(): ?array
    {
        return $this->admData;
    }

    /**
     * If set, will be sent to ADM, deep merged with ['message' => $text] in the data field.
     */
    public function setAdmData(?array $admData): self
    {
        $this->admData = $admData;

        return $this;
    }

    public function getApnsData(): ?array
    {
        return $this->apnsData;
    }

    /**
     * If set, will be sent to APNS, deep merged as the top level, meaning you can add extra data to 'aps'.
     */
    public function setApnsData(?array $apnsData): self
    {
        $this->apnsData = $apnsData;

        return $this;
    }

    public function getPlatforms(): ?array
    {
        return $this->platforms;
    }

    /**
     * Platforms that this message will create JSON for, and throw errors for (for long messages).
     *
     * @see Message::PLATFORM_GCM
     * @see Message::PLATFORM_APNS
     * @see Message::PLATFORM_ADM
     */
    public function setPlatforms(array $platforms): self
    {
        $this->platforms = $platforms;

        return $this;
    }

    public function jsonSerialize()
    {
        $data = [
            'default' => $this->text,
        ];

        if (\in_array(self::PLATFORM_APNS, $this->platforms)) {
            $data['APNS'] = $data['APNS_SANDBOX'] = $data['APNS_VOIP_SANDBOX'] = $data['APNS_VOIP'] = $this->getApnsJson();
        }

        if (\in_array(self::PLATFORM_GCM, $this->platforms)) {
            $data['GCM'] = $this->getGcmJson();
        }

        if (\in_array(self::PLATFORM_ADM, $this->platforms)) {
            $data['ADM'] = $this->getAdmJson();
        }

        return $data;
    }

    public function __toString()
    {
        return json_encode($this);
    }

    /**
     * Get the json to send via Apple Push Notification Server
     * For APNS the max length applies to the whole message.
     *
     * @throws MessageTooLongException
     */
    private function getApnsJson(): string
    {
        return json_encode($this->getTrimmedJson([$this, 'getApnsJsonInner'], static::APNS_MAX_LENGTH, 'You message for APNS is too long'), JSON_UNESCAPED_UNICODE);
    }

    /**
     * Get the correct apple push notification server data.
     */
    private function getApnsJsonInner(?string $text): array
    {
        $apns = [
            'aps' => [],
        ];

        if (null !== $this->localizedKey) {
            $apns['aps']['alert'] = [
                'loc-key' => $this->localizedKey,
            ];
            if ($this->localizedArguments) {
                $apns['aps']['alert']['loc-args'] = $this->localizedArguments;
            }
        } elseif (null !== $text) {
            $apns['aps']['alert'] = $text;
        }

        if ($this->isContentAvailable()) {
            $apns['aps']['content-available'] = 1;
        }

        if (null !== $this->badge) {
            $apns['aps']['badge'] = $this->badge;
        }

        if (null !== $this->sound) {
            $apns['aps']['sound'] = $this->sound;
        }

        $merged = $this->arrayMergeDeep($apns, $this->custom, $this->apnsData ? $this->apnsData : []);

        // Force aps to be an object, because it shouldnt get encoded as [] but as {}
        if (!\count($merged['aps'])) {
            $merged['aps'] = new \stdClass();
        }

        return $merged;
    }

    /**
     * Get the json to send via Amazon Device Messaging.
     */
    private function getAdmJson(): string
    {
        $adm = [
            'data' => $this->getTrimmedJson([$this, 'getAdmJsonInner'], static::ADM_MAX_LENGTH, 'You message for ADM is too long'),
            'expiresAfter' => $this->ttl,
        ];

        foreach ($adm['data'] as $key => $value) {
            if (!\is_string($value)) {
                $adm['data']["{$key}_json"] = json_encode($value);
                unset($adm['data'][$key]);
            }
        }

        if ($this->collapseKey != static::NO_COLLAPSE) {
            $adm['consolidationKey'] = $this->collapseKey;
        }

        return json_encode($adm, JSON_UNESCAPED_UNICODE);
    }

    /**
     * Gets the data part of the GCM message.
     */
    private function getAdmJsonInner(?string $text): array
    {
        $data = $this->getAndroidJsonInner($text);

        $merged = $this->arrayMergeDeep($data, $this->custom, $this->admData ? $this->admData : []);

        // Force to be an object, because it shouldnt get encoded as [] but as {}
        if (!\count($merged)) {
            $merged = new \stdClass();
        }

        return $merged;
    }

    /**
     * Get the json to send via Google Cloud Messaging
     * For GCM the max length is for the data field only.
     *
     * @throws MessageTooLongException
     */
    private function getGcmJson(): string
    {
        return json_encode([
            'collapse_key' => $this->collapseKey,
            'time_to_live' => $this->ttl,
            'delay_while_idle' => $this->delayWhileIdle,
            'priority' => $this->priority,
            'data' => $this->getTrimmedJson([$this, 'getGcmJsonInner'], static::GCM_MAX_LENGTH, 'You message for GCM is too long'),
        ], JSON_UNESCAPED_UNICODE);
    }

    /**
     * Gets the data part of the GCM message.
     */
    private function getGcmJsonInner(?string $text): array
    {
        $data = $this->getAndroidJsonInner($text);

        $merged = $this->arrayMergeDeep($data, $this->custom, $this->gcmData ? $this->gcmData : []);

        // Force to be an object, because it shouldnt get encoded as [] but as {}
        if (!\count($merged)) {
            $merged = new \stdClass();
        }

        return $merged;
    }

    /**
     * Gets the base of the data for the android platforms, with text and localization keys.
     */
    private function getAndroidJsonInner(?string $text): array
    {
        $data = [];
        if (null !== $text) {
            $data['message'] = $text;
        }

        if (null !== $this->localizedKey) {
            $data['message-loc-key'] = $this->localizedKey;
            if ($this->localizedArguments) {
                $data['message-loc-args'] = $this->localizedArguments;
            }
        }

        return $data;
    }

    /**
     * Using a inner function gets the data, and trys again if its too long by trimming the text.
     *
     * @throws MessageTooLongException
     */
    private function getTrimmedJson(callable $inner, int $limit, string $error): array
    {
        $gcmInner = $inner($this->text);
        $gcmInnerJson = json_encode($gcmInner, JSON_UNESCAPED_UNICODE);
        if (($gcmInnerJsonLength = \strlen($gcmInnerJson)) > $limit) {
            $cut = $gcmInnerJsonLength - $limit;
            //Note that strlen returns the byte length of the string
            if ($this->text && ($textLength = \strlen($this->text)) > $cut && $this->allowTrimming) {
                $gcmInner = $inner(mb_strcut($this->text, 0, $textLength - $cut - 3, 'utf8').'...');
            } else {
                throw new MessageTooLongException("$error $gcmInnerJson");
            }
        }

        return $gcmInner;
    }

    /**
     * Merge arrays, deeply.
     */
    private function arrayMergeDeep(array $array1, array $array2): array
    {
        $result = [];
        foreach (\func_get_args() as $array) {
            foreach ($array as $key => $value) {
                // Renumber integer keys as array_merge_recursive() does. Note that PHP
                // automatically converts array keys that are integer strings (e.g., '1')
                // to integers.
                if (\is_int($key)) {
                    $result[] = $value;
                } elseif (isset($result[$key]) && \is_array($result[$key]) && \is_array($value)) {
                    // Recurse when both values are arrays.
                    $result[$key] = $this->arrayMergeDeep($result[$key], $value);
                } else {
                    // Otherwise, use the latter value, overriding any previous value.
                    $result[$key] = $value;
                }
            }
        }

        return $result;
    }
}


<?php
namespace Gt\Dom;

use DOMDocument;
use DOMElement;
use DOMXPath;
use Symfony\Component\CssSelector\CssSelectorConverter;

/**
 * The most general base class from which all objects in a Document inherit.
 *
 * @property-read Attr[] $attributes
 * @property string $className Gets and sets the value of the class attribute
 * @property-read TokenList $classList Returns a live TokenList collection of
 * the class attributes of the element
 * @property bool $checked Indicates whether the element is checked or not
 * @property bool $selected Indicates whether the element is selected or not
 * @property string $value Gets or sets the value of the element according to
 * its element type
 * @property string $id Gets or sets the value of the id attribute
 * @property string $innerHTML Gets or sets the HTML syntax describing the
 * element's descendants
 * @property string $outerHTML Gets or sets the HTML syntax describing the
 * element and its descendants. It can be set to replace the element with nodes
 * parsed from the given string
 * @property string $innerText
 * @property-read StringMap $dataset
 */
class Element extends DOMElement {
	use LiveProperty, NonDocumentTypeChildNode, ChildNode, ParentNode;

	/** @var  TokenList */
	protected $liveProperty_classList;
	/** @var StringMap */
	protected $liveProperty_dataset;
	/**
	 * @const Array
	 * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement#Elements_that_are_considered_form_controls
	 */
	const FORM_CONTROL_ELEMENTS = ['button', 'fieldset', 'input', 'object', 'output', 'select', 'textarea'];

	/**
	 * returns true if the element would be selected by the specified selector
	 * string; otherwise, returns false.
	 * @param string $selectors The CSS selector(s) to check against
	 * @return bool True if this element is selectable by provided selector
	 */
	public function matches(string $selectors):bool {
		$matches = $this->getRootDocument()->querySelectorAll($selectors);
		$i = $matches->length;
		while(--$i >= 0 && $matches->item($i) !== $this) {
			;
		}

		return ($i >= 0);
	}

	/**
	 * Returns a live HTMLCollection containing all child elements which have all
	 * of the given class names. When called on the document object, the complete
	 * document is searched, including the root node.
	 * @param string $names a string representing the list of class names to
	 *  match; class names are separated by whitespace
	 * @return HTMLCollection
	 */
	public function getElementsByClassName(string $names):HTMLCollection {
		$namesArray = explode(" ", $names);
		$dots = "." . implode(".", $namesArray);

		return $this->css($dots);
	}

	/**
	 * Returns the closest ancestor of the current element (or itself)
	 * which matches the selectors.
	 * @param string $selectors CSS selector(s)
	 * @return Element|null
	 */
	public function closest(string $selectors) {
		$collection = $this->css($selectors, "ancestor-or-self::");

		return $collection->item(count($collection) - 1);
	}

	public function getAttribute($name):?string {
		$value = parent::getAttribute($name);
		if(strlen($value) === 0) {
			return null;
		}

		return $value;
	}

	public function prop_get_className() {
		return $this->getAttribute("class");
	}

	public function prop_set_className(string $value):void {
		$this->setAttribute("class", $value);
	}

	public function prop_get_classList() {
		if(!$this->liveProperty_classList) {
			$this->liveProperty_classList = new TokenList($this, "class");
		}

		return $this->liveProperty_classList;
	}

	public function prop_get_value() {
		$methodName = 'value_get_' . $this->tagName;
		if(method_exists($this, $methodName)) {
			return $this->$methodName();
		}

		return null;
	}

	public function prop_set_value(string $newValue) {
		$methodName = 'value_set_' . $this->tagName;
		if(method_exists($this, $methodName)) {
			return $this->$methodName($newValue);
		}
	}

	public function prop_get_id():?string {
		return $this->getAttribute("id");
	}

	public function prop_set_id(string $newValue):void {
		$this->setAttribute("id", $newValue);
	}

	public function prop_get_innerHTML():string {
		$childHtmlArray = [];
		foreach($this->childNodes as $child) {
			$childHtmlArray [] = $this->ownerDocument->saveHTML($child);
		}

		return implode(PHP_EOL, $childHtmlArray);
	}

	public function prop_set_innerHTML(string $html):void {
		$importDocument = new DOMDocument(
			"1.0",
			"utf-8"
		);

		$htmlWrapped = "<!doctype html>$html";
		$htmlWrapped = mb_convert_encoding(
			$htmlWrapped,
			"HTML-ENTITIES",
			"UTF-8"
		);

		$importDocument->loadHTML($htmlWrapped);
		$importBody = $importDocument->getElementsByTagName(
			"body"
		)->item(0);
		$node = $this->ownerDocument->importNode(
			$importBody,
			true
		);

		while($this->firstChild) {
			$this->removeChild($this->firstChild);
		}

		$fragment = $this->ownerDocument->createDocumentFragment();
		while($node->firstChild) {
			$fragment->appendChild($node->firstChild);
		}

		$this->appendChild($fragment);
	}

	public function prop_get_outerHTML():string {
		return $this->ownerDocument->saveHTML($this);
	}

	public function prop_set_outerHTML(string $html):void {
		$fragment = $this->ownerDocument->createDocumentFragment();
		$fragment->appendXML($html);
		$this->replaceWith($fragment);
	}

	public function prop_get_innerText():string {
		return $this->textContent;
	}

	public function prop_set_innerText(string $value):string {
		$this->textContent = $value;
		return $this->textContent;
	}

	public function prop_get_dataset():StringMap {
		if(empty($this->liveProperty_dataset)) {
			$this->liveProperty_dataset = $this->createDataset();
		}

		return $this->liveProperty_dataset;
	}

	public function prop_get_checked():bool {
		return $this->hasAttribute("checked");
	}

	public function prop_set_checked(bool $checked):bool {
		if($checked) {
			if($this->getAttribute("type") === "radio") {
// TODO: Use `form` attribute when implemented: https://github.com/PhpGt/Dom/issues/161
				$parentForm = $this->closest("form");
				if(!is_null($parentForm)) {
					$this->removeAttributeFromNamedElementAndChildren(
						$parentForm,
						$this->getAttribute("name"),
						"checked"
					);
				}
			}

			$this->setAttribute("checked", "checked");
		}
		else {
			$this->removeAttribute("checked");
		}

		return $this->checked;
	}

	public function prop_get_selected():bool {
		return $this->hasAttribute("selected");
	}

	public function prop_set_selected(bool $selected):bool {
		if($selected) {
			$selectElement = $this->closest("select");
			if(!$selectElement->hasAttribute("multiple")) {
				$this->removeAttributeFromNamedElementAndChildren(
					$selectElement->closest("form"),
					$selectElement->getAttribute("name"),
					"selected"
				);
			}

			$this->setAttribute("selected", true);
		}
		else {
			$this->removeAttribute("selected");
		}

		return $this->selected;
	}

	public function prop_get_form() {
		if (in_array($this->tagName, self::FORM_CONTROL_ELEMENTS)) {
			if ($this->tagName === "input" && $this->getAttribute("type") === "image") {
				return null;
			}

			if ($this->hasAttribute("form")) {
				return $this->getRootDocument()->getElementById($this->getAttribute("form"));
			}
			else {
				return $this->closest('form');
			}
		}

		return null;
	}

	protected function createDataset():StringMap {
		return new StringMap(
			$this,
			$this->attributes
		);
	}

	protected function getRootDocument():DOMDocument {
		return $this->ownerDocument;
	}

	private function value_set_select(string $newValue):void {
		$options = $this->getElementsByTagName('option');
		$selectedIndexes = [];
		$newSelectedIndex = null;

		for($i = $options->length - 1; $i >= 0; --$i) {
			if($this->isSelectOptionSelected($options->item($i))) {
				$selectedIndexes[] = $i;
			}

			if($options->item($i)->getAttribute('value') == $newValue) {
				$newSelectedIndex = $i;
			}
		}

		if($newSelectedIndex !== null) {
			foreach($selectedIndexes as $i) {
				$options->item($i)->removeAttribute('selected');
			}

			$options->item($newSelectedIndex)->setAttribute('selected', 'selected');
		}
	}

	private function value_get_select():string {
		$options = $this->getElementsByTagName('option');
		if($options->length == 0) {
			$value = '';
		}
		else {
			$value = $options->item(0)->getAttribute('value');
		}

		foreach($options as $option) {
			if($this->isSelectOptionSelected($option)) {
				$value = $option->getAttribute('value');
				break;
			}
		}

		return $value;
	}

	private function value_set_input(string $newValue) {
		return $this->setAttribute("value", $newValue);
	}

	private function value_get_input() {
		return $this->getAttribute("value");
	}

	public function isSelectOptionSelected(Element $option) {
		return $option->hasAttribute('selected') && $option->getAttribute('selected');
	}
}


<?php
namespace Gt\Dom;

use DOMNode;

/**
 * Contains methods that are particular to Node objects that can have a parent.
 *
 * This trait is used by the following classes:
 *  - Element
 *  - DocumentType
 *  - CharacterData
 */
trait ChildNode {
	/**
	 * Removes this ChildNode from the children list of its parent.
	 */
	public function remove():void {
		$this->parentNode->removeChild($this);
	}

	/**
	 * Inserts a Node into the children list of this ChildNode's parent,
	 * just before this ChildNode.
	 * @param DOMNode $node
	 * @return void
	 */
	public function before(DOMNode $node):void {
		$this->parentNode->insertBefore($node, $this);
	}

	/**
	 * Inserts a Node into the children list of this ChildNode's parent,
	 * just after this ChildNode.
	 * @param DOMNode $node
	 * @return void
	 */
	public function after(DOMNode $node):void {
		$this->parentNode->insertBefore($node, $this->nextSibling);
	}

	/**
	 * Replace this ChildNode in the children list of its parent with the
	 * supplied replacement node.
	 * @param DOMNode $replacement
	 * @return void
	 */
	public function replaceWith(DOMNode $replacement):void {
		$this->parentNode->insertBefore($replacement, $this);
		$this->remove();
	}
}


<?php
namespace Gt\Dom;

use DOMNodeList;
use ArrayAccess;
use Countable;
use Iterator;

class NodeList implements Iterator, ArrayAccess, Countable {
	use LiveProperty;

	protected $domNodeList;
	protected $iteratorKey;

	public function __construct(DOMNodeList $domNodeList) {
		$this->domNodeList = $domNodeList;
	}

	/**
	 * Returns the number of Elements contained in this Collection. Exposed as the
	 * $length property.
	 * @return int Number of Elements
	 */
	private function prop_get_length():int {
		$key = $this->iteratorKey;

		$length = 0;
		foreach($this as $element) {
			$length++;
		}

		$this->iteratorKey = $key;
		return $length;
	}

	public function getDomNodeList():DOMNodeList {
		return $this->domNodeList;
	}

	/**
	 * Gets the nth Element object in the internal DOMNodeList.
	 * @param int $index
	 * @return Element|null
	 */
	public function item($index) {
		$count = 0;
		foreach($this as $element) {
			if($index === $count) {
				return $element;
			}

			$count++;
		}

		return null;
	}

// Iterator --------------------------------------------------------------------

	public function current():?Element {
		return $this->domNodeList[$this->iteratorKey] ?? null;
	}

	public function key():int {
		return $this->iteratorKey;
	}

	public function next() {
		$this->iteratorKey++;
		$this->incrementKeyToNextElement();
	}

	public function rewind() {
		$this->iteratorKey = 0;
		$this->incrementKeyToNextElement();
	}

	public function valid():bool {
		return isset($this->domNodeList[$this->iteratorKey]);
	}

	private function incrementKeyToNextElement() {
		while($this->valid()
			&& !$this->domNodeList[$this->iteratorKey] instanceof Element) {
			$this->iteratorKey++;
		}
	}

// ArrayAccess -----------------------------------------------------------------
	public function offsetExists($offset):bool {
		return isset($offset, $this->domNodeList);
	}

	public function offsetGet($offset):?Element {
		return $this->item($offset);
	}

	public function offsetSet($offset, $value) {
		return $this->offsetUnset($offset);
	}

	public function offsetUnset($offset) {
		throw new \BadMethodCallException("HTMLCollection's items are read only");
	}

// Countable -------------------------------------------------------------------
	public function count():int {
		return $this->length;
	}
}


<?php
namespace Gt\Dom;

use DOMXPath;
use Symfony\Component\CssSelector\CssSelectorConverter;

/**
 * Contains methods that are particular to Node objects that can have children.
 *
 * This trait can only be used in a class that is a trait of LiveProperty.
 *
 * This trait is used by the following classes:
 *  - Element
 *  - Document and its subclasses XMLDocument and HTMLDocument
 *  - DocumentFragment
 * @property-read HTMLCollection $children A live HTMLCollection containing all
 *  objects of type Element that are children of this ParentNode.
 * @property-read Element|null $firstElementChild The Element that is the first
 *  child of this ParentNode.
 * @property-read Element|null $lastElementChild The Element that is the last
 *  child of this ParentNode.
 * @property-read int $childElementCount The amount of children that the
 *  ParentNode has.
 *
 * @method Node getElementById(string $id)
 */
trait ParentNode {
	/** @return Element|null */
	public function querySelector(string $selector) {
		$htmlCollection = $this->css($selector);

		return $htmlCollection->item(0);
	}

	public function querySelectorAll(string $selector):HTMLCollection {
		return $this->css($selector);
	}

	private function prop_get_children():HTMLCollection {
		return new HTMLCollection($this->childNodes);
	}

	private function prop_get_firstElementChild() {
		return $this->children->item(0);
	}

	private function prop_get_lastElementChild() {
		return $this->children->item($this->children->length - 1);
	}

	private function prop_get_childElementCount() {
		return $this->children->length;
	}

	/**
	 * @param string $selectors CSS selector(s)
	 * @param string $prefix
	 *
	 * @return HTMLCollection
	 */
	public function css(
		string $selectors,
		string $prefix = "descendant-or-self::"
	):HTMLCollection {
		$converter = new CssSelectorConverter();
		$xPathSelector = $converter->toXPath($selectors, $prefix);

		return $this->xPath($xPathSelector);
	}

	public function xPath(string $selector):HTMLCollection {
		$x = new DOMXPath($this->getRootDocument());

		return new HTMLCollection($x->query($selector, $this));
	}

	public function getElementsByTagName($name) {
		$nodeList = parent::getElementsByTagName($name);
		if($nodeList instanceof NodeList) {
			return $nodeList;
		}

		return new NodeList($nodeList);
	}

	public function removeAttributeFromNamedElementAndChildren(
		Element $form,
		string $name,
		string $attribute
	):void {
		$selector = "[name='$name'], [name='$name'] *";
		foreach($form->querySelectorAll($selector) as $element) {
			$element->removeAttribute($attribute);
		}
	}

	/**
	 * Normalises access to the parent dom document, which may be located in various places
	 * depending on what type of object is using the trait
	 *
	 * @return \DOMDocument
	 */
	protected abstract function getRootDocument():\DOMDocument;
}


<?php
namespace Gt\Dom;

/**
 * Represents a set of space-separated tokens. Such a set is returned by
 * Element::$classList, HTMLLinkElement::$relList, HTMLAnchorElement::$relList
 * or HTMLAreaElement::$relList.
 *
 * It is indexed beginning with 0 as with arrays.
 *
 * DOMTokenList is always case-sensitive.
 * @property-read int $length
 */
class TokenList {
	use LiveProperty;

	private $element;
	private $attributeName;
	private $tokenArray;

	public function __construct(Element $element, string $attributeName) {
		$this->element = $element;
		$this->attributeName = $attributeName;
	}

	/**
	 * Tokenises the internal element's named attribute to $this->tokenArray.
	 */
	private function tok():void {
		$attributeValue = $this->element->getAttribute($this->attributeName);
		$this->tokenArray = array_filter(
			explode(" ", $attributeValue)
		);
	}

	/**
	 * Untokenises $this->tokenArray to the internal element's named attribute.
	 */
	private function untok():void {
		$attributeValue = implode(" ", $this->tokenArray);
		$this->element->setAttribute($this->attributeName, trim($attributeValue));
	}

	private function prop_get_length() {
		$this->tok();

		return count($this->tokenArray);
	}

	/**
	 * Returns an item in the list by its index (or null if the number is
	 * greater than or equal to the length of the list).
	 * @param int $index
	 * @return string|null
	 */
	public function item(int $index) {
		$this->tok();

		return $this->tokenArray[$index] ?? null;
	}

	/**
	 * Returns true if the underlying string contains $token, otherwise false.
	 * @param string $token
	 * @return bool
	 */
	public function contains(string $token) {
		$this->tok();

		return in_array($token, $this->tokenArray);
	}

	/**
	 * Adds $token to the underlying attribute value.
	 * @param string $token
	 * @return void
	 */
	public function add(string $token):void {
		if($this->contains($token)) {
			return;
		}

		$this->tokenArray [] = $token;

		$this->untok();
	}

	/**
	 * Removes $token from the underlying attribute value.
	 * @param string $token
	 * @return null
	 */
	public function remove(string $token):void {
		if(!$this->contains($token)) {
			return;
		}

		$index = array_search($token, $this->tokenArray);

		unset($this->tokenArray[$index]);
		$this->untok();
	}

	/**
	 * Removes $token from the underlying attribute value and returns false. If
	 * $token doesn't exist, it's added and the function returns true.
	 * @param string $token
	 * @return bool true if token is added, false if token is removed.
	 */
	public function toggle(string $token):bool {
		if($this->contains($token)) {
			$this->remove($token);

			return false;
		}

		$this->add($token);

		return true;
	}
}


<?php

namespace Ds\Component\Api\Service;

use Ds\Component\Api\Model\OrganizationPersona;
use Ds\Component\Api\Query\OrganizationPersonaParameters as Parameters;

/**
 * Class OrganizationPersonaService
 *
 * @package Ds\Component\Api
 */
final class OrganizationPersonaService implements Service
{
    use Base;

    /**
     * @const string
     */
    const MODEL = OrganizationPersona::class;

    /**
     * @const string
     */
    const RESOURCE_LIST = '/organization_personas';
    const RESOURCE_OBJECT = '/organization_personas/{id}';

    /**
     * @var array
     */
    private static $map = [
        'id',
        'uuid',
        'createdAt',
        'updatedAt',
        'owner',
        'ownerUuid',
        'organization',
        'title',
        'data',
        'version',
        'tenant'
    ];

    /**
     * Get organization persona list
     *
     * @param \Ds\Component\Api\Query\OrganizationPersonaParameters $parameters
     * @return array
     */
    public function getList(Parameters $parameters = null): array
    {
        $options = [];

        if ($parameters) {
            $options['query'] = (array) $parameters->toObject(true);

            if (array_key_exists('organizationUuid', $options['query'])) {
                $options['query']['organization.uuid'] = $options['query']['organizationUuid'];
                unset($options['query']['organizationUuid']);
            }
        }

        $objects = $this->execute('GET', static::RESOURCE_LIST, $options);
        $list = [];

        foreach ($objects as $object) {
            $model = static::toModel($object);
            $list[] = $model;
        }

        return $list;
    }

    /**
     * Get organization persona
     *
     * @param string $id
     * @param \Ds\Component\Api\Query\OrganizationPersonaParameters $parameters
     * @return \Ds\Component\Api\Model\OrganizationPersona
     */
    public function get($id, Parameters $parameters = null): OrganizationPersona
    {
        $resource = str_replace('{id}', $id, static::RESOURCE_OBJECT);
        $object = $this->execute('GET', $resource);
        /** @var \Ds\Component\Api\Model\OrganizationPersona $model */
        $model = static::toModel($object);

        return $model;
    }

    /**
     * Create organization persona
     *
     * @param \Ds\Component\Api\Model\OrganizationPersona $persona
     * @param \Ds\Component\Api\Query\OrganizationPersonaParameters $parameters
     * @return \Ds\Component\Api\Model\OrganizationPersona
     */
    public function create(OrganizationPersona $persona, Parameters $parameters = null): OrganizationPersona
    {
        $options = [];
        $options['json'] = (array) static::toObject($persona);

        if ($parameters) {
            $options['query'] = (array) $parameters->toObject(true);
        }

        $object = $this->execute('POST', static::RESOURCE_LIST, $options);
        /** @var \Ds\Component\Api\Model\OrganizationPersona $persona */
        $persona = static::toModel($object);

        return $persona;
    }
}


<?php

namespace Ds\Component\Security\EventListener\Token;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PropertyAccess\PropertyAccess;

/**
 * Class IpListener
 *
 * @package Ds\Component\Security
 */
final class IpListener
{
    /**
     * @var \Symfony\Component\PropertyAccess\PropertyAccessor
     */
    private $accessor;

    /**
     * @var \Symfony\Component\HttpFoundation\RequestStack
     */
    private $requestStack;

    /**
     * @var boolean
     */
    private $validate;

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

    /**
     * Constructor
     *
     * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
     * @param boolean $validate
     * @param string $property
     */
    public function __construct(RequestStack $requestStack, bool $validate = true, string $property = '[ip]')
    {
        $this->accessor = PropertyAccess::createPropertyAccessor();
        $this->requestStack = $requestStack;
        $this->validate = $validate;
        $this->property = $property;
    }

    /**
     * Add the ip to the token
     *
     * @param \Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent $event
     */
    public function created(JWTCreatedEvent $event)
    {
        $request = $this->requestStack->getCurrentRequest();
        $data = $event->getData();
        $this->accessor->setValue($data, $this->property, $request->getClientIp());
        $event->setData($data);
    }

    /**
     * Mark the token as invalid if the ip is missing or is not valid
     *
     * @param \Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent $event
     */
    public function decoded(JWTDecodedEvent $event)
    {
        $request = $this->requestStack->getCurrentRequest();
        $payload = $event->getPayload();

        // Make property accessor paths compatible by converting payload to recursive associative array
        $payload = json_decode(json_encode($payload), true);

        if (!$this->accessor->isReadable($payload, $this->property)) {
            $event->markAsInvalid();
        } elseif ($this->validate && $this->accessor->getValue($payload, $this->property) !== $request->getClientIp()) {
            $event->markAsInvalid();
        }
    }
}


<?php

namespace Ds\Component\Model\Attribute\Accessor;

use DomainException;

/**
 * Trait Assignee
 *
 * @package Ds\Component\Model
 */
trait Assignee
{
    /**
     * Set assignee
     *
     * @param string $assignee
     * @return object
     * @throws \DomainException
     */
    public function setAssignee(?string $assignee)
    {
        if (null !== $assignee) {
            if (!preg_match('/^[a-z]+$/i', $assignee)) {
                throw new DomainException('Assignee is not valid.');
            }
        }

        $this->assignee = $assignee;

        return $this;
    }

    /**
     * Get assignee
     *
     * @return string
     */
    public function getAssignee(): ?string
    {
        return $this->assignee;
    }
}


<?php

namespace Ds\Component\Tenant\Service;

use Doctrine\ORM\EntityManagerInterface;
use Ds\Component\Entity\Service\EntityService;
use Ds\Component\Parameter\Service\ParameterService;
use Ds\Component\Security\Model\User;
use Ds\Component\Tenant\Collection\LoaderCollection;
use Ds\Component\Tenant\Collection\UnloaderCollection;
use Ds\Component\Tenant\Entity\Tenant;
use Exception;
use LogicException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Class TenantService
 *
 * @package Ds\Component\Tenant\Service
 */
final class TenantService extends EntityService
{
    /**
     * @var \Ds\Component\Parameter\Service\ParameterService
     */
    private $parameterService;

    /**
     * @var \Symfony\Component\HttpFoundation\RequestStack
     */
    private $requestStack;

    /**
     * @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var \Ds\Component\Tenant\Collection\LoaderCollection
     */
    private $loaderCollection;

    /**
     * @var \Ds\Component\Tenant\Collection\UnloaderCollection
     */
    private $unloaderCollection;

    /**
     * Constructor
     *
     * @param \Doctrine\ORM\EntityManagerInterface $manager
     * @param \Ds\Component\Parameter\Service\ParameterService $parameterService
     * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
     * @param \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface $tokenStorage
     * @param \Ds\Component\Tenant\Collection\LoaderCollection $loaderCollection
     * @param \Ds\Component\Tenant\Collection\UnloaderCollection $unloaderCollection
     * @param string $entity
     */
    public function __construct(EntityManagerInterface $manager, ParameterService $parameterService, RequestStack $requestStack, TokenStorageInterface $tokenStorage, LoaderCollection $loaderCollection, UnloaderCollection $unloaderCollection, string $entity = Tenant::class)
    {
        parent::__construct($manager, $entity);
        $this->parameterService = $parameterService;
        $this->requestStack = $requestStack;
        $this->tokenStorage = $tokenStorage;
        $this->loaderCollection = $loaderCollection;
        $this->unloaderCollection = $unloaderCollection;
    }

    /**
     * Get the contextual tenant
     *
     * @return string
     */
    public function getContext(): string
    {
        $tenant = $this->parameterService->get('ds_tenant.tenant.default');
        $request = $this->requestStack->getCurrentRequest();

        if ($request->request->has('tenant') && $request->request->get('tenant')) {
            $tenant = $request->request->get('tenant');
        }

        if ($request->query->has('tenant') && $request->query->get('tenant')) {
            $tenant = $request->query->get('tenant');
        }

        $token = $this->tokenStorage->getToken();

        if ($token) {
            $user = $token->getUser();

            if ($user instanceof User) {
                $tenant = $user->getTenant();
            }
        }

        return $tenant;
    }

    /**
     * Load tenant
     *
     * @param \Ds\Component\Tenant\Entity\Tenant
     * @return \Ds\Component\Tenant\Service\TenantService
     * @throws \Exception
     * @example
     * <code>
     * [
     *     'user' => [
     *         'system' => [
     *             'uuid' => 'd9e66aeb-d853-41bc-ad9e-ae6fb18fb067',
     *             'password' => 'password'
     *         ],
     *         'admin' => [
     *             'uuid' => '28de3e41-87c1-47c9-813d-07a87278fc00',
     *             'password' => 'password'
     *         ]
     *     ],
     *     'identity' => [
     *         'system' => [
     *             'uuid' => '782637a1-7366-4805-a853-869f65fa3965'
     *         ],
     *         'admin' => [
     *             'uuid' => '782637a1-7366-4805-a853-869f65fa3965'
     *         ]
     *     ],
     *     'business_unit' => [
     *         'administration' => [
     *             'uuid' => '5675406b-f2ed-453a-b3ef-5be4596e7da6'
     *         ]
     *     ],
     *     'tenant' => [
     *         'uuid' => '10e287ef-fcd0-45e1-b178-b44be9f2363a'
     *     ],
     *     'config' => [
     *         'app.spa.admin' => [
     *             'value' => 'admin.ds'
     *         ],
     *         'app.spa.portal' => [
     *             'value' => 'portal.ds'
     *         ]
     *     ]
     * ]
     * </code>
     */
    public function load(Tenant $tenant): TenantService
    {
        $this->disable('tenant');
        $connection = $this->manager->getConnection();
        $connection->beginTransaction();

        try {
            foreach ($this->loaderCollection as $loader) {
                $loader->load($tenant);
            }

            $connection->commit();
        } catch (Exception $exception) {
            $connection->rollBack();
            throw $exception;
        }

        $this->enable('tenant');

        return $this;
    }

    /**
     * Unload tenant
     *
     * @param \Ds\Component\Tenant\Entity\Tenant
     * @return \Ds\Component\Tenant\Service\TenantService
     * @throws Exception
     */
    public function unload(Tenant $tenant): TenantService
    {
        $this->disable('tenant');
        $connection = $this->manager->getConnection();
        $connection->beginTransaction();

        try {
            foreach ($this->unloaderCollection as $unloader) {
                $unloader->unload($tenant);
            }

            $connection->commit();
        } catch (Exception $exception) {
            $connection->rollBack();
            throw $exception;
        }

        $this->enable('tenant');

        return $this;
    }

    /**
     * Enable filter
     *
     * @param string $filter
     * @return \Ds\Component\Tenant\Service\TenantService
     * @throws \LogicException
     */
    private function enable(string $filter): TenantService
    {
        $filters = $this->manager->getFilters();

        if (!$filters->has($filter)) {
            throw new LogicException('Filter does not exist.');
        }

        if (!$filters->isEnabled($filter)) {
            $filters->enable($filter);
        }

        return $this;
    }

    /**
     * Disable filter
     *
     * @param string $filter
     * @return \Ds\Component\Tenant\Service\TenantService
     * @throws \LogicException
     */
    private function disable(string $filter): TenantService
    {
        $filters = $this->manager->getFilters();

        if (!$filters->has($filter)) {
            throw new LogicException('Filter does not exist.');
        }

        if ($filters->isEnabled($filter)) {
            $filters->disable($filter);
        }

        return $this;
    }
}


<?php

namespace Ds\Component\Api\Service;

use Ds\Component\Api\Model\Individual;
use Ds\Component\Api\Model\IndividualPersona;
use Ds\Component\Api\Query\IndividualParameters as Parameters;

/**
 * Class IndividualService
 *
 * @package Ds\Component\Api
 */
final class IndividualService implements Service
{
    use Base;

    /**
     * @const string
     */
    const MODEL = Individual::class;

    /**
     * @const string
     */
    const RESOURCE_LIST = '/individuals';
    const RESOURCE_OBJECT = '/individuals/{id}';

    /**
     * @var array
     */
    private static $map = [
        'id',
        'uuid',
        'createdAt',
        'updatedAt',
        'owner',
        'ownerUuid',
        'roles',
        'version',
        'tenant'
    ];

    /**
     * Get individual list
     *
     * @param \Ds\Component\Api\Query\IndividualParameters $parameters
     * @return array
     */
    public function getList(Parameters $parameters = null): array
    {
        $objects = $this->execute('GET', static::RESOURCE_LIST);
        $list = [];

        foreach ($objects as $object) {
            $model = static::toModel($object);
            $list[] = $model;
        }

        return $list;
    }

    /**
     * Get individual
     *
     * @param string $id
     * @param \Ds\Component\Api\Query\IndividualParameters $parameters
     * @return \Ds\Component\Api\Model\Individual
     */
    public function get(string $id, Parameters $parameters = null): Individual
    {
        $resource = str_replace('{id}', $id, static::RESOURCE_OBJECT);
        $object = $this->execute('GET', $resource);
        /** @var \Ds\Component\Api\Model\Individual $model */
        $model = static::toModel($object);

        return $model;
    }

    /**
     * Create individual
     *
     * @param \Ds\Component\Api\Model\Individual $individual
     * @param \Ds\Component\Api\Query\IndividualParameters $parameters
     * @return \Ds\Component\Api\Model\Individual
     */
    public function create(Individual $individual, Parameters $parameters = null): Individual
    {
        $options = [];
        $options['json'] = (array) static::toObject($individual);

        if ($parameters) {
            $options['query'] = (array) $parameters->toObject(true);
        }

        $object = $this->execute('POST', static::RESOURCE_LIST, $options);
        /** @var \Ds\Component\Api\Model\Individual $individual */
        $individual = static::toModel($object);

        return $individual;
    }
}


<?php

namespace Ds\Component\Security\EventListener\Token;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Symfony\Component\PropertyAccess\PropertyAccess;

/**
 * Class UuidListener
 *
 * @package Ds\Component\Security
 */
final class UuidListener
{
    /**
     * @var \Symfony\Component\PropertyAccess\PropertyAccessor
     */
    private $accessor;

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

    /**
     * Constructor
     *
     * @param string $property
     */
    public function __construct(string $property = '[uuid]')
    {
        $this->accessor = PropertyAccess::createPropertyAccessor();
        $this->property = $property;
    }

    /**
     * Add the user uuid to the token
     *
     * @param \Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent $event
     */
    public function created(JWTCreatedEvent $event)
    {
        $data = $event->getData();
        $user = $event->getUser();
        $this->accessor->setValue($data, $this->property, $user->getUuid());
        $event->setData($data);
    }

    /**
     * Mark the token as invalid if the user uuid is missing
     *
     * @param \Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent $event
     */
    public function decoded(JWTDecodedEvent $event)
    {
        $payload = $event->getPayload();

        // Make property accessor paths compatible by converting payload to recursive associative array
        $payload = json_decode(json_encode($payload), true);

        if (!$this->accessor->isReadable($payload, $this->property)) {
            $event->markAsInvalid();
        }
    }
}


<?php

namespace Ds\Component\Acl\DependencyInjection;

use Ds\Component\Acl\Collection\EntityCollection;
use Ds\Component\Acl\Collection\PermissionCollection;
use Ds\Component\Acl\Doctrine\ORM\QueryExtension\EntityExtension;
use Ds\Component\Acl\EventListener\EntityListener;
use Ds\Component\Acl\Model\Permission;
use Ds\Component\Acl\Serializer\Normalizer\Property\JsonLdNormalizer;
use Ds\Component\Acl\Serializer\Normalizer\Property\JsonNormalizer;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;

/**
 * Class DsAclExtension
 *
 * @package Ds\Component\Acl
 */
final class DsAclExtension extends Extension implements PrependExtensionInterface
{
    /**
     * {@inheritdoc}
     */
    public function prepend(ContainerBuilder $container)
    {
        $container->prependExtensionConfig('ds_acl', [
            'enabled' => true,
            'entities' => [],
            'permissions' => []
        ]);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('packages/api_platform.yaml');
        $loader->load('config.yaml');
    }

    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yaml');

        $configuration = new Configuration;
        $config = $this->processConfiguration($configuration, $configs);

        $definition = $container->getDefinition(EntityCollection::class);
        $definition->addArgument($config['entities']);

        foreach ($config['permissions'] as $key => $element) {
            $element['key'] = $key;

            if (!array_key_exists('attributes', $element)) {
                $element['attributes'] = [];
            }

            if (!array_key_exists('type', $element)) {
                $element['type'] = null;
            }

            if (!array_key_exists('value', $element)) {
                $element['value'] = null;
            }

            if (!array_key_exists('title', $element)) {
                $element['title'] = null;
            }

            foreach ([Permission::GENERIC, Permission::ENTITY, Permission::PROPERTY] as $type) {
                if (array_key_exists($type, $element)) {
                    $element['type'] = $type;
                    $element['value'] = $element[$type];
                    unset($element[$type]);
                }
            }

            $config['permissions'][$key] = $element;
        }

        $definition = $container->findDefinition(PermissionCollection::class);
        $definition->setArguments([$config['permissions']]);

        if (!$config['enabled']) {
            $container->removeDefinition(EntityExtension::class);
            $container->removeDefinition(EntityListener::class);
            $container->removeDefinition(JsonNormalizer::class);
            $container->removeDefinition(JsonLdNormalizer::class);
        }
    }
}


<?php

namespace Ds\Component\Api\DependencyInjection\Compiler;

use Ds\Component\Api\Collection\ServiceCollection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Class ServicePass
 *
 * @package Ds\Component\Api
 */
final class ServicePass implements CompilerPassInterface
{
    /**
     * Process
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     */
    public function process(ContainerBuilder $container)
    {
        $definition = $container->findDefinition(ServiceCollection::class);
        $services = $container->findTaggedServiceIds('ds_api.service');

        foreach ($services as $id => $tags) {
            foreach ($tags as $tag) {
                if (array_key_exists('alias', $tag)) {
                    $definition->addMethodCall('set', [ $tag['alias'], new Reference($id) ]);
                } else {
                    $definition->addMethodCall('add', [ new Reference($id) ]);
                }
            }
        }
    }
}


<?php

namespace Ds\Component\Entity\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * Class Configuration
 *
 * @package Ds\Component\Entity
 */
final class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $builder = new TreeBuilder;
        $node = $builder->root('ds_entity');
        $node
            ->children()
                ->booleanNode('uuidentifiable')
                    ->defaultFalse()
                ->end()
                ->booleanNode('custom_identifiable')
                    ->defaultFalse()
                ->end()
            ->end();

        return $builder;
    }
}


<?php

namespace Ds\Component\Locale\EventListener;

use ApiPlatform\Core\DataProvider\PaginatorInterface as Paginator;
use Ds\Component\Locale\Model\Type\Localizable;
use Ds\Component\Locale\Service\LocaleService;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;

/**
 * Class LocaleListener
 *
 * @package Ds\Component\Locale
 */
final class LocaleListener
{
    /**
     * @var \Ds\Component\Locale\Service\LocaleService
     */
    private $localeService;

    /**
     * Constructor
     *
     * @param \Ds\Component\Locale\Service\LocaleService $localeService
     */
    public function __construct(LocaleService $localeService)
    {
        $this->localeService = $localeService;
    }

    /**
     * Filter the returned data for a given locale
     *
     * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
     */
    public function kernelView(GetResponseForControllerResultEvent $event)
    {
        $request = $event->getRequest();

        if (!$request->query->has('locale')) {
            return;
        }

        $controllerResult = $event->getControllerResult();
        $locale = $request->query->get('locale');

        if ($controllerResult instanceof Paginator || is_array($controllerResult)) {
            foreach ($controllerResult as $model) {
                if ($model instanceof Localizable) {
                    $this->localeService->localize($model, $locale);
                }
            }
        } elseif ($controllerResult instanceof Localizable) {
            $this->localeService->localize($controllerResult, $locale);
        }

        $event->setControllerResult($controllerResult);
    }
}


<?php

namespace Ds\Component\Metadata\Migration;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
 * Class Version0_15_0
 *
 * @package Ds\Component\Metadata
 */
final class Version0_15_0 extends AbstractMigration
{
    /**
     * Up migration
     *
     * @param \Doctrine\DBAL\Schema\Schema $schema
     * @param array $metadatas
     */
    public function up(Schema $schema, array $metadatas = [])
    {
        $sequences['ds_metadata_id_seq'] = 1 + count($metadatas);
        $sequences['ds_metadata_trans_id_seq'] = 1;

        foreach ($metadatas as $metadata) {
            $sequences['ds_metadata_trans_id_seq'] += count($metadata->title);
        }

        switch ($this->platform->getName()) {
            case 'postgresql':
                $this->addSql('CREATE SEQUENCE ds_metadata_id_seq INCREMENT BY 1 MINVALUE 1 START '.$sequences['ds_metadata_id_seq']);
                $this->addSql('CREATE SEQUENCE ds_metadata_trans_id_seq INCREMENT BY 1 MINVALUE 1 START '.$sequences['ds_metadata_trans_id_seq']);
                $this->addSql('CREATE TABLE ds_metadata (id INT NOT NULL, uuid UUID NOT NULL, "owner" VARCHAR(255) DEFAULT NULL, owner_uuid UUID DEFAULT NULL, slug VARCHAR(255) NOT NULL, type VARCHAR(255) DEFAULT NULL, data JSON NOT NULL, version INT DEFAULT 1 NOT NULL, tenant UUID NOT NULL, deleted_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
                $this->addSql('CREATE UNIQUE INDEX UNIQ_11290F17D17F50A6 ON ds_metadata (uuid)');
                $this->addSql('CREATE UNIQUE INDEX UNIQ_11290F17989D9B624E59C462 ON ds_metadata (slug, tenant)');
                $this->addSql('COMMENT ON COLUMN ds_metadata.data IS \'(DC2Type:json_array)\'');
                $this->addSql('CREATE TABLE ds_metadata_trans (id INT NOT NULL, translatable_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
                $this->addSql('CREATE INDEX IDX_A6447E202C2AC5D3 ON ds_metadata_trans (translatable_id)');
                $this->addSql('CREATE UNIQUE INDEX ds_metadata_trans_unique_translation ON ds_metadata_trans (translatable_id, locale)');
                $this->addSql('ALTER TABLE ds_metadata_trans ADD CONSTRAINT FK_A6447E202C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES ds_metadata (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
                break;

            default:
                $this->abortIf(true,'Migration cannot be executed on "'.$this->platform->getName().'".');
                break;
        }
    }

    /**
     * Down migration
     *
     * @param \Doctrine\DBAL\Schema\Schema $schema
     */
    public function down(Schema $schema)
    {
        switch ($this->platform->getName()) {
            case 'postgresql':
                $this->addSql('ALTER TABLE ds_metadata_trans DROP CONSTRAINT FK_A6447E202C2AC5D3');
                $this->addSql('DROP SEQUENCE ds_metadata_id_seq CASCADE');
                $this->addSql('DROP SEQUENCE ds_metadata_trans_id_seq CASCADE');
                $this->addSql('DROP TABLE ds_metadata');
                $this->addSql('DROP TABLE ds_metadata_trans');
                break;

            default:
                $this->abortIf(true,'Migration cannot be executed on "'.$this->platform->getName().'".');
                break;
        }
    }
}


<?php

namespace Ds\Component\Model\Attribute\Accessor;

use DomainException;
use ReflectionClass;

/**
 * Trait Status
 *
 * @package Ds\Component\Model
 */
trait Status
{
    /**
     * Set status
     *
     * @param integer $status
     * @return object
     * @throws \DomainException
     */
    public function setStatus(?int $status)
    {
        if (null !== $status && $this->getStatuses() && !in_array($status, $this->getStatuses(), true)) {
            throw new DomainException('Status "'.$status.'" does not exist.');
        }

        $this->status = $status;

        return $this;
    }

    /**
     * Get status
     *
     * @return integer
     */
    public function getStatus(): ?int
    {
        return $this->status;
    }

    /**
     * Get statuses
     *
     * @return array
     */
    public function getStatuses()
    {
        static $statuses;

        if (null === $statuses) {
            $statuses = [];
            $class = new ReflectionClass($this);

            foreach ($class->getConstants() as $constant => $value) {
                if ('STATUS_' === substr($constant, 0, 7)) {
                    $statuses[] = $value;
                }
            }
        }

        return $statuses;
    }
}


<?php

namespace Ds\Component\Statistic\Model;

use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Ds\Component\Model\Attribute;
use stdClass;

/**
 * Class Data
 *
 * @package Ds\Component\Statistic
 */
final class Data
{
    use Attribute\Collection;
    use Attribute\Timestamp;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->timestamp = new DateTime;
        $this->collection = new ArrayCollection;
    }

    /**
     * Type cast to object
     *
     * @return \stdClass
     */
    public function toObject(): stdClass
    {
        $object = new stdClass;
        $object->timestamp = $this->timestamp;
        $object->collection = $this->collection->toArray();

        foreach ($object->collection as $alias => $status) {
            $object->collection[$alias] = $status->toObject();
        }

        return $object;
    }
}


<?php

namespace Ds\Component\Statistic;

use Ds\Component\Statistic\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
 * Class DsStatisticBundle
 *
 * @package Ds\Component\Statistic
 */
final class DsStatisticBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new Compiler\StatPass);
    }
}


<?php

namespace Ds\Component\Acl\Tenant\Unloader;

use Doctrine\ORM\EntityManagerInterface;
use Ds\Component\Acl\Entity\Access;
use Ds\Component\Acl\Entity\Permission;
use Ds\Component\Tenant\Entity\Tenant;
use Ds\Component\Tenant\Loader\Unloader;

/**
 * Class AclUnloader
 *
 * @package Ds\Component\Acl
 */
final class AclUnloader implements Unloader
{
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    private $entityManager;

    /**
     * Constructor
     *
     * @param \Doctrine\ORM\EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    /**
     * {@inheritdoc}
     */
    public function unload(Tenant $tenant)
    {
        $builder = $this->entityManager->getRepository(Permission::class)->createQueryBuilder('e');
        $builder
            ->delete()
            ->where('e.tenant = :tenant')
            ->setParameter('tenant', $tenant->getUuid());
        $query = $builder->getQuery();
        $query->execute();

        $builder = $this->entityManager->getRepository(Access::class)->createQueryBuilder('e');
        $builder
            ->delete()
            ->where('e.tenant = :tenant')
            ->setParameter('tenant', $tenant->getUuid());
        $query = $builder->getQuery();
        $query->execute();
    }
}


<?php

namespace Ds\Component\Database\Util;

use LogicException;
use Symfony\Component\Yaml\Yaml;

/**
 * Class Objects
 *
 * @package Ds\Component\Database
 */
final class Objects
{
    /**
     * Parse objects-formatted string into objects
     *
     * @param string $string
     * @param array $data
     * @return array
     * @throws \LogicException
     */
    public static function parse(string $string, array $data = []): array
    {
        if ($data) {
            $string = Parameters::replace($string, $data);
        }

        $data = Yaml::parse($string, Yaml::PARSE_OBJECT_FOR_MAP);

        if (!property_exists($data,'objects')) {
            throw new LogicException('Property "objects" does not exist.');
        }

        if (!is_array($data->objects)) {
            throw new LogicException('Property "objects" is not an array.');
        }

        $prototype = [];

        if (property_exists($data,'prototype')) {
            if (!is_object($data->prototype)) {
                throw new LogicException('Property "prototype" is not an object.');
            }

            $prototype = $data->prototype;
        }

        $objects = [];

        foreach ($data->objects as $object) {
            $objects[] = (object) array_merge((array) $prototype, (array) $object);
        }

        return $objects;
    }

    /**
     * Parse objects file into objects
     *
     * @param string $file
     * @param array $data
     * @return array
     */
    public static function parseFile(string $file, array $data = []): array
    {
        $string = file_get_contents($file);

        return static::parse($string, $data);
    }
}


<?php

namespace Ds\Component\Camunda\Query\Attribute;

/**
 * Trait SortOrder
 *
 * @package Ds\Component\Camunda
 */
trait SortOrder
{
    /**
     * @var string
     */
    private $sortOrder; # region accessors

    /**
     * Set sort order
     *
     * @param string $sortOrder
     * @return object
     */
    public function setSortOrder(?string $sortOrder)
    {
        $this->sortOrder = $sortOrder;
        $this->_sortOrder = null !== $sortOrder;

        return $this;
    }

    /**
     * Get sort order
     *
     * @return string
     */
    public function getSortOrder(): ?string
    {
        return $this->sortOrder;
    }

    # endregion

    /**
     * @var boolean
     */
    private $_sortOrder;
}


<?php

namespace Ds\Component\Camunda\Service;

use DateTime;
use Ds\Component\Camunda\Model\Model;
use Ds\Component\Camunda\Model\Variable;
use GuzzleHttp\Client;
use InvalidArgumentException;
use LogicException;
use UnexpectedValueException;
use stdClass;

/**
 * Trait Base
 *
 * @package Ds\Component\Camunda
 */
trait Base
{
    /**
     * Cast object to model
     *
     * @param \stdClass $object
     * @return \Ds\Component\Camunda\Model\Model
     * @throws \LogicException
     */
    public static function toModel(stdClass $object)
    {
        if (null === static::MODEL) {
            throw new LogicException('Model class is not defined.');
        }

        $class = static::MODEL;
        $model = new $class;

        foreach (static::$map as $local => $remote) {
            if (is_int($local)) {
                $local = $remote;
            }

            if (property_exists($object, $remote)) {
                switch ($local) {
                    case 'created':
                    case 'updated':
                    case 'due':
                    case 'followUp':
                    case 'deploymentTime':
                        if (null !== $object->$remote) {
                            $model->{'set' . ucfirst($local)}(new DateTime($object->$remote));
                        }

                        break;

                    case 'variables':
                        foreach ($object->$remote as $name => $variable) {
                            if (Variable::TYPE_JSON === $variable->type) {
                                $variable->value = json_decode($variable->value);
                            }

                            $model->addVariable(new Variable($name, $variable->value, $variable->type, $variable->valueInfo));
                        }

                        break;

                    default:
                        $model->{'set'.ucfirst($local)}($object->$remote);
                }
            }
        }

        return $model;
    }

    /**
     * Cast model to object
     *
     * @param \Ds\Component\Camunda\Model\Model $model
     * @return stdClass
     * @throws \LogicException
     */
    public static function toObject(Model $model)
    {
        if (null === static::MODEL) {
            throw new LogicException('Model class is not defined.');
        }

        $object = new stdClass;

        foreach (static::$map as $local => $remote) {
            if (is_int($local)) {
                $local = $remote;
            }

            switch ($local) {
                case 'created':
                case 'updated':
                case 'due':
                case 'followUp':
                case 'deploymentTime':
                    $object->$remote = null;
                    $value = $model->{'get'.ucfirst($local)}();

                    if (null !== $value) {
                        $object->$remote = $value->format('Y-m-dTH:i:s');
                    }

                    break;

                default:
                    $object->$remote = $model->{'get'.ucfirst($local)}();
            }
        }

        return $object;
    }

    /**
     * @var \GuzzleHttp\ClientInterface
     */
    private $client;

    /**
     * @var string
     */
    private $host; # region accessors

    /**
     * {@inheritdoc}
     */
    public function setHost(string $host)
    {
        $this->host = $host;

        return $this;
    }

    # endregion

    /**
     * @var array
     */
    private $headers; # region accessors

    /**
     * {@inheritdoc}
     */
    public function setHeaders(array $headers)
    {
        $this->headers = $headers;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function setHeader(string $name, string $value)
    {
        $this->headers[$name] = $value;

        return $this;
    }

    # endregion

    /**
     * Constructor
     *
     * @param string $host
     * @param array $headers
     */
    public function __construct(?string $host = null, array $headers = [])
    {
        $this->client = new Client;
        $this->host = $host;
        $this->headers = $headers;
    }

    /**
     * Execute api request
     *
     * @param string $method
     * @param string $resource
     * @param array $options
     * @return mixed
     */
    protected function execute(string $method, string $resource, array $options = [])
    {
        $uri = $this->host.$resource;

        if ($this->headers) {
            if (!array_key_exists('headers', $options)) {
                $options['headers'] = [];
            }

            foreach ($this->headers as $key => $value) {
                if (!array_key_exists($key, $options['headers'])) {
                    $options['headers'][$key] = $value;
                }
            }
        }

        $response = $this->client->request($method, $uri, $options);
        $data = (string) $response->getBody();

        if ('' !== $data) {
            try {
                $data = \GuzzleHttp\json_decode($data);
            } catch (InvalidArgumentException $exception) {
                throw new UnexpectedValueException('Service response is not valid.', 0, $exception);
            }
        }

        return $data;
    }
}


<?php

namespace Ds\Component\Translation\Doctrine\ORM\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter as BaseOrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use DomainException;

/**
 * Class OrderFilter
 *
 * @package Ds\Component\Translation
 */
final class OrderFilter extends BaseOrderFilter
{
    use Translation;

    /**
     * @const string
     */
    const ORDER_ASC = 'ASC';
    const ORDER_DESC = 'DESC';

    /**
     * Filter property
     *
     * @param string $property
     * @param mixed $order
     * @param \Doctrine\ORM\QueryBuilder $queryBuilder
     * @param \ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface $queryNameGenerator
     * @param string $resourceClass
     * @param string|null $operationName
     * @throws \DomainException
     */
    protected function filterProperty(string $property, $order, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        if (!$this->isPropertyEnabled($property)) {
            return;
        }

        $alias = $this->addJoinTranslation($queryBuilder, $resourceClass);
        $order = strtoupper($order);

        if (!in_array($order, [static::ORDER_ASC, static::ORDER_DESC], true)) {
            throw new DomainException('Filter order "'.$order.'" does not exist.');
        }

        $queryBuilder->addOrderBy(sprintf('%s.%s', $alias, $property), $order);
    }
}


<?php

namespace Ds\Component\Parameter\EventListener\Entity\Parameter\Serializer;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Ds\Component\Parameter\Entity\Parameter;

/**
 * Class PostUpdateListener
 *
 * @package Ds\Component\Parameter
 */
final class PostUpdateListener
{
    /**
     * Unserialize parameter entity value property after saving in storage.
     *
     * @param \Doctrine\ORM\Event\LifecycleEventArgs $args
     */
    public function postUpdate(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        if (!$entity instanceof Parameter) {
            return;
        }

        $entity->setValue(unserialize($entity->getValue()));
    }
}


<?php

namespace Ds\Component\Formio\Query\Attribute;

/**
 * Trait Dryrun
 *
 * @package Ds\Component\Formio
 */
trait Dryrun
{
    /**
     * @var string
     */
    private $dryrun; # region accessors

    /**
     * Set dryrun
     *
     * @param boolean $dryrun
     * @return object
     */
    public function setDryrun(?bool $dryrun)
    {
        $this->dryrun = $dryrun;
        $this->_dryrun = null !== $dryrun;

        return $this;
    }

    /**
     * Get dryrun
     *
     * @return string
     */
    public function getDryrun(): ?bool
    {
        return $this->dryrun;
    }

    # endregion

    /**
     * @var boolean
     */
    private $_dryrun;
}


<?php

namespace Ds\Component\Formio\Query\Attribute;

/**
 * Trait Form
 *
 * @package Ds\Component\Formio
 */
trait Form
{
    /**
     * @var string
     */
    private $form; # region accessors

    /**
     * Set form
     *
     * @param string $form
     * @return object
     */
    public function setForm(?string $form)
    {
        $this->form = $form;
        $this->_form = null !== $form;

        return $this;
    }

    /**
     * Get form
     *
     * @return string
     */
    public function getForm(): ?string
    {
        return $this->form;
    }

    # endregion

    /**
     * @var boolean
     */
    private $_form;
}


<?php

namespace Ds\Component\Api;

use Ds\Component\Api\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
 * Class DsApiBundle
 *
 * @package Ds\Component\Api
 */
final class DsApiBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new Compiler\ServicePass);
    }
}


<?php

namespace Ds\Component\Parameter\EventListener\Entity\Parameter\Serializer;

use Doctrine\ORM\Event\PreFlushEventArgs;
use Ds\Component\Parameter\Entity\Parameter;

/**
 * Class PreFlushListener
 *
 * @package Ds\Component\Parameter
 */
final class PreFlushListener
{
    /**
     * Serialize parameter entity value property before saving to storage.
     *
     * @param \Doctrine\ORM\Event\PreFlushEventArgs $args
     */
    public function preFlush(PreFlushEventArgs $args)
    {
        $entities = $args->getEntityManager()->getUnitOfWork()->getScheduledEntityInsertions();

        foreach ($entities as $entity) {
            if (!$entity instanceof Parameter) {
                continue;
            }

            $entity->setValue(serialize($entity->getValue()));
        }
    }
}


<?php

namespace Ds\Component\Acl\Entity\Attribute\Accessor;

use Ds\Component\Acl\Entity\Permission;

/**
 * Trait Permissions
 *
 * @package Ds\Component\Acl
 */
trait Permissions
{
    /**
     * Set permissions
     *
     * @return object
     */
    public function setPermissions($permissions)
    {
        $this->permissions = $permissions;

        return $this;
    }

    /**
     * Add permission
     *
     * @param \Ds\Component\Acl\Entity\Permission $permission
     * @return object
     */
    public function addPermission(Permission $permission)
    {
        $permission->setAccess($this);
        $this->permissions->add($permission);

        return $this;
    }

    /**
     * Remove permission
     *
     * @param \Ds\Component\Acl\Entity\Permission $permission
     * @return object
     */
    public function removePermission(Permission $permission)
    {
        $this->permissions->removeElement($permission);

        return $this;
    }

    /**
     * Get permissions
     *
     * @return \Doctrine\Common\Collections\ArrayCollection
     */
    public function getPermissions()
    {
        return $this->permissions;
    }
}


<?php

namespace Ds\Component\Parameter\Service;

use Doctrine\ORM\EntityManagerInterface;
use Ds\Component\Parameter\Collection\ParameterCollection;
use Ds\Component\Parameter\Entity\Parameter;
use Ds\Component\Entity\Service\EntityService;
use OutOfRangeException;

/**
 * Class ParameterService
 *
 * @package Ds\Component\Parameter
 */
final class ParameterService extends EntityService
{
    /**
     * @var \Ds\Component\Parameter\Collection\ParameterCollection
     */
    private $collection;

    /**
     * Constructor
     *
     * @param \Doctrine\ORM\EntityManagerInterface $manager
     * @param \Ds\Component\Parameter\Collection\ParameterCollection $collection
     * @param string $entity
     */
    public function __construct(EntityManagerInterface $manager, ParameterCollection $collection, $entity = Parameter::class)
    {
        parent::__construct($manager, $entity);
        $this->collection = $collection;
    }

    /**
     * Get parameter value
     *
     * @param string $key
     * @return mixed
     * @throws \OutOfRangeException
     */
    public function get(string $key)
    {
        $parameter = $this->repository->findOneBy(['key' => $key]);

        if (!$parameter) {
            throw new OutOfRangeException('Parameter "'.$key.'" does not exist.');
        }

        $this->manager->detach($parameter);

        return $parameter->getValue();
    }

    /**
     * Set parameter value
     *
     * @param string $key
     * @param mixed $value
     */
    public function set(string $key, $value)
    {
        $parameter = $this->repository->findOneBy(['key' => $key]);

        if (!$parameter) {
            throw new OutOfRangeException('Parameter "'.$key.'" does not exist.');
        }

        $parameter
            ->setKey($key)
            ->setValue($value);
        $this->manager->persist($parameter);
        $this->manager->flush();
        $this->manager->detach($parameter);
    }
}


<?php

namespace Ds\Component\Security\EventListener\Token\Identity;

use Ds\Component\Security\Model\User;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Symfony\Component\PropertyAccess\PropertyAccess;

/**
 * Class TypeListener
 *
 * @package Ds\Component\Security
 */
final class TypeListener
{
    /**
     * @var \Symfony\Component\PropertyAccess\PropertyAccessor
     */
    private $accessor;

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

    /**
     * Constructor
     *
     * @param string $property
     */
    public function __construct(string $property = '[identity][type]')
    {
        $this->accessor = PropertyAccess::createPropertyAccessor();
        $this->property = $property;
    }

    /**
     * Add the identity type to the token
     *
     * @param \Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent $event
     * @throws \Ds\Component\Security\Exception\InvalidUserTypeException
     */
    public function created(JWTCreatedEvent $event)
    {
        $data = $event->getData();
        $user = $event->getUser();

        // @todo Standardize interface between app user entity and security user model
        if ($user instanceof User) {
            $this->accessor->setValue($data, $this->property, $user->getIdentity()->getType());
        } else {
            $this->accessor->setValue($data, $this->property, $user->getIdentity());
        }

        $event->setData($data);
    }

    /**
     * Mark the token as invalid if the identity type is missing
     *
     * @param \Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent $event
     */
    public function decoded(JWTDecodedEvent $event)
    {
        $payload = $event->getPayload();

        // Make property accessor paths compatible by converting payload to recursive associative array
        $payload = json_decode(json_encode($payload), true);

        if (!$this->accessor->isReadable($payload, $this->property)) {
            $event->markAsInvalid();
        }
    }
}


<?php

namespace Ds\Component\Config\EventListener\Entity\Config;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Ds\Component\Config\Collection\ConfigCollection;
use Ds\Component\Config\Entity\Config;

/**
 * Class EncryptListener
 *
 * @package Ds\Component\Config
 */
final class EncryptListener
{
    /**
     * @var \Ds\Component\Config\Collection\ConfigCollection
     */
    private $configCollection;

    /**
     * Constructor
     *
     * @param \Ds\Component\Config\Collection\ConfigCollection $configCollection
     */
    public function __construct(ConfigCollection $configCollection)
    {
        $this->configCollection = $configCollection;
    }

    /**
     * Set config entity encrypt property after retrieving them from storage.
     *
     * @param \Doctrine\ORM\Event\LifecycleEventArgs $args
     */
    public function postLoad(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        if (!$entity instanceof Config) {
            return;
        }

        $key = $entity->getKey();
        $encrypt = $this->configCollection->get($key)['encrypt'];
        $entity->setEncrypt($encrypt);
    }

    /**
     * Set config entity encrypt property before saving in storage.
     *
     * @param \Doctrine\ORM\Event\PreUpdateEventArgs $args
     */
    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();

        if (!$entity instanceof Config) {
            return;
        }

        $key = $entity->getKey();
        $encrypt = $this->configCollection->get($key)['encrypt'];
        $entity->setEncrypt($encrypt);
    }

    /**
     * Set config entity encrypt property before saving in storage.
     *
     * @param \Doctrine\ORM\Event\PreFlushEventArgs $args
     */
    public function preFlush(PreFlushEventArgs $args)
    {
        $entities = $args->getEntityManager()->getUnitOfWork()->getScheduledEntityInsertions();

        foreach ($entities as $entity) {
            if (!$entity instanceof Config) {
                continue;
            }

            $key = $entity->getKey();
            $encrypt = $this->configCollection->get($key)['encrypt'];
            $entity->setEncrypt($encrypt);
        }
    }
}


<?php

namespace Ds\Component\Model\Attribute\Accessor;

use DomainException;

/**
 * Trait Owner
 *
 * @package Ds\Component\Model
 */
trait Owner
{
    /**
     * Set owner
     *
     * @param string $owner
     * @return object
     * @throws \DomainException
     */
    public function setOwner(?string $owner)
    {
        if (null !== $owner) {
            if (!preg_match('/^[a-z]+$/i', $owner)) {
                throw new DomainException('Owner is not valid.');
            }
        }

        $this->owner = $owner;

        return $this;
    }

    /**
     * Get owner
     *
     * @return string
     */
    public function getOwner(): ?string
    {
        return $this->owner;
    }
}


<?php

namespace Ds\Component\Camunda\Query\Attribute;

/**
 * Trait Source
 *
 * @package Ds\Component\Camunda
 */
trait Source
{
    /**
     * @var string
     */
    private $source; # region accessors

    /**
     * Set source
     *
     * @param string $source
     * @return object
     */
    public function setSource(?string $source)
    {
        $this->source = $source;
        $this->_source = null !== $source;

        return $this;
    }

    /**
     * Get source
     *
     * @return string
     */
    public function getSource(): ?string
    {
        return $this->source;
    }

    # endregion

    /**
     * @var boolean
     */
    private $_source;
}


<?php

namespace Ds\Component\Discovery\Repository;

use Doctrine\Common\Collections\ArrayCollection;
use Ds\Component\Discovery\Model\Config;
use GuzzleHttp\Exception\ClientException;
use stdClass;

/**
 * Class ConfigRepository
 *
 * @package Ds\Component\Discovery
 */
final class ConfigRepository extends Repository
{
    /**
     * @const string
     */
    const RESOURCE_LIST = '/v1/kv';
    const RESOURCE_OBJECT = '/v1/kv/{id}';

    /**
     * {@inheritdoc}
     */
    public function find($id)
    {
        $resource = 'http://'.$this->host.str_replace('{id}', $id, static::RESOURCE_OBJECT);

        try {
            $response = $this->client->request('GET', $resource, [
                'headers' => [
                    'X-Consul-Token' => $this->token
                ]
            ]);
        } catch (ClientException $exception) {
            return null;
        }

        $body = (string) $response->getBody();
        $objects = \GuzzleHttp\json_decode($body);

        if (!$objects) {
            return null;
        }

        $model = $this->toModel($objects[0]);

        return $model;
    }

    /**
     * {@inheritdoc}
     */
    public function findAll()
    {

    }

    /**
     * {@inheritdoc}
     */
    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
    {
        $resource = 'http://'.$this->host.static::RESOURCE_LIST;

        if (array_key_exists('directory', $criteria)) {
            $resource .= '/'.$criteria['directory'];
            unset($criteria['directory']);
        }

        $response = $this->client->request('GET', $resource, [
            'headers' => [
                'X-Consul-Token' => $this->token
            ],
            'query' => $criteria
        ]);

        $body = (string) $response->getBody();
        $objects = \GuzzleHttp\json_decode($body);
        $models = new ArrayCollection;

        foreach ($objects as $object) {
            $models->add($this->toModel($object));
        }

        return $models;
    }

    /**
     * {@inheritdoc}
     */
    public function findOneBy(array $criteria)
    {

    }

    /**
     * {@inheritdoc}
     */
    public function getClassName()
    {
        return Config::class;
    }

    /**
     * Type cast object to model
     *
     * @param \stdClass $object
     * @return \Ds\Component\Discovery\Model\Config
     */
    protected function toModel(stdClass $object)
    {
        $class = $this->getClassName();
        $model = new $class;
        $model
            ->setKey($object->Key)
            ->setValue(base64_decode($object->Value));

        return $model;
    }
}


<?php

namespace CommerceGuys\Zone\Repository;

use CommerceGuys\Zone\Exception\UnknownZoneException;
use CommerceGuys\Zone\Model\Zone;
use CommerceGuys\Zone\Model\ZoneMemberCountry;
use CommerceGuys\Zone\Model\ZoneMemberZone;

/**
 * Manages zones based on JSON definitions.
 */
class ZoneRepository implements ZoneRepositoryInterface
{
    /**
     * The path where zone definitions are stored.
     *
     * @var string
     */
    protected $definitionPath;

    /**
     * Zone index.
     *
     * @var array
     */
    protected $zoneIndex = [];

    /**
     * Zones.
     *
     * @var array
     */
    protected $zones = [];

    /**
     * Creates a ZoneRepository instance.
     *
     * @param string $definitionPath Path to the zone definitions.
     */
    public function __construct($definitionPath)
    {
        $this->definitionPath = $definitionPath;
    }

    /**
     * {@inheritdoc}
     */
    public function get($id)
    {
        if (!isset($this->zones[$id])) {
            $definition = $this->loadDefinition($id);
            $this->zones[$id] = $this->createZoneFromDefinition($definition);
        }

        return $this->zones[$id];
    }

    /**
     * {@inheritdoc}
     */
    public function getAll($scope = null)
    {
        // Build the list of all available zones.
        if (empty($this->zoneIndex)) {
            if ($handle = opendir($this->definitionPath)) {
                while (false !== ($entry = readdir($handle))) {
                    if (substr($entry, 0, 1) != '.') {
                        $id = strtok($entry, '.');
                        $this->zoneIndex[] = $id;
                    }
                }
                closedir($handle);
            }
        }

        // Load each zone, filter by scope if needed.
        $zones = [];
        foreach ($this->zoneIndex as $id) {
            $zone = $this->get($id);
            if (is_null($scope) || ($zone->getScope() == $scope)) {
                $zones[$id] = $this->get($id);
            }
        }

        return $zones;
    }

    /**
     * Loads the zone definition for the provided id.
     *
     * @param string $id The zone id.
     *
     * @return array The zone definition.
     */
    protected function loadDefinition($id)
    {
        $filename = $this->definitionPath . $id . '.json';
        $definition = @file_get_contents($filename);
        if (empty($definition)) {
            throw new UnknownZoneException($id);
        }
        $definition = json_decode($definition, true);
        $definition['id'] = $id;

        return $definition;
    }

    /**
     * Creates a Zone instance from the provided definition.
     *
     * @param array $definition The zone definition.
     *
     * @return Zone
     */
    protected function createZoneFromDefinition(array $definition)
    {
        $zone = new Zone();
        // Bind the closure to the Zone object, giving it access to
        // its protected properties. Faster than both setters and reflection.
        $setValues = \Closure::bind(function ($definition) {
            $this->id = $definition['id'];
            $this->name = $definition['name'];
            if (isset($definition['scope'])) {
                $this->scope = $definition['scope'];
            }
            if (isset($definition['priority'])) {
                $this->priority = $definition['priority'];
            }
        }, $zone, '\CommerceGuys\Zone\Model\Zone');
        $setValues($definition);

        // Add the zone members.
        foreach ($definition['members'] as $memberDefinition) {
            if ($memberDefinition['type'] == 'country') {
                $zoneMember = $this->createZoneMemberCountryFromDefinition($memberDefinition);
                $zone->addMember($zoneMember);
            } elseif ($memberDefinition['type'] == 'zone') {
                $zoneMember = $this->createZoneMemberZoneFromDefinition($memberDefinition);
                $zone->addMember($zoneMember);
            }
        }

        return $zone;
    }

    /**
     * Creates a ZoneMemberCountry instance from the provided definition.
     *
     * @param array $definition The zone member definition.
     *
     * @return ZoneMemberCountry
     */
    protected function createZoneMemberCountryFromDefinition(array $definition)
    {
        $zoneMember = new ZoneMemberCountry();
        $setValues = \Closure::bind(function ($definition) {
            $this->id = $definition['id'];
            $this->name = $definition['name'];
            $this->countryCode = $definition['country_code'];
            if (isset($definition['administrative_area'])) {
                $this->administrativeArea = $definition['administrative_area'];
            }
            if (isset($definition['locality'])) {
                $this->locality = $definition['locality'];
            }
            if (isset($definition['dependent_locality'])) {
                $this->dependentLocality = $definition['dependent_locality'];
            }
            if (isset($definition['included_postal_codes'])) {
                $this->includedPostalCodes = $definition['included_postal_codes'];
            }
            if (isset($definition['excluded_postal_codes'])) {
                $this->excludedPostalCodes = $definition['excluded_postal_codes'];
            }
        }, $zoneMember, '\CommerceGuys\Zone\Model\ZoneMemberCountry');
        $setValues($definition);

        return $zoneMember;
    }

    /**
     * Creates a ZoneMemberZone instance from the provided definition.
     *
     * @param array $definition The zone member definition.
     *
     * @return ZoneMemberZone
     */
    protected function createZoneMemberZoneFromDefinition(array $definition)
    {
        $zone = $this->get($definition['zone']);
        $zoneMember = new ZoneMemberZone();
        $zoneMember->setZone($zone);
        $setValues = \Closure::bind(function ($definition) {
            $this->id = $definition['id'];
        }, $zoneMember, '\CommerceGuys\Zone\Model\ZoneMemberZone');
        $setValues($definition);

        return $zoneMember;
    }
}


<?php

namespace Ds\Component\Entity\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Ds\Component\Entity\Service\IdService;
use Ds\Component\Model\Type\CustomIdentifiable;

/**
 * Class CustomIdentifiableListener
 *
 * @package Ds\Component\Entity
 */
final class CustomIdentifiableListener
{
    /**
     * @var \Ds\Component\Entity\Service\IdService
     */
    private $idService;

    /**
     * Constructor
     *
     * @param \Ds\Component\Entity\Service\IdService $idService
     */
    public function __construct(IdService $idService)
    {
        $this->idService = $idService;
    }

    /**
     * Generate a custom id before persisting the entity, if none provided
     *
     * @param \Doctrine\ORM\Event\LifecycleEventArgs $args
     */
    public function prePersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        if (!$entity instanceof CustomIdentifiable) {
            return;
        }

        $this->idService->generateCustomId($entity);
    }
}


<?php

namespace Ds\Component\Health\DependencyInjection\Compiler;

use Ds\Component\Health\Collection\CheckCollection;
use LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Class CheckPass
 *
 * @package Ds\Component\Health
 */
final class CheckPass implements CompilerPassInterface
{
    /**
     * Process
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     * @throws \LogicException
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->has(CheckCollection::class)) {
            return;
        }

        $collection = $container->findDefinition(CheckCollection::class);
        $checks = $container->findTaggedServiceIds('ds_health.check');

        foreach ($checks as $id => $tags) {
            foreach ($tags as $tag) {
                if (!array_key_exists('alias', $tag)) {
                    $tag['alias'] = $id;
                }

                $check = $container->findDefinition($id);
                $check->addMethodCall('setAlias', [$tag['alias']]);
                $collection->addMethodCall('set', [$tag['alias'], new Reference($id)]);
            }
        }
    }
}


<?php

namespace Ds\Component\Api\Query\Attribute;

/**
 * Trait AssigneeUuid
 *
 * @package Ds\Component\Api
 */
trait AssigneeUuid
{
    /**
     * @var string
     */
    private $assigneeUuid; # region accessors

    /**
     * Set assignee uuid
     *
     * @param string $assigneeUuid
     * @return object
     */
    public function setAssigneeUuid(?string $assigneeUuid)
    {
        $this->assigneeUuid = $assigneeUuid;
        $this->_assigneeUuid = true;

        return $this;
    }

    /**
     * Get assignee uuid
     *
     * @return string
     */
    public function getAssigneeUuid(): ?string
    {
        return $this->assigneeUuid;
    }

    # endregion

    /**
     * @var boolean
     */
    private $_assigneeUuid;
}