<?php

namespace Gdbots\Pbj;

use Gdbots\Common\Enum;
use Gdbots\Common\ToArray;
use Gdbots\Common\Util\ArrayUtils;
use Gdbots\Common\Util\NumberUtils;
use Gdbots\Pbj\Enum\FieldRule;
use Gdbots\Pbj\Enum\Format;
use Gdbots\Pbj\Enum\TypeName;
use Gdbots\Pbj\Exception\AssertionFailed;
use Gdbots\Pbj\Type\Type;
use Gdbots\Pbj\WellKnown\Identifier;

final class Field implements ToArray, \JsonSerializable
{
    /**
     * Regular expression pattern for matching a valid field name.  The pattern allows
     * for camelCase fields name but snake_case is recommend.
     *
     * @constant string
     */
    const VALID_NAME_PATTERN = '/^[a-zA-Z_]{1}[a-zA-Z0-9_]*$/';

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

    /** @var Type */
    private $type;

    /** @var FieldRule */
    private $rule;

    /** @var bool */
    private $required = false;

    /** @var int */
    private $minLength;

    /** @var int */
    private $maxLength;

    /**
     * A regular expression to match against for string types.
     * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#pattern
     *
     * @var string
     */
    private $pattern;

    /**
     * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format
     *
     * @var Format
     */
    private $format;

    /** @var int */
    private $min;

    /** @var int */
    private $max;

    /** @var int */
    private $precision = 10;

    /** @var int */
    private $scale = 2;

    /** @var mixed */
    private $default;

    /** @var bool */
    private $useTypeDefault = true;

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

    /** @var array */
    private $anyOfClassNames;

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

    /** @var bool */
    private $overridable = false;

    /**
     * @param string        $name
     * @param Type          $type
     * @param FieldRule     $rule
     * @param bool          $required
     * @param null|int      $minLength
     * @param null|int      $maxLength
     * @param null|string   $pattern
     * @param null|string   $format
     * @param null|int      $min
     * @param null|int      $max
     * @param int           $precision
     * @param int           $scale
     * @param null|mixed    $default
     * @param bool          $useTypeDefault
     * @param null|string   $className
     * @param null|array    $anyOfClassNames
     * @param \Closure|null $assertion
     * @param bool          $overridable
     */
    public function __construct(
        $name,
        Type $type,
        FieldRule $rule = null,
        $required = false,
        $minLength = null,
        $maxLength = null,
        $pattern = null,
        $format = null,
        $min = null,
        $max = null,
        $precision = 10,
        $scale = 2,
        $default = null,
        $useTypeDefault = true,
        $className = null,
        array $anyOfClassNames = null,
        \Closure $assertion = null,
        $overridable = false
    ) {
        Assertion::betweenLength($name, 1, 127);
        Assertion::regex($name, self::VALID_NAME_PATTERN,
            sprintf('Field [%s] must match pattern [%s].', $name, self::VALID_NAME_PATTERN)
        );
        Assertion::boolean($required);
        Assertion::boolean($useTypeDefault);
        Assertion::boolean($overridable);

        if ($type->getTypeValue() !== TypeName::MESSAGE) {
            // anyOf is only supported on nested messages
            Assertion::nullOrClassExists($className);
            $anyOfClassNames = null;
        }

        $this->name = $name;
        $this->type = $type;
        $this->required = $required;
        $this->useTypeDefault = $useTypeDefault;
        $this->className = $className;
        $this->anyOfClassNames = $anyOfClassNames;
        $this->assertion = $assertion;
        $this->overridable = $overridable;

        $this->applyFieldRule($rule);
        $this->applyStringOptions($minLength, $maxLength, $pattern, $format);
        $this->applyNumericOptions($min, $max, $precision, $scale);
        $this->applyDefault($default);
    }

    /**
     * @param FieldRule $rule
     *
     * @throws AssertionFailed
     */
    private function applyFieldRule(FieldRule $rule = null)
    {
        $this->rule = $rule ?: FieldRule::A_SINGLE_VALUE();
        if ($this->isASet()) {
            Assertion::true(
                $this->type->allowedInSet(),
                sprintf(
                    'Field [%s] with type [%s] cannot be used in a set.',
                    $this->name,
                    $this->type->getTypeValue()
                )
            );
        }
    }

    /**
     * @param null|int    $minLength
     * @param null|int    $maxLength
     * @param null|string $pattern
     * @param null|string $format
     */
    private function applyStringOptions($minLength = null, $maxLength = null, $pattern = null, $format = null)
    {
        $minLength = (int)$minLength;
        $maxLength = (int)$maxLength;
        if ($maxLength > 0) {
            $this->maxLength = $maxLength;
            $this->minLength = NumberUtils::bound($minLength, 0, $this->maxLength);
        } else {
            // arbitrary string minimum range
            $this->minLength = NumberUtils::bound($minLength, 0, $this->type->getMaxBytes());
        }

        if (null !== $pattern) {
            $this->pattern = '/' . trim($pattern, '/') . '/';
        }

        if (null !== $format && in_array($format, Format::values())) {
            $this->format = Format::create($format);
        } else {
            $this->format = Format::UNKNOWN();
        }
    }

    /**
     * @param null|int $min
     * @param null|int $max
     * @param int      $precision
     * @param int      $scale
     */
    private function applyNumericOptions($min = null, $max = null, $precision = 10, $scale = 2)
    {
        if (null !== $max) {
            $this->max = (int)$max;
        }

        if (null !== $min) {
            $this->min = (int)$min;
            if (null !== $this->max) {
                if ($this->min > $this->max) {
                    $this->min = $this->max;
                }
            }
        }

        $this->precision = NumberUtils::bound((int)$precision, 1, 65);
        $this->scale = NumberUtils::bound((int)$scale, 0, $this->precision);
    }

    /**
     * @param mixed $default
     *
     * @throws AssertionFailed
     * @throws \Exception
     */
    private function applyDefault($default = null)
    {
        $this->default = $default;

        if ($this->type->isScalar()) {
            if ($this->type->getTypeName() !== TypeName::TIMESTAMP()) {
                $this->useTypeDefault = true;
            }
        } else {
            $decodeDefault = null !== $this->default && !$this->default instanceof \Closure;
            switch ($this->type->getTypeValue()) {
                case TypeName::IDENTIFIER:
                    Assertion::notNull($this->className, sprintf('Field [%s] requires a className.', $this->name));
                    if ($decodeDefault && !$this->default instanceof Identifier) {
                        $this->default = $this->type->decode($this->default, $this);
                    }
                    break;

                case TypeName::INT_ENUM:
                case TypeName::STRING_ENUM:
                    Assertion::notNull($this->className, sprintf('Field [%s] requires a className.', $this->name));
                    if ($decodeDefault && !$this->default instanceof Enum) {
                        $this->default = $this->type->decode($this->default, $this);
                    }
                    break;

                default:
                    break;
            }
        }

        if (null !== $this->default && !$this->default instanceof \Closure) {
            $this->guardDefault($this->default);
        }
    }

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

    /**
     * @return Type
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return FieldRule
     */
    public function getRule()
    {
        return $this->rule;
    }

    /**
     * @return bool
     */
    public function isASingleValue()
    {
        return FieldRule::A_SINGLE_VALUE === $this->rule->getValue();
    }

    /**
     * @return bool
     */
    public function isASet()
    {
        return FieldRule::A_SET === $this->rule->getValue();
    }

    /**
     * @return bool
     */
    public function isAList()
    {
        return FieldRule::A_LIST === $this->rule->getValue();
    }

    /**
     * @return bool
     */
    public function isAMap()
    {
        return FieldRule::A_MAP === $this->rule->getValue();
    }

    /**
     * @return bool
     */
    public function isRequired()
    {
        return $this->required;
    }

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

    /**
     * @return int
     */
    public function getMaxLength()
    {
        if (null === $this->maxLength) {
            return $this->type->getMaxBytes();
        }
        return $this->maxLength;
    }

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

    /**
     * @return Format
     */
    public function getFormat()
    {
        return $this->format;
    }

    /**
     * @return int
     */
    public function getMin()
    {
        if (null === $this->min) {
            return $this->type->getMin();
        }
        return $this->min;
    }

    /**
     * @return int
     */
    public function getMax()
    {
        if (null === $this->max) {
            return $this->type->getMax();
        }
        return $this->max;
    }

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

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

    /**
     * @param Message $message
     *
     * @return mixed
     */
    public function getDefault(Message $message = null)
    {
        if (null === $this->default) {
            if ($this->useTypeDefault) {
                return $this->isASingleValue() ? $this->type->getDefault() : [];
            }
            return $this->isASingleValue() ? null : [];
        }

        if ($this->default instanceof \Closure) {
            $default = call_user_func($this->default, $message, $this);
            $this->guardDefault($default);
            if (null === $default) {
                if ($this->useTypeDefault) {
                    return $this->isASingleValue() ? $this->type->getDefault() : [];
                }
                return $this->isASingleValue() ? null : [];
            }
            return $default;
        }

        return $this->default;
    }

    /**
     * @param mixed $default
     *
     * @throws AssertionFailed
     * @throws \Exception
     */
    private function guardDefault($default)
    {
        if ($this->isASingleValue()) {
            $this->guardValue($default);
            return;
        }

        Assertion::nullOrIsArray($default, sprintf('Field [%s] default must be an array.', $this->name));
        if (null === $default) {
            return;
        }

        if ($this->isAMap()) {
            Assertion::true(
                ArrayUtils::isAssoc($default),
                sprintf('Field [%s] default must be an associative array.', $this->name)
            );
        }

        foreach ($default as $k => $v) {
            Assertion::notNull($v, sprintf('Field [%s] default for key [%s] cannot be null.', $this->name, $k));
            $this->guardValue($v);
        }
    }

    /**
     * @return bool
     */
    public function hasClassName()
    {
        return null !== $this->className;
    }

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

    /**
     * @return bool
     */
    public function hasAnyOfClassNames()
    {
        return null !== $this->anyOfClassNames;
    }

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

    /**
     * @return bool
     */
    public function isOverridable()
    {
        return $this->overridable;
    }

