Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/validators/Attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
# Attributes

- `Attributes()`
- `Attributes(PropertyResolver $propertyResolver)`

Validates the PHP attributes defined in the properties of the input.

Expand Down
3 changes: 2 additions & 1 deletion src-dev/Commands/LintMixinCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Respect\Validation\Mixins\Chain;
use Respect\Validation\Validator;
use Respect\Validation\ValidatorBuilder;
use Respect\Validation\Validators\Attributes\PropertyResolver;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -75,7 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
scanner: $scanner,
methodBuilder: new MethodBuilder(
excludedTypePrefixes: ['Sokil', 'Egulias'],
excludedTypeNames: ['finfo'],
excludedTypeNames: ['finfo', PropertyResolver::class],
),
interfaces: [
new InterfaceConfig(
Expand Down
85 changes: 33 additions & 52 deletions src/Validators/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@
use Attribute;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionIntersectionType;
use ReflectionNamedType;
use ReflectionObject;
use ReflectionProperty;
use ReflectionUnionType;
use Respect\Fluent\Attributes\Composable;
use Respect\Validation\Id;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Attributes\CompositePropertyResolver;
use Respect\Validation\Validators\Attributes\DeclaredTypePropertyResolver;
use Respect\Validation\Validators\Attributes\ExplicitAttributePropertyResolver;
use Respect\Validation\Validators\Attributes\PropertyResolver;
use Respect\Validation\Validators\Core\Reducer;

use function spl_object_id;
use WeakMap;

#[Composable(without: [All::class, Key::class, Property::class, Not::class, UndefOr::class])]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
Expand All @@ -40,8 +40,20 @@ final class Attributes implements Validator
{
public const string TEMPLATE_CIRCULAR_REFERENCE = '__circular_reference__';

/** @var array<int, true> */
private array $visited = [];
/** @var WeakMap<object, true> */
private WeakMap $visited;

private readonly PropertyResolver $propertyResolver;

public function __construct(
PropertyResolver|null $propertyResolver = null,
) {
$this->propertyResolver = $propertyResolver ?? new CompositePropertyResolver(
new ExplicitAttributePropertyResolver(),
new DeclaredTypePropertyResolver(),
);
$this->visited = new WeakMap();
}

public function evaluate(mixed $input): Result
{
Expand All @@ -51,12 +63,11 @@ public function evaluate(mixed $input): Result
return $objectType->withId($id);
}

$objectId = spl_object_id($input);
if (isset($this->visited[$objectId])) {
if ($this->visited->offsetExists($input)) {
return Result::failed($input, $this, [], self::TEMPLATE_CIRCULAR_REFERENCE)->withId($id);
}

$this->visited[$objectId] = true;
$this->visited[$input] = true;

$reflection = new ReflectionObject($input);
$validators = [...$this->getClassValidators($reflection), ...$this->getPropertyValidators($reflection)];
Expand Down Expand Up @@ -87,7 +98,7 @@ private function getPropertyValidators(ReflectionObject $reflection): array
{
$validators = [];
foreach ($this->getProperties($reflection) as $propertyName => $property) {
$propertyValidators = $this->getPropertyInnerValidators($property);
$propertyValidators = $this->propertyResolver->resolve($property, $this);
if ($propertyValidators === []) {
continue;
}
Expand All @@ -101,47 +112,6 @@ private function getPropertyValidators(ReflectionObject $reflection): array
return $validators;
}

/** @return array<Validator> */
private function getPropertyInnerValidators(ReflectionProperty $property): array
{
$propertyValidators = [];
$hasExplicitAttributes = false;
foreach ($property->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$propertyValidator = $attribute->getName() === self::class ? $this : $attribute->newInstance();
$hasExplicitAttributes = $propertyValidator === $this;
$propertyValidators[] = $propertyValidator;
}

if ($hasExplicitAttributes) {
return $propertyValidators;
}

$type = $property->getType();
if ($type instanceof ReflectionNamedType) {
if (!$type->isBuiltin()) {
$propertyValidators[] = $this;
}
}

if ($type instanceof ReflectionIntersectionType) {
$propertyValidators[] = $this;
}

if ($type instanceof ReflectionUnionType) {
foreach ($type->getTypes() as $innerType) {
if (!$innerType instanceof ReflectionNamedType || $innerType->isBuiltin()) {
continue;
}

/** @var class-string $class */
$class = $innerType->getName();
$propertyValidators[] = new Given(new Instance($class), $this);
}
}

return $propertyValidators;
}

/** @return array<ReflectionProperty> */
private function getProperties(ReflectionObject $reflection): array
{
Expand All @@ -156,4 +126,15 @@ private function getProperties(ReflectionObject $reflection): array

return $properties;
}

/** @return list<string> */
public function __sleep(): array
{
return ['propertyResolver'];
}

public function __wakeup(): void
{
$this->visited = new WeakMap();
}
}
58 changes: 58 additions & 0 deletions src/Validators/Attributes/CompositePropertyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Validators\Attributes;

use ReflectionProperty;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Attributes;

use function array_values;
use function in_array;

final class CompositePropertyResolver implements PropertyResolver
{
/** @var list<PropertyResolver> */
private readonly array $resolvers;

public function __construct(PropertyResolver ...$resolvers)
{
$this->resolvers = array_values($resolvers);
}

/** @return array<Validator> */
public function resolve(ReflectionProperty $property, Attributes $attributes): array
{
$accumulated = [];

foreach ($this->resolvers as $resolver) {
$validators = $resolver->resolve($property, $attributes);
if ($validators === []) {
continue;
}

// When more than one resolver recognizes the same property (e.g. a
// class-typed property annotated with #[Attributes], which both
// DeclaredTypePropertyResolver and ExplicitAttributePropertyResolver
// emit `$attributes` for), collapse duplicate `$attributes` entries
// so the nested Attributes validator runs once instead of tripping
// its circular-reference guard on the second pass.
foreach ($validators as $validator) {
if ($validator === $attributes && in_array($validator, $accumulated, true)) {
continue;
}

$accumulated[] = $validator;
}
}

return $accumulated;
}
}
58 changes: 58 additions & 0 deletions src/Validators/Attributes/DeclaredTypePropertyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Validators\Attributes;

use ReflectionIntersectionType;
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionUnionType;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Attributes;
use Respect\Validation\Validators\Given;
use Respect\Validation\Validators\Instance;

final class DeclaredTypePropertyResolver implements PropertyResolver
{
/** @return array<Validator> */
public function resolve(ReflectionProperty $property, Attributes $attributes): array
{
$type = $property->getType();

if ($type instanceof ReflectionNamedType) {
if ($type->isBuiltin()) {
return [];
}

return [$attributes];
}

if ($type instanceof ReflectionIntersectionType) {
return [$attributes];
}

if ($type instanceof ReflectionUnionType) {
$validators = [];
foreach ($type->getTypes() as $innerType) {
if (!$innerType instanceof ReflectionNamedType || $innerType->isBuiltin()) {
continue;
}

/** @var class-string $class */
$class = $innerType->getName();
$validators[] = new Given(new Instance($class), $attributes);
}

return $validators;
}

return [];
}
}
31 changes: 31 additions & 0 deletions src/Validators/Attributes/ExplicitAttributePropertyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Validators\Attributes;

use ReflectionAttribute;
use ReflectionProperty;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Attributes;

final class ExplicitAttributePropertyResolver implements PropertyResolver
{
/** @return array<Validator> */
public function resolve(ReflectionProperty $property, Attributes $attributes): array
{
$validators = [];
foreach ($property->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$propertyValidator = $attribute->getName() === Attributes::class ? $attributes : $attribute->newInstance();
$validators[] = $propertyValidator;
}

return $validators;
}
}
24 changes: 24 additions & 0 deletions src/Validators/Attributes/PropertyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Validators\Attributes;

use ReflectionProperty;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Attributes;

/**
* Resolves validators from properties.
*/
interface PropertyResolver
{
/** @return array<Validator> */
public function resolve(ReflectionProperty $property, Attributes $attributes): array;
}
35 changes: 35 additions & 0 deletions tests/src/Stubs/StubPropertyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Test\Stubs;

use ReflectionProperty;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Attributes;
use Respect\Validation\Validators\Attributes\PropertyResolver;

use function array_values;

final class StubPropertyResolver implements PropertyResolver
{
/** @var list<Validator> */
private readonly array $validators;

public function __construct(Validator ...$validators)
{
$this->validators = array_values($validators);
}

/** @return list<Validator> */
public function resolve(ReflectionProperty $property, Attributes $attributes): array
{
return $this->validators;
}
}
19 changes: 19 additions & 0 deletions tests/src/Stubs/WithClassTypedProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Test\Stubs;

final class WithClassTypedProperty
{
public function __construct(
public NestedAddress $value,
) {
}
}
Loading
Loading