honeybee/trellis

View on GitHub
src/Runtime/Attribute/Attribute.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace Trellis\Runtime\Attribute;

use Trellis\Common\BaseObject;
use Trellis\Common\Error\BadValueException;
use Trellis\Common\Error\InvalidTypeException;
use Trellis\Common\Error\InvalidConfigException;
use Trellis\Runtime\EntityTypeInterface;
use Trellis\Runtime\Validator\Rule\RuleList;
use Trellis\Runtime\Validator\Validator;
use Trellis\Runtime\Validator\ValidatorInterface;
use Trellis\Runtime\Validator\Result\IncidentInterface;
use Trellis\Runtime\ValueHolder\ValueHolderInterface;

/**
 * Base class that all Trellis AttributeInterface implementations should extend.
 * Provides a pretty complete implementation for the interface, concrete
 * attributes should at least implement buildValidationRules in order to
 * provide validation rules that sanitize the given attribute value.
 *
 * basic options: 'validator', 'value', 'default_value', 'null_value', 'mandatory'
 * @todo extends BaseObject; which introduces a breaking change to the create method.
 * TODO introduce 'mandatory' option
 */
abstract class Attribute extends BaseObject implements AttributeInterface
{
    /**
     * Holds a reference to the attribute's type.
     *
     * @hiddenProperty
     * @var EntityTypeInterface $type;
     */
    protected $type;

    /**
     * Holds a reference to the parent attribute, if there is one.
     *
     * @var AttributeInterface|null $parent;
     */
    protected $parent;

    /**
     * Holds the attribute's name.
     *
     * @var string $name
     */
    protected $name;

    /**
     * Holds the attribute's options.
     *
     * @var array $options
     */
    protected $options = [];

    /**
     * Holds the attribute's validator instance.
     *
     * @var ValidatorInterface $validator
     */
    protected $validator;

    /**
     * @var string fully qualified class name implementing ValueHolderInterface
     */
    protected $value_holder_implementor;

    /**
     * Constructs a new attribute instance.
     *
     * @param string $name
     * @param EntityTypeInterface $type
     * @param array $options
     * @param AttributeInterface $parent
     */
    public function __construct(
        $name,
        EntityTypeInterface $type,
        array $options = [],
        AttributeInterface $parent = null
    ) {
        parent::__construct([]);
        $this->name = $name;
        $this->type = $type;
        $this->options = $options;
        $this->parent = $parent;
    }

    /**
     * Returns the name of the attribute.
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Returns the attribute's entity type.
     *
     * @return EntityTypeInterface
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Returns the attribute's parent, if it has one.
     *
     * @return AttributeInterface|null
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * Returns attribute path of this attribute. Depending on this attribute
     * being part of an embedded entity this may look like this format:
     * {attribute_name}.{type_prefix}.{attribute_name}
     *
     * @return string attribute path of this attribute
     */
    public function getPath()
    {
        return AttributePath::getAttributePath($this);
    }

    /**
     * Returns the attribute's top-most entity type. That is the entity type
     * of the root attribute of this attribute.
     *
     * @return EntityTypeInterface
     */
    public function getRootType()
    {
        return AttributePath::getRootEntityType($this);
    }

    /**
     * Returns the default value of the attribute.
     *
     * @return mixed value to be used/interpreted as the default value
     */
    public function getDefaultValue()
    {
        if ($this->hasOption(self::OPTION_DEFAULT_VALUE)) {
            return $this->getSanitizedValue(
                $this->getOption(self::OPTION_DEFAULT_VALUE, $this->getNullValue())
            );
        }

        return $this->getNullValue();
    }

    /**
     * Returns the attribute's null value.
     *
     * @return mixed value to be used/interpreted as null (not set)
     */
    public function getNullValue()
    {
        return null;
    }