    /**
     * @param mixed $value
     *
     * @throws AssertionFailed
     * @throws \Exception
     */
    public function guardValue($value)
    {
        if ($this->required) {
            Assertion::notNull($value, sprintf('Field [%s] is required and cannot be null.', $this->name));
        }

        if (null !== $value) {
            $this->type->guard($value, $this);
        }

        if (null !== $this->assertion) {
            call_user_func($this->assertion, $value, $this);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        return [
            'name'               => $this->name,
            'type'               => $this->type->getTypeValue(),
            'rule'               => $this->rule->getName(),
            'required'           => $this->required,
            'min_length'         => $this->minLength,
            'max_length'         => $this->maxLength,
            'pattern'            => $this->pattern,
            'format'             => $this->format->getValue(),
            'min'                => $this->min,
            'max'                => $this->max,
            'precision'          => $this->precision,
            'scale'              => $this->scale,
            'default'            => $this->getDefault(),
            'use_type_default'   => $this->useTypeDefault,
            'class_name'         => $this->className,
            'any_of_class_names' => $this->anyOfClassNames,
            'has_assertion'      => null !== $this->assertion,
            'overridable'        => $this->overridable,
        ];
    }

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

    /**
     * Returns true if this field is likely compatible with the
     * provided field during a mergeFrom operation.
     *
     * todo: implement/test isCompatibleForMerge
     *
     * @param Field $other
     *
     * @return bool
     */
    public function isCompatibleForMerge(Field $other)
    {
        if ($this->name !== $other->name) {
            return false;
        }

        if ($this->type !== $other->type) {
            return false;
        }

        if ($this->rule !== $other->rule) {
            return false;
        }

        if ($this->className !== $other->className) {
            return false;
        }

        if (!array_intersect($this->anyOfClassNames, $other->anyOfClassNames)) {
            return false;
        }

        return true;
    }

    /**
     * Returns true if the provided field can be used as an
     * override to this field.
     *
     * @param Field $other
     *
     * @return bool
     */
    public function isCompatibleForOverride(Field $other)
    {
        if (!$this->overridable) {
            return false;
        }

        if ($this->name !== $other->name) {
            return false;
        }

        if ($this->type !== $other->type) {
            return false;
        }

        if ($this->rule !== $other->rule) {
            return false;
        }

        if ($this->required !== $other->required) {
            return false;
        }

        return true;
    }
}


<?php

namespace Gdbots\Pbj;

use Gdbots\Common\ToArray;
use Gdbots\Common\Util\ClassUtils;
use Gdbots\Pbj\Exception\FieldAlreadyDefined;
use Gdbots\Pbj\Exception\FieldNotDefined;
use Gdbots\Pbj\Exception\FieldOverrideNotCompatible;
use Gdbots\Pbj\Exception\MixinAlreadyAdded;
use Gdbots\Pbj\Exception\MixinNotDefined;

final class Schema implements ToArray, \JsonSerializable
{
    const PBJ_FIELD_NAME = '_schema';

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

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

    /** @var SchemaId */
    private $id;

    /** @var Field[] */
    private $fields = [];

    /** @var Field[] */
    private $requiredFields = [];

    /** @var Mixin[] */
    private $mixins = [];

    /** @var Mixin[] */
    private $mixinsByCurie = [];

    /** @var string[] */
    private $mixinIds = [];

    /** @var string[] */
    private $mixinCuries = [];

    /**
     * @param SchemaId|string $id
     * @param string          $className
     * @param Field[]         $fields
     * @param Mixin[]         $mixins
     */
    public function __construct($id, $className, array $fields = [], array $mixins = [])
    {
        Assertion::classExists($className, null, 'className');
        Assertion::allIsInstanceOf($fields, Field::class, null, 'fields');
        Assertion::allIsInstanceOf($mixins, Mixin::class, null, 'mixins');

        $this->id = $id instanceof SchemaId ? $id : SchemaId::fromString($id);
        $this->className = $className;
        $this->classShortName = ClassUtils::getShortName($this->className);

        $this->addField(
            FieldBuilder::create(self::PBJ_FIELD_NAME, Type\StringType::create())
                ->required()
                ->pattern(SchemaId::VALID_PATTERN)
                ->withDefault($this->id->toString())
                ->build()
        );

        foreach ($mixins as $mixin) {
            $this->addMixin($mixin);
        }

        foreach ($fields as $field) {
            $this->addField($field);
        }

        $this->mixinIds = array_keys($this->mixins);
        $this->mixinCuries = array_keys($this->mixinsByCurie);
    }

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

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        return [
            'id'          => $this->id,
            'curie'       => $this->getCurie(),
            'curie_major' => $this->getCurieMajor(),
            'qname'       => $this->getQName(),
            'class_name'  => $this->className,
            'mixins'      => array_map(
                function (Mixin $mixin) {
                    return $mixin->getId();
                },
                array_values($this->mixins)
            ),
            'fields'      => $this->fields,
        ];
    }

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

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->id->toString();
    }

    /**
     * @param Field $field
     *
     * @throws FieldAlreadyDefined
     * @throws FieldOverrideNotCompatible
     */
    private function addField(Field $field)
    {
        $fieldName = $field->getName();
        if ($this->hasField($fieldName)) {
            $existingField = $this->getField($fieldName);
            if (!$existingField->isOverridable()) {
                throw new FieldAlreadyDefined($this, $fieldName);
            }

            if (!$existingField->isCompatibleForOverride($field)) {
                throw new FieldOverrideNotCompatible($this, $fieldName, $field);
            }
        }

        $this->fields[$fieldName] = $field;
        if ($field->isRequired()) {
            $this->requiredFields[$fieldName] = $field;
        }
    }

    /**
     * @param Mixin $mixin
     *
     * @throws MixinAlreadyAdded
     */
    private function addMixin(Mixin $mixin)
    {
        $id = $mixin->getId();
        $curieStr = $id->getCurie()->toString();

        if (isset($this->mixinsByCurie[$curieStr])) {
            throw new MixinAlreadyAdded($this, $this->mixinsByCurie[$curieStr], $mixin);
        }

        $this->mixins[$id->getCurieMajor()] = $mixin;
        $this->mixinsByCurie[$curieStr] = $mixin;

        foreach ($mixin->getFields() as $field) {
            $this->addField($field);
        }
    }

    /**
     * @return SchemaId
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return SchemaCurie
     */
    public function getCurie()
    {
        return $this->id->getCurie();
    }

    /**
     * @see SchemaId::getCurieMajor
     * @return string
     */
    public function getCurieMajor()
    {
        return $this->id->getCurieMajor();
    }

    /**
     * @return SchemaQName
     */
    public function getQName()
    {
        return $this->id->getCurie()->getQName();
    }

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

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

    /**
     * Convenience method to return the name of the method that should
     * exist to handle this message.
     *
     * For example, an ImportUserV1 message would be handled by:
     * SomeClass::importUserV1(ImportUserV1 $command)
     *
     * @param bool $withMajor
     *
     * @return string
     */
    public function getHandlerMethodName($withMajor = true)
    {
        if (true === $withMajor) {
            return lcfirst($this->classShortName);
        }

        return lcfirst(str_replace('V' . $this->id->getVersion()->getMajor(), '', $this->classShortName));
    }

    /**
     * Convenience method that creates a message instance with this schema.
     *
     * @param array $data
     *
     * @return Message
     */
    public function createMessage(array $data = [])
    {
        /** @var Message $className */
        $className = $this->className;
        if (empty($data)) {
            return $className::create();
        }

        return $className::fromArray($data);
    }

    /**
     * @param string $fieldName
     *
     * @return bool
     */
    public function hasField($fieldName)
    {
        return isset($this->fields[$fieldName]);
    }

    /**
     * @param string $fieldName
     *
     * @return Field
     * @throws FieldNotDefined
     */
    public function getField($fieldName)
    {
        if (!isset($this->fields[$fieldName])) {
            throw new FieldNotDefined($this, $fieldName);
        }
        return $this->fields[$fieldName];
    }

    /**
     * @return Field[]
     */
    public function getFields()
    {
        return $this->fields;
    }

    /**
     * @return Field[]
     */
    public function getRequiredFields()
    {
        return $this->requiredFields;
    }

    /**
     * Returns true if the mixin is on this schema.  Id provided can be
     * qualified to major rev or just the curie.
     * @see SchemaId::getCurieMajor
     *
     * @param string $mixinId
     *
     * @return bool
     */
    public function hasMixin($mixinId)
    {
        return isset($this->mixins[$mixinId]) || isset($this->mixinsByCurie[$mixinId]);
    }

    /**
     * @param string $mixinId
     *
     * @return Mixin
     * @throws MixinNotDefined
     */
    public function getMixin($mixinId)
    {
        if (isset($this->mixins[$mixinId])) {
            return $this->mixins[$mixinId];
        }

        if (isset($this->mixinsByCurie[$mixinId])) {
            return $this->mixinsByCurie[$mixinId];
        }

        throw new MixinNotDefined($this, $mixinId);
    }

    /**
     * @return Mixin[]
     */
    public function getMixins()
    {
        return $this->mixins;
    }

    /**
     * Returns an array of curies with the major rev.
     * @see SchemaId::getCurieMajor
     *
     * @return array
     */
    public function getMixinIds()
    {
        return $this->mixinIds;
    }

    /**
     * Returns an array of curies (string version).
     *
     * @return array
     */
    public function getMixinCuries()
    {
        return $this->mixinCuries;
    }
}


<?php

namespace Gdbots\Pbj\Type;

use Gdbots\Common\Enum;
use Gdbots\Pbj\Assertion;
use Gdbots\Pbj\Codec;
use Gdbots\Pbj\Exception\DecodeValueFailed;
use Gdbots\Pbj\Field;

