0 follower

Final Class Yiisoft\Validator\Helper\ObjectParser

InheritanceYiisoft\Validator\Helper\ObjectParser

A helper class used to parse rules from PHP attributes (attached to class properties and class itself) and data from object properties. The attributes introduced in PHP 8 simplify rules' configuration process, especially for nested data and relations. This way the validated structures can be presented as DTO classes with references to each other.

An example of parsed object with both one-to-one (requires PHP > 8.0) and one-to-many (requires PHP > 8.1) relations:

final class Post
{
    #[Length(max: 255)]
    public string $title = '';

    #[Nested]
    public Author|null $author = null;

    // Passing instances is available only since PHP 8.1.
    #[Each(new Nested(File::class))]
    public array $files = [];

    public function __construct()
    {
        $this->author = new Author();
    }
}

final class Author
{
    #[Length(min: 1)]
    public string $name = '';
}

// Some rules, like "Nested" can be also configured through the class attribute.

#[Nested(['url' => new Url()])]
final class File
{
    public string $url = '';
}

$post = new Post(title: 'Yii3 Overview 3', author: 'Dmitriy');
$parser = new ObjectParser($post);
$rules = $parser->getRules();
$data = $parser->getData();

The parsed $rules will contain:

[
    new Nested([
        'title' => [new Length(max: 255)],
        'author' => new Nested([
            'name' => [new Length(min: 1)],
        ]),
        'files' => new Each([
            new Nested([
                'url' => [new Url()],
            ]),
        ]),
    ]);
];

And the result of $data will be:

[
    'title' => 'Yii3 Overview 3',
    'author' => 'John',
    'files' => [],
];

A class name string is valid as a source too. This way only rules will be parsed:

$parser = new ObjectParser(Post::class);
$rules = $parser->getRules(); // The result is the same as in previous example.
$data = $parser->getData(); // Returns empty array.

Please refer to the guide for more examples.

Note that the rule attributes can be combined with others without affecting parsing. Which properties to parse can be configured via {@see \Yiisoft\Validator\Helper\ObjectParser::$propertyVisibility} and {@see \Yiisoft\Validator\Helper\ObjectParser::$skipStaticProperties} options.

Uses Reflection for getting object data and metadata. Supports caching for Reflection of a class / an obhect with properties and rules which can be disabled on demand.

Psalm Types

Name Value
RulesCacheItem array{0: Yiisoft\Validator\RuleInterface, 1: \Attribute::TARGET_*}

Public Methods

Hide inherited methods

Method Description Defined By
__construct() Yiisoft\Validator\Helper\ObjectParser
getData() Returns the parsed object's data as a whole in a form of associative array. Yiisoft\Validator\Helper\ObjectParser
getLabels() Parses labels specified via {@see Label} attributes attached to class properties. Yiisoft\Validator\Helper\ObjectParser
getPropertyTranslator() An optional property names translator. It's taken from the {@see $source} object when {@see PropertyTranslatorProviderInterface} is implemented. In case of it's missing or {@see $source} being a class name string, a null value is returned. Yiisoft\Validator\Helper\ObjectParser
getPropertyValue() Returns a property value of the parsed object. Yiisoft\Validator\Helper\ObjectParser
getReflectionProperties() Returns Reflection properties parsed from {@see $source} in accordance with {@see $propertyVisibility} and {@see $skipStaticProperties} values. Repetitive calls utilize cache if it's enabled in {@see $useCache}. Yiisoft\Validator\Helper\ObjectParser
getRules() Parses rules specified via attributes attached to class properties and class itself. Repetitive calls utilize cache if it's enabled in {@see $useCache}. Yiisoft\Validator\Helper\ObjectParser
hasProperty() Whether the parsed object has the property with a given name. Note that this means existence only and properties with empty values are treated as present too. Yiisoft\Validator\Helper\ObjectParser

Method Details

Hide inherited methods

__construct() public method

public mixed __construct ( string|object $source, integer $propertyVisibility ReflectionProperty::IS_PRIVATE ReflectionProperty::IS_PROTECTED ReflectionProperty::IS_PUBLIC, boolean $skipStaticProperties false, boolean $useCache true )
$source string|object
$propertyVisibility integer
$skipStaticProperties boolean
$useCache boolean
throws InvalidArgumentException

If a class name string provided in {@see $source} refers to a non-existing class.

                public function __construct(
    /**
     * @var object|string A source for parsing rules and data. Can be either a class name string or an
     * instance.
     *
     * @psalm-var class-string|object
     */
    private readonly string|object $source,
    /**
     * @var int Visibility levels the parsed properties must have. For example: public and protected only, this
     * means that the rest (private ones) will be skipped. Defaults to all visibility levels (public, protected and
     * private).
     *
     * @psalm-var int-mask-of<ReflectionProperty::IS_*>
     */
    private readonly int $propertyVisibility = ReflectionProperty::IS_PRIVATE
    | ReflectionProperty::IS_PROTECTED
    | ReflectionProperty::IS_PUBLIC,
    /**
     * @var bool Whether the properties with "static" modifier must be skipped.
     */
    private readonly bool $skipStaticProperties = false,
    /**
     * @var bool Whether some results of parsing (Reflection of a class / an object with properties and rules) must
     * be cached.
     */
    bool $useCache = true,
) {
    /** @var object|string $source */
    if (is_string($source) && !class_exists($source)) {
        throw new InvalidArgumentException(
            sprintf('Class "%s" not found.', $source),
        );
    }
    if ($useCache) {
        $this->cacheKey = (is_object($source) ? $source::class : $source)
            . '_' . $this->propertyVisibility
            . '_' . (int) $this->skipStaticProperties;
    }
}

            
getData() public method