    /**
     * Returns the ValidatorInterface implementation to use when validating values for this attribute.
     * Override this method if you want inject your own implementation.
     *
     * @return ValidatorInterface implementation
     */
    public function getValidator()
    {
        if (!$this->validator) {
            $default_validator_class = Validator::CLASS;
            $validator_implementor = $this->getOption(self::OPTION_VALIDATOR, $default_validator_class);

            if (!class_exists($validator_implementor, true)) {
                throw new InvalidConfigException(
                    sprintf(
                        "Unable to resolve validator implementor '%s' given for attribute '%s' on entity type '%s'.",
                        $validator_implementor,
                        $this->getName(),
                        $this->getType()->getName()
                    )
                );
            }

            $validator = new $validator_implementor($this->getName(), $this->buildValidationRules());
            if (!$validator instanceof ValidatorInterface) {
                throw new InvalidTypeException(
                    sprintf(
                        "Invalid validator implementor '%s' given for attribute '%s' on entity type '%s'. " .
                        "Make sure to implement '%s'.",
                        $validator_implementor,
                        $this->getName(),
                        $this->getType() ? $this->getType()->getName() : 'undefined',
                        ValidatorInterface::CLASS
                    )
                );
            }
            $this->validator = $validator;
        }

        return $this->validator;
    }

    /**
     * Creates a ValueHolderInterface, that is specific to the current attribute instance.
     *
     * @return ValueHolderInterface
     */
    public function createValueHolder($apply_default_values = false)
    {
        if (!$this->value_holder_implementor) {
            $implementor = $this->hasOption(self::OPTION_VALUE_HOLDER)
                ? $this->getOption(self::OPTION_VALUE_HOLDER)
                : $this->buildDefaultValueHolderClassName();

            if (!class_exists($implementor)) {
                throw new InvalidConfigException(
                    sprintf(
                        "Invalid valueholder implementor '%s' configured for attribute '%s' on entity '%s'.",
                        $implementor,
                        $this->getName(),
                        $this->getType() ? $this->getType()->getName() : 'undefined'
                    )
                );
            }

            $test_value_holder = new $implementor($this);

            if (!$test_value_holder instanceof ValueHolderInterface) {
                throw new InvalidTypeException(
                    sprintf(
                        "Invalid valueholder implementation '%s' given for attribute '%s' on entity type '%s'. " .
                        "Make sure to implement '%s'.",
                        $implementor,
                        $this->getName(),
                        $this->getType() ? $this->getType()->getName() : 'undefined',
                        ValueHolderInterface::CLASS
                    )
                );
            }

            $this->value_holder_implementor = $implementor;
        }

        $value_holder = new $this->value_holder_implementor($this);
        if ($apply_default_values === true) {
            $value_holder->setValue($this->getDefaultValue());
        } elseif ($apply_default_values === false) {
            $value_holder->setValue($this->getNullValue());
        } else {
            throw new InvalidTypeException(
                sprintf(
                    "Only boolean arguments are acceptable for attribute '%s' on entity type '%s'. ",
                    $this->getName(),
                    $this->getType() ? $this->getType()->getName() : 'undefined'
                )
            );
        }

        return $value_holder;
    }

    /**
     * Returns the attribute's options.
     *
     * @return array
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Returns a attribute option by name if it exists.
     * Otherwise an optional default is returned.
     *
     * @param string $name
     * @param mixed $default
     *
     * @return mixed
     */
    public function getOption($name, $default = null)
    {
        return $this->hasOption($name) ? $this->options[$name] : $default;
    }

    /**
     * Tells if a attribute currently owns a specific option.
     *
     * @param string $name
     *
     * @return boolean
     */
    public function hasOption($name)
    {
        return array_key_exists($name, $this->options);
    }

    /**
     * Build a list of rules used by the attribute's validator to validate values for this attribute.
     *
     * @return RuleList
     */
    protected function buildValidationRules()
    {
        return new RuleList;
    }

    /**
     * @param mixed $value value to be assigned somewhere
     *
     * @return mixed sanitized version of the given value
     *
     * @throws BadValueException on invalid input value according to validation
     */
    protected function getSanitizedValue($value)
    {
        $validation_result = $this->getValidator()->validate($value);

        if ($validation_result->getSeverity() > IncidentInterface::NOTICE) {
            throw new BadValueException(
                sprintf(
                    "Given value for attribute '%s' on entity type '%s' is not valid.",
                    $this->getName(),
                    $this->getType() ? $this->getType()->getName() : 'undefined'
                )
            );
        }

        return $validation_result->getSanitizedValue();
    }

    /**
     * Returns the ValueHolderInterface implementation to use when aggregating (value)data for this attribute.
     * Override this method if you want inject your own implementation.
     *
     * @return string Fully qualified name of an ValueHolderInterface implementation.
     */
    protected function buildDefaultValueHolderClassName()
    {
        return preg_replace('#Attribute$#', 'ValueHolder', get_class($this));
    }
}