final class StringEnumType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function guard($value, Field $field)
    {
        /** @var Enum $value */
        Assertion::isInstanceOf($value, Enum::class, null, $field->getName());
        Assertion::isInstanceOf($value, $field->getClassName(), null, $field->getName());
        $v = $value->getValue();
        Assertion::string($v, null, $field->getName());

        // intentionally using strlen to get byte length, not mb_strlen
        $length = strlen($v);
        $maxBytes = $this->getMaxBytes();
        $okay = $length > 0 && $length <= $maxBytes;
        Assertion::true(
            $okay,
            sprintf(
                'Field [%s] must be between [1] and [%d] bytes, [%d] bytes given.',
                $field->getName(),
                $maxBytes,
                $length
            ),
            $field->getName()
        );
    }

    /**
     * {@inheritdoc}
     */
    public function encode($value, Field $field, Codec $codec = null)
    {
        if ($value instanceof Enum) {
            return (string) $value->getValue();
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function decode($value, Field $field, Codec $codec = null)
    {
        if (empty($value)) {
            return null;
        }

        /** @var Enum $className */
        $className = $field->getClassName();

        try {
            return $className::create((string) $value);
        } catch (\Exception $e) {
            throw new DecodeValueFailed($value, $field, $e->getMessage());
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isScalar()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isString()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getMaxBytes()
    {
        return 100;
    }
}


<?php

namespace KassaCom\SDK\Actions;

use KassaCom\SDK\Model\Types\AbstractCustomType;
use KassaCom\SDK\Exception\Validation\BadGetterException;
use KassaCom\SDK\Exception\Validation\EmptyRequiredPropertyException;
use KassaCom\SDK\Exception\Validation\InvalidPropertyException;
use KassaCom\SDK\Exception\Validation\InvalidPropertyTypeException;
use KassaCom\SDK\Model\Interfaces\DataContainerInterface;
use KassaCom\SDK\Model\Interfaces\RestorableInterface;
use KassaCom\SDK\Utils\AccessorUtil;

class ObjectRecursiveValidator
{
    /**
     * @param DataContainerInterface $object
     *
     * @throws BadGetterException
     * @throws EmptyRequiredPropertyException
     * @throws InvalidPropertyException
     * @throws InvalidPropertyTypeException
     */
    public static function validate(DataContainerInterface $object)
    {
        $existedFields = [];

        foreach ($object->getRequiredFields() as $field => $type) {
            $value = self::getFieldValue($object, $field);

            if (empty($value)) {
                throw new EmptyRequiredPropertyException(sprintf('Required property (%s) is empty', $field), 0, $field);
            }

            self::validateValue($value, $field, $type);
            $existedFields[] = $field;
        }

        foreach ($object->getOptionalFields() as $field => $type) {
            $value = self::getFieldValue($object, $field);

            if (empty($value)) {
                continue;
            }

            self::validateValue($value, $field, $type);
            $existedFields[] = $field;
        }

        foreach ($object->getThoughOneField() as $thoughFields) {
            $missedFields = array_diff($thoughFields, $existedFields);

            if (count($missedFields) === count($thoughFields)) {
                throw new EmptyRequiredPropertyException(sprintf('There must be at least one field: %s', join(' or ', $thoughFields)));
            }
        }

    }

    /**
     * @param mixed        $value
     * @param string       $field
     * @param string|array $type
     *
     * @throws InvalidPropertyException
     * @throws InvalidPropertyTypeException
     * @throws BadGetterException
     */
    protected static function validateValue($value, $field, $type)
    {

        switch ($type) {
            case RestorableInterface::TYPE_INTEGER:
                if (!is_int($value)) {
                    throw new InvalidPropertyException('Property value is not integer', 0, $field);
                }
                break;
            case RestorableInterface::TYPE_BOOLEAN:
                if (!is_bool($value)) {
                    throw new InvalidPropertyException('Property value is not boolean', 0, $field);
                }
                break;
            case RestorableInterface::TYPE_FLOAT:
                if (!is_float($value)) {
                    throw new InvalidPropertyException('Property value is not float', 0, $field);
                }
                break;
            case RestorableInterface::TYPE_STRING:
                if (!is_string($value)) {
                    throw new InvalidPropertyException('Property value is not string', 0, $field);
                }
                break;
            case RestorableInterface::TYPE_ARRAY:
                if (!is_array($value)) {
                    throw new InvalidPropertyException('Property value is not array', 0, $field);
                }
                break;
            case RestorableInterface::TYPE_DATE:
                if (!($value instanceof \DateTimeInterface)) {
                    throw new InvalidPropertyException('Property value is not date', 0, $field);
                }
                break;
            case $type instanceof AbstractCustomType:
                $type->validate($field);
                break;
            case is_array($type):
                if (empty($type) || count($type) > 1) {
                    throw new InvalidPropertyException('Incorrect array type. Please provide one element in array.');
                }

                $type = reset($type);

                foreach ((array)$value as $data) {
                    self::validateValue($data, $field, $type);
                }
                break;
            default:
                if ($value instanceof DataContainerInterface && is_subclass_of($type, DataContainerInterface::class)) {
                    self::validate($value);
                    break;
                }

                throw new InvalidPropertyTypeException('There is no provided type of property', 0, $field);
        }
    }

    /**
     * @param DataContainerInterface $object
     * @param                        $field
     *
     * @return mixed
     * @throws BadGetterException
     */
    protected static function getFieldValue(DataContainerInterface $object, $field)
    {
        $getter = $method = AccessorUtil::getter($field);

        if (!method_exists($object, $getter)) {
            $isser = $method = AccessorUtil::isser($field);

            if (!method_exists($object, $isser)) {
                throw new BadGetterException(sprintf('Getter %s or %s for object does not exists', $getter, $isser));
            }
        }

        return $object->{$method}();
    }
}


<?php

/**
 * This file is part of MetaModels/attribute_translatedselect.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/attribute_translatedselect
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Christian de la Haye <service@delahaye.de>
 * @author     Andreas Isaak <info@andreas-isaak.de>
 * @author     Markus Gerards <markus.gerards@googlemail.com>
 * @author     Paul Pflugradt <paulpflugradt@googlemail.com>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/attribute_translatedselect/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\AttributeTranslatedSelectBundle\Attribute;

use MetaModels\Attribute\ITranslated;
use MetaModels\AttributeSelectBundle\Attribute\Select;

/**
 * This is the MetaModelAttribute class for handling translated select attributes.
 */
class TranslatedSelect extends Select implements ITranslated
{
    /**
     * Determine the correct language column to use.
     *
     * @return string
     */
    protected function getLanguageColumn()
    {
        return $this->get('select_langcolumn');
    }

    /**
     * Determine the correct sorting table to use.
     *
     * @return string
     */
    protected function getSortingOverrideTable()
    {
        return $this->get('select_srctable') ?: false;
    }

    /**
     * Determine the correct sorting column to use.
     *
     * @return string
     */
    protected function getSortingOverrideColumn()
    {
        return $this->get('select_srcsorting') ?: 'id';
    }

    /**
     * {@inheritdoc}
     */
    public function sortIds($idList, $strDirection)
    {
        $metaModel    = $this->getMetaModel();
        $strTableName = $this->getSortingOverrideTable();
        if ($strTableName) {
            $strColNameId  = 'id';
            $strSortColumn = $this->getSortingOverrideColumn();

            return $this->connection->createQueryBuilder()
                ->select('m.id')
                ->from($metaModel->getTableName(), 'm')
                ->leftJoin('m', $strTableName, 't', sprintf('t.%s=m.%s', $strColNameId, $this->getColName()))
                ->orderBy('t.' . $strSortColumn, $strDirection)
                ->execute()
                ->fetchAll(\PDO::FETCH_COLUMN, 0);
        }

        $addWhere = $this->getAdditionalWhere();
        $langSet  = sprintf('\'%s\',\'%s\'', $metaModel->getActiveLanguage(), $metaModel->getFallbackLanguage());

        $subSelect = $this->connection->createQueryBuilder()
            ->select('z.id')
            ->from($this->getSelectSource(), 'z')
            ->where($this->getLanguageColumn() . 'IN(:langset)')
            ->andWhere('z.' . $this->getIdColumn() . '=m.' . $this->getColName())
            ->orderBy(sprintf('FIELD(z.%s,%s)', $this->getLanguageColumn(), $langSet))
            ->setMaxResults(1);

        if ($addWhere) {
            $subSelect->andWhere($addWhere);
        }

        $sorted = $this->connection->createQueryBuilder()
            ->select('m.id')
            ->from($this->getMetaModel()->getTableName(), 'm')
            ->leftJoin('m', $this->getSelectSource(), 's', sprintf('s.id = (%s)', $subSelect->getSQL()))
            ->where('m.id IN(:ids)')
            ->orderBy('s.' . $this->getSortingColumn(), $strDirection)
            ->setParameter('ids', $idList)
            ->setParameter('langset', $langSet)
            ->execute()->fetchAll(\PDO::FETCH_COLUMN, 0);

        return $sorted;
    }

    /**
     * {@inheritdoc}
     */
    public function getAttributeSettingNames()
    {
        return \array_merge(
            parent::getAttributeSettingNames(),
            [
                'select_langcolumn',
                'select_srctable',
                'select_srcsorting'
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function valueToWidget($varValue)
    {
        $strColNameWhere = $this->getAdditionalWhere();
        $strColNameAlias = $this->getAliasColumn();
        if (!$strColNameAlias) {
            $strColNameAlias = $this->getIdColumn();
        }

        // Easy out, we have the correct language.
        if ($varValue[$this->getLanguageColumn()] == $this->getMetaModel()->getActiveLanguage()) {
            return $varValue[$strColNameAlias];
        }

        // Translate to current language.
        $builder = $this->connection->createQueryBuilder()
            ->select('s.*')
            ->from($this->getSelectSource(), 's')
            ->where($this->getIdColumn() . '=:id')
            ->andWhere($this->getLanguageColumn() . '=:language')
            ->setParameter('id', $varValue[$this->getIdColumn()])
            ->setParameter('language', $this->getMetaModel()->getActiveLanguage());

        if ($strColNameWhere) {
            $builder->andWhere($strColNameWhere);
        }

        $statement = $builder->execute();
        $result    = $statement->fetch(\PDO::FETCH_OBJ);

        return $result->$strColNameAlias;
    }

    /**
     * {@inheritdoc}
     */
    public function widgetToValue($varValue, $itemId)
    {
        $strColNameAlias = $this->getAliasColumn();
        $strColNameId    = $this->getIdColumn();
        $strColNameWhere = $this->getAdditionalWhere();
        $strColNameLang  = $this->getLanguageColumn();

        if (!$strColNameAlias) {
            $strColNameAlias = $strColNameId;
        }

        $builder = $this->connection->createQueryBuilder()
            ->select('*')
            ->from($this->getSelectSource())
            ->where($strColNameAlias . '=:alias')
            ->andWhere($strColNameLang . 'IN(:languages)')
            ->setParameter('alias', $varValue)
            ->setParameter(
                'languages',
                [$this->getMetaModel()->getActiveLanguage(), $this->getMetaModel()->getFallbackLanguage()]
            );

        if ($strColNameWhere) {
            $builder->andWhere($strColNameWhere);
        }

        return $builder
            ->execute()
            ->fetch(\PDO::FETCH_ASSOC);
    }

    /**
     * Retrieve the sorting part for the getFilterOptions() queries.
     *
     * @return string
     */
    protected function getFilterOptionsOrderBy()
    {
        if ($this->getSortingOverrideTable() && $this->getSortingOverrideColumn()) {
            return \sprintf(
                'FIELD(%s.id, (SELECT GROUP_CONCAT(id ORDER BY %s) FROM %s)),',
                $this->getSelectSource(),
                $this->getSortingOverrideColumn(),
                $this->getSortingOverrideTable()
            );
        }

        return '';
    }

    /**
     * {@inheritdoc}
     */
    public function getFilterOptionsForUsedOnly($usedOnly)
    {
        $tableName   = $this->getSelectSource();
        $idColumn    = $this->getIdColumn();
        $langColumn  = $this->getLanguageColumn();
        $addWhere    = $this->getAdditionalWhere();
        $firstOrder  = $this->getFilterOptionsOrderBy();
        $secondOrder = $this->getSortingColumn();
        $langSet     = \sprintf(
            '\'%s\',\'%s\'',
            $this->getMetaModel()->getActiveLanguage(),
            $this->getMetaModel()->getFallbackLanguage()
        );

        if ($usedOnly) {
            return $this
                ->getDatabase()
                ->prepare(
                    \sprintf(
                        'SELECT COUNT(%1$s.%2$s) as mm_count, %1$s.*
                            FROM %3$s
                            LEFT JOIN %1$s ON (%1$s.id = (SELECT
                                %1$s.id
                                FROM %1$s
                                WHERE %5$s IN (%6$s)
                                AND (%1$s.%2$s=%3$s.%4$s)
                                %7$s
                                ORDER BY FIELD(%1$s.%5$s,%6$s)
                                LIMIT 1
                            ))
                            GROUP BY %1$s.%2$s
                            ORDER BY %9$s %8$s',
                        // @codingStandardsIgnoreStart - we want to keep the numbers at the end of the lines below.
                        $tableName,                                // 1
                        $idColumn,                                 // 2
                        $this->getMetaModel()->getTableName(),     // 3
                        $this->getColName(),                       // 4
                        $langColumn,                               // 5
                        $langSet,                                  // 6
                        ($addWhere ? ' AND ('.$addWhere.')' : ''), // 7
                        $secondOrder,                              // 8
                        $firstOrder                                // 9
                        // @codingStandardsIgnoreEnd
                    )
                )
                ->execute();
        }

        return $this
            ->getDatabase()
            ->prepare(
                \sprintf(
                    'SELECT COUNT(%1$s.%2$s) as mm_count, %1$s.*
                        FROM %1$s
                        WHERE %3$s IN (%4$s)
                        %5$s
                        GROUP BY %1$s.%2$s
                        ORDER BY %7$s %6$s',
                    // @codingStandardsIgnoreStart - we want to keep the numbers at the end of the lines below.
                    $tableName,                                // 1
                    $idColumn,                                 // 2
                    $langColumn,                               // 3
                    $langSet,                                  // 4
                    ($addWhere ? ' AND ('.$addWhere.')' : ''), // 5
                    $secondOrder,                              // 6
                    $firstOrder                                // 7
                    // @codingStandardsIgnoreEnd
                )
            )
            ->execute();
    }

    /**
     * {@inheritdoc}
     *
     * Fetch filter options from foreign table.
     */
    public function getFilterOptions($idList, $usedOnly, &$arrCount = null)
    {
        if (($idList !== null) && empty($idList)) {
            return [];
        }

        $strTableName = $this->getSelectSource();
        $strColNameId = $this->getIdColumn();

        if (!($strTableName && $strColNameId)) {
            return [];
        }

        if ($idList) {
            $strColNameWhere = $this->getAdditionalWhere();
            $strLangSet      = \sprintf(
                '\'%s\',\'%s\'',
                $this->getMetaModel()->getActiveLanguage(),
                $this->getMetaModel()->getFallbackLanguage()
            );

            $objValue = $this
                ->getDatabase()
                ->prepare(
                    \sprintf(
                        'SELECT COUNT(%1$s.%2$s) as mm_count, %1$s.*
                        FROM %3$s
                        LEFT JOIN %1$s ON (%1$s.id = (SELECT
                            %1$s.id
                            FROM %1$s
                            WHERE %7$s IN (%8$s)
                            AND (%1$s.%2$s=%3$s.%4$s)
                            %6$s
                            ORDER BY FIELD(%1$s.%7$s,%8$s)
                            LIMIT 1
                        ))
                        WHERE %3$s.id IN (%5$s)
                        GROUP BY %1$s.%2$s
                        ORDER BY %10$s %9$s',
                        // @codingStandardsIgnoreStart - we want to keep the numbers at the end of the lines below.
                        $strTableName,                                               // 1
                        $strColNameId,                                               // 2
                        $this->getMetaModel()->getTableName(),                       // 3
                        $this->getColName(),                                         // 4
                        \implode(',', $idList),                                       // 5
                        ($strColNameWhere ? ' AND (' . $strColNameWhere . ')' : ''), // 6
                        $this->getLanguageColumn(),                                  // 7
                        $strLangSet,                                                 // 8
                        $this->getSortingColumn(),                                   // 9
                        $this->getFilterOptionsOrderBy()                             // 10
                        // @codingStandardsIgnoreEnd
                    )
                )
                ->execute($this->get('id'));
        } else {
            $objValue = $this->getFilterOptionsForUsedOnly($usedOnly);
        }

        return $this->convertOptionsList($objValue, $this->getAliasColumn(), $this->getValueColumn(), $arrCount);
    }

    /**
     * {@inheritdoc}
     *
     * Search the attribute in the current language.
     */
    public function searchFor($strPattern)
    {
        return $this->searchForInLanguages($strPattern, [$this->getMetaModel()->getActiveLanguage()]);
    }

    /**
     * {@inheritdoc}
     */
    public function getDataFor($arrIds)
    {
        $strActiveLanguage   = $this->getMetaModel()->getActiveLanguage();
        $strFallbackLanguage = $this->getMetaModel()->getFallbackLanguage();

        $arrReturn = $this->getTranslatedDataFor($arrIds, $strActiveLanguage);

        // Second round, fetch fallback languages if not all items could be resolved.
        if ((\count($arrReturn) < \count($arrIds)) && ($strActiveLanguage != $strFallbackLanguage)) {
            $arrFallbackIds = [];
            foreach ($arrIds as $intId) {
                if (empty($arrReturn[$intId])) {
                    $arrFallbackIds[] = $intId;
                }
            }

            if ($arrFallbackIds) {
                $arrFallbackData = $this->getTranslatedDataFor($arrFallbackIds, $strFallbackLanguage);
                // Cannot use array_merge here as it would renumber the keys.
                foreach ($arrFallbackData as $intId => $arrValue) {
                    $arrReturn[$intId] = $arrValue;
                }
            }
        }
        return $arrReturn;
    }

    /**
     * {@inheritdoc}
     */
    public function setDataFor($arrValues)
    {
        $this->setTranslatedDataFor($arrValues, $this->getMetaModel()->getActiveLanguage());
    }

    /**
     * {@inheritdoc}
     *
     * Search the attribute in the given languages.
     */
    public function searchForInLanguages($strPattern, $arrLanguages = [])
    {
        $objDB              = $this->getDatabase();
        $strTableNameId     = $this->getSelectSource();
        $strColNameId       = $this->getIdColumn();
        $strColNameLangCode = $this->getLanguageColumn();
        $strColValue        = $this->getValueColumn();
        $strColAlias        = $this->getAliasColumn();
        $strColNameWhere    = $this->getAdditionalWhere();
        $arrReturn          = [];

        if ($strTableNameId && $strColNameId) {
            $strMetaModelTableName   = $this->getMetaModel()->getTableName();
            $strMetaModelTableNameId = $strMetaModelTableName.'_id';

            $strPattern = \str_replace(['*', '?'], ['%', '_'], $strPattern);

            // Using aliased join here to resolve issue #3 for normal select attributes
            // (SQL error for self referencing table).
            $objValue = $objDB->prepare(\sprintf(
                'SELECT sourceTable.*, %2$s.id AS %3$s
                FROM %1$s sourceTable
                RIGHT JOIN %2$s ON (sourceTable.%4$s=%2$s.%5$s)
                WHERE '.($arrLanguages ? '(sourceTable.%6$s IN (%7$s))' : '').'
                AND (sourceTable.%8$s LIKE ? OR sourceTable.%9$s LIKE ?) %10$s',
                // @codingStandardsIgnoreStart - we want to keep the numbers at the end of the lines below.
                $strTableNameId,                                      // 1
                $strMetaModelTableName,                               // 2
                $strMetaModelTableNameId,                             // 3
                $strColNameId,                                        // 4
                $this->getColName(),                                  // 5
                $strColNameLangCode,                                  // 6
                '\'' . \implode('\',\'', $arrLanguages) . '\'',        // 7
                $strColValue,                                         // 8
                $strColAlias,                                         // 9
                ($strColNameWhere ? ('AND ' . $strColNameWhere) : '') // 10
            // @codingStandardsIgnoreEnd
            ))
            ->execute($strPattern, $strPattern);

            while ($objValue->next()) {
                $arrReturn[] = $objValue->$strMetaModelTableNameId;
            }
        }
        return $arrReturn;
    }

    /**
     * {@inheritdoc}
     */
    public function setTranslatedDataFor($arrValues, $strLangCode)
    {
        $strMetaModelTableName = $this->getMetaModel()->getTableName();
        $strTableName          = $this->getSelectSource();
        $strColNameId          = $this->getIdColumn();

        if ($strTableName && $strColNameId) {
            $objDB    = $this->getDatabase();
            $strQuery = \sprintf(
                'UPDATE %1$s SET %2$s=? WHERE %1$s.id=?',
                $strMetaModelTableName,
                $this->getColName()
            );

            foreach ($arrValues as $intItemId => $arrValue) {
                $objDB->prepare($strQuery)->execute($arrValue[$strColNameId], $intItemId);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getTranslatedDataFor($arrIds, $strLangCode)
    {
        $objDB              = $this->getDatabase();
        $strTableNameId     = $this->getSelectSource();
        $strColNameId       = $this->getIdColumn();
        $strColNameLangCode = $this->getLanguageColumn();
        $strColNameWhere    = $this->getAdditionalWhere();
        $arrReturn          = [];

        if ($strTableNameId && $strColNameId) {
            $strMetaModelTableName   = $this->getMetaModel()->getTableName();
            $strMetaModelTableNameId = $strMetaModelTableName.'_id';

            // Using aliased join here to resolve issue #3 for normal select attributes
            // (SQL error for self referencing table).
            $objValue = $objDB->prepare(\sprintf(
                'SELECT sourceTable.*, %2$s.id AS %3$s
                FROM %1$s sourceTable
                LEFT JOIN %2$s
                    ON ((sourceTable.%7$s=?) AND (sourceTable.%4$s=%2$s.%5$s))
                WHERE %2$s.id IN (%6$s) %8$s',
                // @codingStandardsIgnoreStart - we want to keep the numbers at the end of the lines below.
                $strTableNameId,                                        // 1
                $strMetaModelTableName,                                 // 2
                $strMetaModelTableNameId,                               // 3
                $strColNameId,                                          // 4
                $this->getColName(),                                    // 5
                \implode(',', $arrIds),                                  // 6
                $strColNameLangCode,                                    // 7
                ($strColNameWhere ? ' AND ('.$strColNameWhere.')' : '') // 8
            // @codingStandardsIgnoreEnd
            ))
                ->execute($strLangCode);
            while ($objValue->next()) {
                $arrReturn[$objValue->$strMetaModelTableNameId] = $objValue->row();
            }
        }
        return $arrReturn;
    }

    /**
     * {@inheritdoc}
     */
    public function unsetValueFor($arrIds, $strLangCode)
    {
        parent::unsetDataFor($arrIds);
    }
}


<?php

namespace Gdbots\Pbj\WellKnown;

use Gdbots\Common\FromArray;
use Gdbots\Common\ToArray;
use Gdbots\Pbj\Assertion;
use Gdbots\Pbj\Enum\DynamicFieldKind;
use Gdbots\Pbj\Enum\FieldRule;
use Gdbots\Pbj\Exception\InvalidArgumentException;
use Gdbots\Pbj\Field;
use Gdbots\Pbj\Type\BooleanType;
use Gdbots\Pbj\Type\DateType;
use Gdbots\Pbj\Type\FloatType;
use Gdbots\Pbj\Type\IntType;
use Gdbots\Pbj\Type\StringType;
use Gdbots\Pbj\Type\TextType;

/**
 * DynamicField is a wrapper for fields which would not be ideal as a map because
 * you don't know what the field name is going to be until runtime or the number
 * of fields you'll end up having will be too large.
 *
 * A common use case is a polling or custom form service.  Eventually the number of
 * fields you have is in the thousands and systems like SQL, ElasticSearch will not
 * do well with that many fields.  DynamicField is designed to be a "named union".
 *
 * For example:
 *  [
 *      // the name of the field
 *      'name' => 'your-field-name',
 *      // only one of the following values can be populated.
 *      'bool_val' => true,
 *      'date_val' => '2015-12-25',
 *      'float_val' => 1.0,
 *      'int_val' => 1,
 *      'string_val' => 'string',
 *      'text_val' => 'some text',
 *  ]
 */
final class DynamicField implements FromArray, ToArray, \JsonSerializable
{
    /**
     * Regular expression pattern for matching a valid dynamic field name.
     *
     * @constant string
     */
    const VALID_NAME_PATTERN = '/^[a-zA-Z_]{1}[a-zA-Z0-9_-]*$/';

    /**
     * Fields are only used to allow for type guarding/encoding/decoding.
     *
     * @var Field[]
     */
    private static $fields;

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

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

    /** @var mixed */
    private $value;

    /**
     * @param string $name
     * @param DynamicFieldKind $kind
     * @param mixed $value
     */
    private function __construct($name, DynamicFieldKind $kind, $value)
    {
        Assertion::betweenLength($name, 1, 127);
        Assertion::regex($name, self::VALID_NAME_PATTERN,
            sprintf('DynamicField name [%s] must match pattern [%s].', $name, self::VALID_NAME_PATTERN)
        );

        $this->name = $name;
        $this->kind = $kind->getValue();
        $field = self::createField($this->kind);

        $this->value = $field->getType()->decode($value, $field);
        $field->guardValue($this->value);
    }

    /**
     * @param string $kind
     *
     * @return Field
     */
    private static function createField($kind)
    {
        if (!isset(self::$fields[$kind])) {
            switch ($kind) {
                case DynamicFieldKind::STRING_VAL:
                    $type = StringType::create();
                    break;

                case DynamicFieldKind::TEXT_VAL:
                    $type = TextType::create();
                    break;

                case DynamicFieldKind::INT_VAL:
                    $type = IntType::create();
                    break;

                case DynamicFieldKind::BOOL_VAL:
                    $type = BooleanType::create();
                    break;

                case DynamicFieldKind::FLOAT_VAL:
                    $type = FloatType::create();
                    break;

                case DynamicFieldKind::DATE_VAL:
                    $type = DateType::create();
                    break;

                default:
                    throw new InvalidArgumentException(sprintf('DynamicField "%s" is not a valid type.', $kind));
            }

            self::$fields[$kind] = new Field($kind, $type, FieldRule::A_SINGLE_VALUE(), true);
        }

        return self::$fields[$kind];
    }

    /**
     * @param string $name
     * @param bool $value
     *
     * @return self
     */
    public static function createBoolVal($name, $value = false)
    {
        return new self($name, DynamicFieldKind::BOOL_VAL(), $value);
    }

    /**
     * @param string $name
     * @param \DateTime $value
     *
     * @return self
     */
    public static function createDateVal($name, \DateTime $value)
    {
        return new self($name, DynamicFieldKind::DATE_VAL(), $value);
    }

    /**
     * @param string $name
     * @param float $value
     *
     * @return self
     */
    public static function createFloatVal($name, $value = 0.0)
    {
        return new self($name, DynamicFieldKind::FLOAT_VAL(), $value);
    }

    /**
     * @param string $name
     * @param int $value
     *
     * @return self
     */
    public static function createIntVal($name, $value = 0)
    {
        return new self($name, DynamicFieldKind::INT_VAL(), $value);
    }

    /**
     * @param string $name
     * @param string $value
     *
     * @return self
     */
    public static function createStringVal($name, $value)
    {
        return new self($name, DynamicFieldKind::STRING_VAL(), $value);
    }

    /**
     * @param string $name
     * @param string $value
     *
     * @return self
     */
    public static function createTextVal($name, $value)
    {
        return new self($name, DynamicFieldKind::TEXT_VAL(), $value);
    }

    /**
     * {@inheritdoc}
     */
    public static function fromArray(array $data = [])
    {
        if (!isset($data['name'])) {
            throw new InvalidArgumentException('DynamicField "name" property must be set.');
        }

        $name = $data['name'];
        unset($data['name']);
        $kind = key($data);

        try {
            $kind = DynamicFieldKind::create($kind);
        } catch (\Exception $e) {
            throw new InvalidArgumentException(sprintf('DynamicField "%s" is not a valid kind.', $kind));
        }

        return new self($name, $kind, $data[$kind->getValue()]);
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        $field = self::createField($this->kind);
        return ['name' => $this->name, $this->kind => $field->getType()->encode($this->value, $field)];
    }

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

    /**
     * @return string
     */
    public function toString()
    {
        return json_encode($this);
    }

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

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

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

    /**
     * @return Field
     */
    public function getField()
    {
        return self::createField($this->kind);
    }

    /**
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * @param DynamicField $other
     *
     * @return bool
     */
    public function equals(DynamicField $other)
    {
        return $this->name === $other->name
            && $this->kind === $other->kind
            && $this->value === $other->value;
    }
}


<?php

namespace Gdbots\Pbj;

use Gdbots\Common\FromArray;
use Gdbots\Common\ToArray;
use Gdbots\Pbj\Exception\InvalidArgumentException;
use Gdbots\Pbj\Exception\LogicException;

/**
 * Represents a reference to a message.  Typically used to link messages
 * together via a correlator or "links".  Format for a reference:
 * vendor:package:category:message:id#tag (tag is optional)
 */
final class MessageRef implements FromArray, ToArray, \JsonSerializable
{
    /** @var SchemaCurie */
    private $curie;

    /**
     * Any string matching pattern /^[\w\/\.:-]+$/
     * @var string
     */
    private $id;

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

    /**
     * When serialized we store the curie as a string so we can
     * restore the singleton instance upon wakeup.
     *
     * @var string
     */
    private $cs;

    /**
     * todo: review random failures from YamlSerializer on $id when no tag is present.
     *
     * @param SchemaCurie $curie
     * @param string      $id
     * @param string      $tag The tag will be automatically fixed to a slug-formatted-string.
     *
     * @throws \Exception
     */
    public function __construct(SchemaCurie $curie, $id, $tag = null)
    {
        $this->curie = $curie;
        $this->id = trim((string)$id) ?: 'null';
        Assertion::regex($this->id, '/^[\w\/\.:-]+$/', null, 'MessageRef.id');

        if (null !== $tag) {
            $this->tag = strtolower(preg_replace('/[^\w\.-]/', '-', $tag)) ?: null;
        }

        if ($this->curie->isMixin()) {
            throw new LogicException('Mixins cannot be used in a MessageRef.');
        }
    }

    /**
     * {@inheritdoc}
     */
    public static function fromArray(array $data = [])
    {
        if (isset($data['curie'])) {
            $id = isset($data['id']) ? $data['id'] : 'null';
            $tag = isset($data['tag']) ? $data['tag'] : null;
            return new self(SchemaCurie::fromString($data['curie']), $id, $tag);
        }
        throw new InvalidArgumentException('Payload must be a MessageRef type.');
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        if (null !== $this->tag) {
            return ['curie' => $this->curie->toString(), 'id' => $this->id, 'tag' => $this->tag];
        }

        return ['curie' => $this->curie->toString(), 'id' => $this->id];
    }

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

    /**
     * @param string $string A string with format curie:id#tag
     *
     * @return self
     */
    public static function fromString($string)
    {
        $parts = explode('#', $string, 2);
        $ref = $parts[0];
        $tag = isset($parts[1]) ? $parts[1] : null;

        $parts = explode(':', $ref, 5);
        $id = array_pop($parts);
        $curie = SchemaCurie::fromString(implode(':', $parts));
        return new self($curie, $id, $tag);
    }

    /**
     * @return string
     */
    public function toString()
    {
        if (null !== $this->tag) {
            return $this->curie->toString() . ':' . $this->id . '#' . $this->tag;
        }

        return $this->curie->toString() . ':' . $this->id;
    }

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

    /**
     * @return SchemaCurie
     */
    public function getCurie()
    {
        return $this->curie;
    }

    /**
     * @return bool
     */
    public function hasId()
    {
        return 'null' != $this->id;
    }

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

    /**
     * @return bool
     */
    public function hasTag()
    {
        return null !== $this->tag;
    }

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

    /**
     * @param MessageRef $other
     *
     * @return bool
     */
    public function equals(MessageRef $other)
    {
        return $this->toString() === $other->toString();
    }

    /**
     * {@inheritdoc}
     */
    public function __sleep()
    {
        $this->cs = $this->curie->toString();
        return ['cs', 'id', 'tag'];
    }

    /**
     * {@inheritdoc}
     */
    public function __wakeup()
    {
        $this->curie = SchemaCurie::fromString($this->cs);
        $this->cs = null;
    }
}


<?php

namespace Gdbots\Pbj\Type;

use Gdbots\Pbj\Assertion;
use Gdbots\Pbj\Codec;
use Gdbots\Pbj\Exception\DecodeValueFailed;
use Gdbots\Pbj\Field;

final class DateType extends AbstractType
{
    /** @var \DateTimeZone */
    private $utc;

    /**
     * {@inheritdoc}
     */
    public function guard($value, Field $field)
    {
        /** @var \DateTimeInterface $value */
        Assertion::isInstanceOf($value, \DateTimeInterface::class, null, $field->getName());
    }

    /**
     * {@inheritdoc}
     */
    public function encode($value, Field $field, Codec $codec = null)
    {
        if ($value instanceof \DateTimeInterface) {
            return $value->format('Y-m-d');
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function decode($value, Field $field, Codec $codec = null)
    {
        if (empty($value)) {
            return null;
        }

        if ($value instanceof \DateTimeInterface) {
            // ensures we're always in UTC and have no time parts.
            $value = $value->format('Y-m-d');
        }

        $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $value, $this->getUtcTimeZone());
        if ($date instanceof \DateTimeImmutable) {
            return $date;
        }

        throw new DecodeValueFailed(
            $value,
            $field,
            sprintf(
                'Format must be [Y-m-d].  Errors: [%s]',
                print_r(\DateTimeImmutable::getLastErrors(), true)
            )
        );
    }

    /**
     * {@inheritdoc}
     */
    public function isScalar()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isString()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function allowedInSet()
    {
        return false;
    }

    /**
     * @return \DateTimeZone
     */
    private function getUtcTimeZone()
    {
        if (null === $this->utc) {
            $this->utc = new \DateTimeZone('UTC');
        }

        return $this->utc;
    }
}


<?php
declare(strict_types=1);

namespace Gdbots\Pbj;

use Gdbots\Pbj\Exception\MoreThanOneMessageForMixin;
use Gdbots\Pbj\Exception\NoMessageForCurie;
use Gdbots\Pbj\Exception\NoMessageForMixin;
use Gdbots\Pbj\Exception\NoMessageForQName;
use Gdbots\Pbj\Exception\NoMessageForSchemaId;

final class MessageResolver
{
    /**
     * An array of all the available schemas keyed by a curie or curie major.
     * The value is an int used to correlate with the other maps.
     *
     * @var int[]
     */
    private static $curies = [];

    /**
     * An array of all the classes, the numeric key relates to the int
     * from the curies array.
     *
     * @var string[]
     */
    private static $classes = [];

    /**
     * An array of ints keyed by the mixin curie. The value is a list of
     * ints which relate to the curies that are using the mixin.
     *
     * @var array
     */
    private static $mixins = [];

    /**
     * An array of resolved lookups by mixin, keyed by the mixin id with major rev
     *
     * @see SchemaId::getCurieMajor
     *
     * @var Schema[]
     */
    private static $resolvedMixins = [];

    /**
     * An array of resolved lookups by qname.
     *
     * @see SchemaQName
     *
     * @var SchemaCurie[]
     */
    private static $resolvedQnames = [];

    /**
     * An array with the following structure (gdbots/pbjc-php automatically creates this)
     * [
     *     'curies' => [
     *         'vendor:package:category:message' => 1, // int is used to connect other values
     *     ],
     *     'classes' => [
     *         1 => 'Vendor\Package\Category\MessageV1', // 1 refers to the value of the curies entry
     *     ],
     *     'mixins' => [
     *         'gdbots:pbjx:mixin:command:v1' => [1] // 1 refers to the value of the curies entry
     *     ],
     * ]
     *
     * @param array $manifest
     */
    public static function registerManifest(array $manifest): void
    {
        self::$curies = $manifest['curies'] ?? [];
        self::$classes = $manifest['classes'] ?? [];
        self::$mixins = $manifest['mixins'] ?? [];
    }

    /**
     * Returns all of the registed schemas.
     *
     * @return Message[]
     */
    public static function all(): array
    {
        return array_values(self::$classes);
    }

    /**
     * Returns the fully qualified php class name to be used for the provided schema id.
     *
     * @param SchemaId $id
     *
     * @return Message
     *
     * @throws NoMessageForSchemaId
     */
    public static function resolveId(SchemaId $id): string
    {
        $curieMajor = $id->getCurieMajor();
        if (isset(self::$curies[$curieMajor])) {
            return self::$classes[self::$curies[$curieMajor]];
        }

        $curie = $id->getCurie()->toString();
        if (isset(self::$curies[$curie])) {
            return self::$classes[self::$curies[$curie]];
        }

        throw new NoMessageForSchemaId($id);
    }

    /**
     * Returns the fully qualified php class name to be used for the provided curie.
     *
     * @param SchemaCurie|string $curie
     *
     * @return Message
     *
     * @throws NoMessageForCurie
     */
    public static function resolveCurie($curie): string
    {
        $key = (string)$curie;
        if (isset(self::$curies[$key])) {
            return self::$classes[self::$curies[$key]];
        }

        throw new NoMessageForCurie(SchemaCurie::fromString($key));
    }

    /**
     * @param SchemaQName|string $qname
     *
     * @return SchemaCurie
     *
     * @throws NoMessageForQName
     */
    public static function resolveQName($qname): SchemaCurie
    {
        if (!$qname instanceof SchemaQName) {
            $qname = SchemaQName::fromString((string)$qname);
        }

        $key = $qname->toString();

        if (isset(self::$resolvedQnames[$key])) {
            return self::$resolvedQnames[$key];
        }

        $qvendor = $qname->getVendor();
        $qmessage = $qname->getMessage();

        foreach (self::$curies as $curie => $id) {
            list($vendor, $package, $category, $message) = explode(':', $curie);
            if ($qvendor === $vendor && $qmessage === $message) {
                return self::$resolvedQnames[$key] = SchemaCurie::fromString($vendor . ':' . $package . ':' . $category . ':' . $message);
            }
        }

        throw new NoMessageForQName($qname);
    }

    /**
     * Adds a single schema to the resolver.  This is used in tests or dynamic
     * message schema creation (not a typical or recommended use case).
     *
     * @param Schema $schema
     */
    public static function registerSchema(Schema $schema): void
    {
        $nextId = count(self::$curies) + 10000;
        self::$curies[$schema->getId()->getCurieMajor()] = $nextId;
        self::$classes[$nextId] = $schema->getClassName();
        foreach ($schema->getMixinIds() as $mixin) {
            unset(self::$resolvedMixins[$mixin]);
            if (!isset(self::$mixins[$mixin])) {
                self::$mixins[$mixin] = [];
            }

            self::$mixins[$mixin][] = $nextId;
        }
    }

    /**
     * Adds a single schema id and class name.
     * @see SchemaId::getCurieMajor
     *
     * @param SchemaId|string $id
     * @param string          $className
     */
    public static function register($id, string $className): void
    {
        @trigger_error(sprintf('"%s" is deprecated. Use "registerManifest" instead.', __CLASS__), E_USER_DEPRECATED);

        if ($id instanceof SchemaId) {
            $id = $id->getCurieMajor();
        }

        $nextId = count(self::$curies) + 20000;
        self::$curies[$id] = $nextId;
        self::$classes[$nextId] = $className;
    }

    /**
     * Registers an array of id => className values to the resolver.
     *
     * @param array $map
     */
    public static function registerMap(array $map)
    {
        @trigger_error(sprintf('"%s" is deprecated. Use "registerManifest" instead.', __CLASS__), E_USER_DEPRECATED);
        $nextId = count(self::$curies) + 30000;
        foreach ($map as $curie => $class) {
            ++$nextId;
            self::$curies[$curie] = $nextId;
            self::$classes[$nextId] = $class;
        }
    }

    /**
     * Return the one schema expected to be using the provided mixin.
     *
     * @param Mixin|string $mixin Mixin or curie major
     *
     * @return Schema
     *
     * @throws MoreThanOneMessageForMixin
     * @throws NoMessageForMixin
     */
    public static function findOneUsingMixin($mixin): Schema
    {
        $schemas = self::findAllUsingMixin($mixin);
        if (1 !== count($schemas)) {
            throw new MoreThanOneMessageForMixin($mixin, $schemas);
        }

        return current($schemas);
    }

    /**
     * Returns an array of Schemas using the provided mixin.
     *
     * @param Mixin|string $mixin Mixin or curie major
     *
     * @return Schema[]
     *
     * @throws NoMessageForMixin
     */
    public static function findAllUsingMixin($mixin): array
    {
        if ($mixin instanceof Mixin) {
            $key = $mixin->getId()->getCurieMajor();
        } else {
            $key = $mixin;
        }

        if (!isset(self::$resolvedMixins[$key])) {
            $schemas = [];
            foreach ((self::$mixins[$key] ?? []) as $id) {
                $schemas[] = self::$classes[$id]::schema();
            }
            self::$resolvedMixins[$key] = $schemas;
        }

        if (empty(self::$resolvedMixins[$key])) {
            throw new NoMessageForMixin($mixin);
        }

        return self::$resolvedMixins[$key];
    }
}


<?php

namespace Gdbots\Pbj\Marshaler\Elastica;

use Elastica\Type\Mapping;
use Gdbots\Common\Util\SlugUtils;
use Gdbots\Common\Util\StringUtils;
use Gdbots\Pbj\Enum\Format;
use Gdbots\Pbj\Field;
use Gdbots\Pbj\Message;
use Gdbots\Pbj\Schema;

class MappingFactory
{
    /**
     * During the creation of a mapping any string types that are indexed will
     * use the "standard" analyzer unless something else is specified.
     * @link https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
     * @var string
     */
    protected $defaultAnalyzer = null;

    /**
     * Map of pbj type -> elastica mapping types.
     * @var array
     */
    protected $types = [
        'big-int'           => ['type' => 'long', 'include_in_all' => false],
        'binary'            => ['type' => 'binary'],
        'blob'              => ['type' => 'binary'],
        'boolean'           => ['type' => 'boolean', 'include_in_all' => false],
        'date'              => ['type' => 'date', 'include_in_all' => false],
        'date-time'         => ['type' => 'date', 'include_in_all' => false],
        'decimal'           => ['type' => 'double', 'include_in_all' => false],
        'dynamic-field'     => [
            'type'       => 'object',
            'properties' => [
                'name'       => ['type' => 'keyword', 'normalizer' => 'pbj_keyword', 'include_in_all' => false],
                'bool_val'   => ['type' => 'boolean', 'include_in_all' => false],
                'date_val'   => ['type' => 'date', 'include_in_all' => false],
                'float_val'  => ['type' => 'float', 'include_in_all' => false],
                'int_val'    => ['type' => 'long', 'include_in_all' => false],
                'string_val' => [
                    'type'   => 'text',
                    'fields' => ['raw' => ['type' => 'keyword', 'normalizer' => 'pbj_keyword']],
                ],
                'text_val'   => ['type' => 'text'],
            ],
        ],
        'float'             => ['type' => 'float', 'include_in_all' => false],
        'geo-point'         => ['type' => 'geo_point', 'include_in_all' => false],
        'identifier'        => ['type' => 'keyword', 'include_in_all' => false],
        'int'               => ['type' => 'long', 'include_in_all' => false],
        'int-enum'          => ['type' => 'integer', 'include_in_all' => false],
        'medium-blob'       => ['type' => 'binary'],
        'medium-int'        => ['type' => 'integer', 'include_in_all' => false],
        'medium-text'       => ['type' => 'text'],
        'message'           => ['type' => 'object'],
        'message-ref'       => [
            'type'       => 'object',
            'properties' => [
                'curie' => ['type' => 'keyword', 'include_in_all' => false],
                'id'    => ['type' => 'keyword', 'include_in_all' => false],
                'tag'   => ['type' => 'keyword', 'include_in_all' => false],
            ],
        ],
        'microtime'         => ['type' => 'long', 'include_in_all' => false],
        'signed-big-int'    => ['type' => 'long', 'include_in_all' => false],
        'signed-int'        => ['type' => 'integer', 'include_in_all' => false],
        'signed-medium-int' => ['type' => 'integer', 'include_in_all' => false],
        'signed-small-int'  => ['type' => 'short', 'include_in_all' => false],
        'signed-tiny-int'   => ['type' => 'byte', 'include_in_all' => false],
        'small-int'         => ['type' => 'integer', 'include_in_all' => false],
        'string'            => ['type' => 'text'],
        'string-enum'       => ['type' => 'keyword', 'include_in_all' => false],
        'text'              => ['type' => 'text'],
        'time-uuid'         => ['type' => 'keyword', 'include_in_all' => false],
        'timestamp'         => ['type' => 'date', 'include_in_all' => false],
        'tiny-int'          => ['type' => 'short', 'include_in_all' => false],
        'trinary'           => ['type' => 'byte', 'include_in_all' => false],
        'uuid'              => ['type' => 'keyword', 'include_in_all' => false],
    ];

    /**
     * Returns the custom analyzers that an index will need to when indexing some
     * pbj fields/types when certain options are used (urls, hashtag format, etc.)
     *
     * @link http://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html
     *
     * @return array
     */
    public static function getCustomAnalyzers()
    {
        return [
            'pbj_keyword' => [
                'tokenizer' => 'keyword',
                'filter'    => 'lowercase',
            ],
        ];
    }

    /**
     * Returns the custom normalizers that an index will need to when indexing some
     * pbj fields/types when certain options are used (urls, hashtag format, etc.)
     *
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-normalizers.html
     *
     * @return array
     */
    public static function getCustomNormalizers()
    {
        return [
            'pbj_keyword' => [
                'type'        => 'custom',
                'char_filter' => [],
                'filter'      => ['lowercase', 'asciifolding'],
            ],
        ];
    }

    /**
     * @param Schema $schema
     * @param string $defaultAnalyzer
     *
     * @return Mapping
     */
    public function create(Schema $schema, $defaultAnalyzer = null)
    {
        $this->defaultAnalyzer = $defaultAnalyzer;
        $rootObject = new \stdClass();
        $rootObject->dynamic_templates = [];
        $mapping = new Mapping(null, $this->mapSchema($schema, $rootObject));
        foreach (get_object_vars($rootObject) as $k => $v) {
            if (!empty($v)) {
                $mapping->setParam($k, $v);
            }
        }
        return $mapping;
    }

    /**
     * @param Schema    $schema
     * @param \stdClass $rootObject
     * @param string    $path
     *
     * @return array
     */
    protected function mapSchema(Schema $schema, \stdClass $rootObject, $path = null)
    {
        $map = [];

        foreach ($schema->getFields() as $field) {
            $fieldName = $field->getName();
            $type = $field->getType();
            $fieldPath = empty($path) ? $fieldName : $path . '.' . $fieldName;

            if ($fieldName === Schema::PBJ_FIELD_NAME) {
                $map[$fieldName] = ['type' => 'keyword', 'include_in_all' => false];
                continue;
            }

            $method = 'map' . ucfirst(StringUtils::toCamelFromSlug($type->getTypeValue()));

            if ($field->isAMap()) {
                $templateName = str_replace('-', '_', SlugUtils::create($fieldPath . '-template'));
                if (is_callable([$this, $method])) {
                    $rootObject->dynamic_templates[] = [
                        $templateName => [
                            'path_match' => $fieldPath . '.*',
                            'mapping'    => $this->$method($field, $rootObject, $fieldPath),
                        ],
                    ];
                } else {
                    $rootObject->dynamic_templates[] = [
                        $templateName => [
                            'path_match' => $fieldPath . '.*',
                            'mapping'    => $this->applyAnalyzer(
                                $this->types[$type->getTypeValue()],
                                $field,
                                $rootObject,
                                $path
                            ),
                        ],
                    ];
                }
            } else {
                if (is_callable([$this, $method])) {
                    $map[$fieldName] = $this->$method($field, $rootObject, $fieldPath);
                } else {
                    $map[$fieldName] = $this->applyAnalyzer(
                        $this->types[$type->getTypeValue()],
                        $field,
                        $rootObject,
                        $path
                    );
                }
            }
        }

        return $map;
    }

    /**
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
     *
     * @param Field     $field
     * @param \stdClass $rootObject
     * @param string    $path
     *
     * @return array
     */
    protected function mapMessage(Field $field, \stdClass $rootObject, $path = null)
    {
        /** @var Message $class */
        $class = null;
        $anyOfClassNames = $field->getAnyOfClassNames();

        if (!empty($anyOfClassNames) && count($anyOfClassNames) === 1) {
            $class = current($anyOfClassNames);
            if (!class_exists($class)) {
                /*
                 * gdbots/pbjc compiler generates an interface and a concrete class with
                 * a V# suffix.  v1 would of course generally exist so we have a good chance
                 * of finding a class and thus a schema using this strategy.  we will however
                 * need to get fancier as versions increase and when mixins are used.
                 *
                 * fixme: address mapping messages that use a mixin as the anyOf
                 */
                $class = "{$class}V1";
            }
        }

        if (!empty($class) && class_exists($class)) {
            $schema = $class::schema();
            return [
                'type'       => $field->isAList() ? 'nested' : 'object',
                'properties' => $this->mapSchema($schema, $rootObject, $path),
            ];
        }

        return [
            'type'       => $field->isAList() ? 'nested' : 'object',
            'properties' => [
                Schema::PBJ_FIELD_NAME => [
                    'type'           => 'keyword',
                    'include_in_all' => false,
                ],
            ],
        ];
    }

    /**
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
     *
     * @param Field     $field
     * @param \stdClass $rootObject
     * @param string    $path
     *
     * @return array
     */
    protected function mapDynamicField(Field $field, \stdClass $rootObject, $path = null)
    {
        $mapping = $this->types[$field->getType()->getTypeValue()];

        if ($field->isAList()) {
            $mapping['type'] = 'nested';
        }

        $mapping['properties']['string_val'] = $this->applyAnalyzer(
            $mapping['properties']['string_val'], $field, $rootObject, $path
        );

        $mapping['properties']['text_val'] = $this->applyAnalyzer(
            $mapping['properties']['text_val'], $field, $rootObject, $path
        );

        return $mapping;
    }

    /**
     * @param Field     $field
     * @param \stdClass $rootObject
     * @param string    $path
     *
     * @return array
     */
    protected function mapString(Field $field, \stdClass $rootObject, $path = null)
    {
        return $this->mapUsingFormat($field, $rootObject, $path);
    }

    /**
     * @param Field     $field
     * @param \stdClass $rootObject
     * @param string    $path
     *
     * @return array
     */
    protected function mapText(Field $field, \stdClass $rootObject, $path = null)
    {
        return $this->mapUsingFormat($field, $rootObject, $path);
    }

    /**
     * @param Field     $field
     * @param \stdClass $rootObject
     * @param string    $path
     *
     * @return array
     */
    protected function mapUsingFormat(Field $field, \stdClass $rootObject, $path = null)
    {
        switch ($field->getFormat()->getValue()) {
            case Format::DATE:
            case Format::DATE_TIME:
                return $this->types['date-time'];

            /**
             * String fields with these formats should use "pbj_keyword" (or something similar)
             * so searches on these fields are not case sensitive.
             *
             * @link http://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html
             * @link http://stackoverflow.com/questions/15079064/how-to-setup-a-tokenizer-in-elasticsearch
             */
            case Format::EMAIL:
            case Format::HASHTAG:
            case Format::HOSTNAME:
            case Format::SLUG:
            case Format::URI:
            case Format::URL:
            case Format::UUID:
                return ['type' => 'keyword', 'normalizer' => 'pbj_keyword', 'include_in_all' => false];

            case Format::IPV4:
            case Format::IPV6:
                return ['type' => 'ip', 'include_in_all' => false];

            default:
                if ($field->getPattern()) {
                    return ['type' => 'keyword', 'normalizer' => 'pbj_keyword', 'include_in_all' => false];
                }

                return $this->applyAnalyzer(['type' => 'text'], $field, $rootObject, $path);
        }
    }

    /**
     * Modify the analyzer for a property prior to adding it to the document mapping.
     * This is only applied to "text" types.
     *
     * @param array     $mapping
     * @param Field     $field
     * @param \stdClass $rootObject
     * @param null      $path
     *
     * @return array
     */
    protected function applyAnalyzer(array $mapping, Field $field, \stdClass $rootObject, $path = null)
    {
        if (null === $this->defaultAnalyzer) {
            return $mapping;
        }

        if (!isset($mapping['type']) || 'text' !== $mapping['type']) {
            return $mapping;
        }

        if (isset($mapping['index']) && false === $mapping['index']) {
            return $mapping;
        }

        if (isset($mapping['analyzer'])) {
            return $mapping;
        }

        $mapping['analyzer'] = $this->defaultAnalyzer;
        return $mapping;
    }
}


<?php

namespace KassaCom\SDK\Transport;

use GuzzleHttp\Psr7;
use KassaCom\SDK\Client;
use KassaCom\SDK\Exception\TransportException;
use KassaCom\SDK\Transport\Authorization\AuthorizationInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;

abstract class AbstractApiTransport implements LoggerAwareInterface
{
    const METHOD_GET = 'GET';
    const METHOD_POST = 'POST';

    /**
     * Domain
     *
     * @var string
     */
    protected $apiUrl = 'https://api.kassa.com/v1';

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

    /**
     * @var AuthorizationInterface
     */
    protected $authorization;

    /**
     * @var string[]
     */
    protected $defaultHeaders;

    /**
     * AbstractApiTransport constructor.
     */
    public function __construct()
    {
        $this->defaultHeaders = [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'User-Agent' => 'Kassa.com PHP SDK v' . Client::VERSION,
            'X-Client-Name' => 'PHP SDK',
            'X-Client-Version' => Client::VERSION,
        ];
    }

    /**
     * @param Psr7\Request $request
     *
     * @return Psr7\Response
     */
    abstract protected function sendRequest(Psr7\Request $request);

    /**
     * @param string $path        Название метода из api
     * @param string $method      GET или POST
     * @param array  $queryParams GET параметры
     * @param mixed  $body
     * @param array  $headers
     *
     * @return Psr7\Response
     * @throws TransportException
     */
    public function send($path, $method, $queryParams = [], $body = null, $headers = [])
    {

        $uri = rtrim($this->apiUrl, '/') . '/' . ltrim($path, '/');

        if (is_array($queryParams) && count($queryParams)) {
            $uri .= '?' . http_build_query($queryParams);
        }

        if (!$this->authorization) {
            throw new TransportException('Please provide authorization data');
        }

        $headers = array_replace($this->defaultHeaders, $headers);

        if ($method == self::METHOD_GET) {
            $body = null;
        }

        if ($this->logger) {
            $this->logger->info('Send request', [
                'method' => $method,
                'uri' => $uri,
                'body' => $body,
                'headers' => $headers,
            ]);
        }

        $headers['Authorization'] = $this->authorization->getAuthorizationHeader();

        $request = new Psr7\Request(
            $method,
            $uri,
            $headers,
            $body
        );

        return $this->sendRequest($request);
    }

    /**
     * @param string $apiUrl
     *
     * @return AbstractApiTransport
     */
    public function setApiUrl($apiUrl)
    {
        $this->apiUrl = $apiUrl;

        return $this;
    }

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param AuthorizationInterface $authorization
     */
    public function setAuth(AuthorizationInterface $authorization)
    {
        $this->authorization = $authorization;
    }
}


<?php

namespace Gdbots\Pbj\Serializer;

use Gdbots\Pbj\Exception\DeserializeMessageFailed;
use Gdbots\Pbj\Message;
use Symfony\Component\Yaml\Yaml;

class YamlSerializer extends PhpArraySerializer
{
    /**
     * {@inheritdoc}
     *
     * Note that the greater the yaml_inline option the slower it is.
     * 3 provides really good human readability but if you need
     * speed use 0 or 1.
     */
    public function serialize(Message $message, array $options = [])
    {
        if (!isset($options['yaml_inline'])) {
            $options['yaml_inline'] = 3;
        }

        if (!isset($options['yaml_indent'])) {
            $options['yaml_indent'] = 2;
        }

        return Yaml::dump(
            parent::serialize($message, $options),
            (int) $options['yaml_inline'],
            (int) $options['yaml_indent']
        );
    }

    /**
     * {@inheritdoc}
     *
     * @return Message
     */
    public function deserialize($data, array $options = [])
    {
        if (!is_array($data)) {
            try {
                $data = Yaml::parse($data);
            } catch (\Exception $e) {
                throw new DeserializeMessageFailed($e->getMessage(), 0, $e);
            }
        }

        return parent::deserialize($data, $options);
    }
}


<?php

namespace Gdbots\Pbj\Marshaler\Elastica;

use Elastica\Document;
use Gdbots\Common\Util\ArrayUtils;
use Gdbots\Pbj\Assertion;
use Gdbots\Pbj\Codec;
use Gdbots\Pbj\Enum\FieldRule;
use Gdbots\Pbj\Exception\GdbotsPbjException;
use Gdbots\Pbj\Exception\InvalidResolvedSchema;
use Gdbots\Pbj\Field;
use Gdbots\Pbj\Message;
use Gdbots\Pbj\MessageRef;
use Gdbots\Pbj\MessageResolver;
use Gdbots\Pbj\Schema;
use Gdbots\Pbj\SchemaId;
use Gdbots\Pbj\WellKnown\DynamicField;
use Gdbots\Pbj\WellKnown\GeoPoint;

final class DocumentMarshaler implements Codec
{
    /**
     * @param Message  $message
     * @param Document $document
     *
     * @return Document
     *
     * @throws \Exception
     * @throws GdbotsPbjException
     */
    public function marshal(Message $message, Document $document = null)
    {
        $document = $document ?: new Document();
        return $document->setData($this->doMarshal($message));
    }

    /**
     * @param Document|array $documentOrSource Document object or source array
     *
     * @return Message
     *
     * @throws \Exception
     * @throws GdbotsPbjException
     */
    public function unmarshal($documentOrSource)
    {
        if ($documentOrSource instanceof Document) {
            return $this->doUnmarshal($documentOrSource->getData());
        }

        return $this->doUnmarshal($documentOrSource);
    }

    /**
     * @param Message $message
     * @param Field   $field
     *
     * @return mixed
     */
    public function encodeMessage(Message $message, Field $field)
    {
        return $this->doMarshal($message);
    }

    /**
     * @param mixed $value
     * @param Field $field
     *
     * @return Message
     */
    public function decodeMessage($value, Field $field)
    {
        return $this->doUnmarshal($value);
    }

    /**
     * @param MessageRef $messageRef
     * @param Field      $field
     *
     * @return mixed
     */
    public function encodeMessageRef(MessageRef $messageRef, Field $field)
    {
        return $messageRef->toArray();
    }

    /**
     * @param mixed $value
     * @param Field $field
     *
     * @return MessageRef
     */
    public function decodeMessageRef($value, Field $field)
    {
        return MessageRef::fromArray($value);
    }

    /**
     * @param GeoPoint $geoPoint
     * @param Field    $field
     *
     * @return mixed
     */
    public function encodeGeoPoint(GeoPoint $geoPoint, Field $field)
    {
        return [$geoPoint->getLongitude(), $geoPoint->getLatitude()];
    }

    /**
     * @param mixed $value
     * @param Field $field
     *
     * @return GeoPoint
     */
    public function decodeGeoPoint($value, Field $field)
    {
        return new GeoPoint($value[1], $value[0]);
    }

    /**
     * @param DynamicField $dynamicField
     * @param Field        $field
     *
     * @return mixed
     */
    public function encodeDynamicField(DynamicField $dynamicField, Field $field)
    {
        return $dynamicField->toArray();
    }

    /**
     * @param mixed $value
     * @param Field $field
     *
     * @return DynamicField
     */
    public function decodeDynamicField($value, Field $field)
    {
        return DynamicField::fromArray($value);
    }

    /**
     * @param Message $message
     *
     * @return array
     */
    private function doMarshal(Message $message)
    {
        $schema = $message::schema();
        $message->validate();

        $payload = [];

        foreach ($schema->getFields() as $field) {
            $fieldName = $field->getName();

            if (!$message->has($fieldName)) {
                /*
                if ($message->hasClearedField($fieldName)) {
                    $payload[$fieldName] = null;
                }
                */

                continue;
            }

            $value = $message->get($fieldName);
            $type = $field->getType();

            switch ($field->getRule()->getValue()) {
                case FieldRule::A_SINGLE_VALUE:
                    $payload[$fieldName] = $type->encode($value, $field, $this);
                    break;

                case FieldRule::A_SET:
                case FieldRule::A_LIST:
                    $payload[$fieldName] = [];
                    foreach ($value as $v) {
                        $payload[$fieldName][] = $type->encode($v, $field, $this);
                    }
                    break;

                case FieldRule::A_MAP:
                    $payload[$fieldName] = [];
                    foreach ($value as $k => $v) {
                        $payload[$fieldName][$k] = $type->encode($v, $field, $this);
                    }
                    break;

                default:
                    break;
            }
        }

        return $payload;
    }

    /**
     * @param array $data
     *
     * @return Message
     *
     * @throws \Exception
     * @throws GdbotsPbjException
     */
    private function doUnmarshal(array $data)
    {
        $schemaId = SchemaId::fromString((string)$data[Schema::PBJ_FIELD_NAME]);
        $className = MessageResolver::resolveId($schemaId);

        /** @var Message $message */
        $message = new $className();
        Assertion::isInstanceOf($message, Message::class);

        if ($message::schema()->getCurieMajor() !== $schemaId->getCurieMajor()) {
            throw new InvalidResolvedSchema($message::schema(), $schemaId, $className);
        }

        $schema = $message::schema();

        foreach ($data as $fieldName => $value) {
            if (!$schema->hasField($fieldName)) {
                continue;
            }

            if (null === $value) {
                $message->clear($fieldName);
                continue;
            }

            $field = $schema->getField($fieldName);
            $type = $field->getType();

            switch ($field->getRule()->getValue()) {
                case FieldRule::A_SINGLE_VALUE:
                    $message->set($fieldName, $type->decode($value, $field, $this));
                    break;

                case FieldRule::A_SET:
                case FieldRule::A_LIST:
                    Assertion::isArray($value, sprintf('Field [%s] must be an array.', $fieldName), $fieldName);
                    $values = [];
                    foreach ($value as $v) {
                        $values[] = $type->decode($v, $field, $this);
                    }

                    if ($field->isASet()) {
                        $message->addToSet($fieldName, $values);
                    } else {
                        $message->addToList($fieldName, $values);
                    }
                    break;

                case FieldRule::A_MAP:
                    Assertion::true(
                        ArrayUtils::isAssoc($value),
                        sprintf('Field [%s] must be an associative array.', $fieldName),
                        $fieldName
                    );
                    foreach ($value as $k => $v) {
                        $message->addToMap($fieldName, $k, $type->decode($v, $field, $this));
                    }
                    break;

                default:
                    break;
            }
        }

        return $message->set(Schema::PBJ_FIELD_NAME, $schema->getId()->toString())->populateDefaults();
    }
}