Returns the parsed object's data as a whole in a form of associative array.

If a {@see $source} is a class name string, an empty array is always returned.

public array getData ( )
return array

A mapping between property names and their values.

                public function getData(): array
{
    if (!is_object($this->source)) {
        return [];
    }
    $data = [];
    foreach ($this->getReflectionProperties() as $name => $property) {
        if (!$property->isInitialized($this->source)) {
            continue;
        }
        /** @var mixed */
        $data[$name] = $property->getValue($this->source);
    }
    return $data;
}

            
getLabels() public method

Parses labels specified via {@see Label} attributes attached to class properties.

public array<string, string> getLabels ( )

                public function getLabels(): array
{
    if ($this->hasCacheItem('labels')) {
        /** @var array<string, string> */
        return $this->getCacheItem('labels');
    }
    $labels = [];
    foreach ($this->getReflectionProperties() as $property) {
        $attributes = $property->getAttributes(Label::class, ReflectionAttribute::IS_INSTANCEOF);
        foreach ($attributes as $attribute) {
            /** @var Label $instance */
            $instance = $attribute->newInstance();
            $labels[$property->getName()] = $instance->getLabel();
        }
    }
    $this->setCacheItem('labels', $labels);
    return $labels;
}

            
getPropertyTranslator() public method

An optional property names translator. It's taken from the {@see $source} object when {@see PropertyTranslatorProviderInterface} is implemented. In case of it's missing or {@see $source} being a class name string, a null value is returned.

public Yiisoft\Validator\PropertyTranslatorInterface|null getPropertyTranslator ( )
return Yiisoft\Validator\PropertyTranslatorInterface|null

A property translator instance or `null if it was not provided.

                public function getPropertyTranslator(): ?PropertyTranslatorInterface
{
    return $this->source instanceof PropertyTranslatorProviderInterface
        ? $this->source->getPropertyTranslator()
        : null;
}

            
getPropertyValue() public method

Returns a property value of the parsed object.

Note that in case of non-existing property a default null value is returned. If you need to check the presence of a property or return a different default value, use {@see \Yiisoft\Validator\Helper\hasProperty()} instead.

If a {@see $source} is a class name string, null value is always returned.

public mixed getPropertyValue ( string $property )
$property string

Property name.

return mixed

Property value.

                public function getPropertyValue(string $property): mixed
{
    if (!is_object($this->source)) {
        return null;
    }
    $reflectionProperty = $this->getReflectionProperties()[$property] ?? null;
    if ($reflectionProperty === null || !$reflectionProperty->isInitialized($this->source)) {
        return null;
    }
    return $reflectionProperty->getValue($this->source);
}

            
getReflectionProperties() public method

Returns Reflection properties parsed from {@see $source} in accordance with {@see $propertyVisibility} and {@see $skipStaticProperties} values. Repetitive calls utilize cache if it's enabled in {@see $useCache}.

See also https://github.com/yiisoft/form for usage in form collector.

public array<string, ReflectionProperty> getReflectionProperties ( )
return array<string, ReflectionProperty>

A mapping between Reflection property names and their values.

                public function getReflectionProperties(): array
{
    if ($this->hasCacheItem('reflectionProperties')) {
        /** @var array<string, ReflectionProperty> */
        return $this->getCacheItem('reflectionProperties');
    }
    $reflectionProperties = [];
    foreach ($this->getReflectionSource()->getProperties($this->propertyVisibility) as $property) {
        if ($this->skipStaticProperties && $property->isStatic()) {
            continue;
        }
        $reflectionProperties[$property->getName()] = $property;
    }
    $this->setCacheItem('reflectionProperties', $reflectionProperties);
    return $reflectionProperties;
}

            
getRules() public method

Parses rules specified via attributes attached to class properties and class itself. Repetitive calls utilize cache if it's enabled in {@see $useCache}.

public array<integer, Yiisoft\Validator\RuleInterface>|array<string, list<Yiisoft\Validator\RuleInterface>> getRules ( )
return array<integer, Yiisoft\Validator\RuleInterface>|array<string, list<Yiisoft\Validator\RuleInterface>>

The resulting rules array with the following structure:

[
    [new FilledAtLeast(['name', 'author'])], // Parsed from class attribute.
    'files' => [new Count(max: 3)], // Parsed from property attribute.
],

                public function getRules(): array
{
    if ($this->hasCacheItem('rules')) {
        /** @var array $rules */
        $rules = $this->getCacheItem('rules');
        return $this->prepareRules($rules);
    }
    $rules = [];
    // Class rules
    $attributes = $this
        ->getReflectionSource()
        ->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
    foreach ($attributes as $attribute) {
        $rules[] = [$attribute->newInstance(), Attribute::TARGET_CLASS];
    }
    // Properties rules
    foreach ($this->getReflectionProperties() as $property) {
        // TODO: use Generator to collect attributes.
        $attributes = $property->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
        foreach ($attributes as $attribute) {
            /** @psalm-suppress UndefinedInterfaceMethod */
            $rules[$property->getName()][] = [$attribute->newInstance(), Attribute::TARGET_PROPERTY];
        }
    }
    $this->setCacheItem('rules', $rules);
    return $this->prepareRules($rules);
}

            
hasProperty() public method

Whether the parsed object has the property with a given name. Note that this means existence only and properties with empty values are treated as present too.

If a {@see $source} is a class name string, false value is always returned.

public boolean hasProperty ( string $property )
$property string
return boolean

Whether the property exists: true - exists and false - otherwise.

                public function hasProperty(string $property): bool
{
    return is_object($this->source) && array_key_exists($property, $this->getReflectionProperties());
}