Skip to content

Commit

Permalink
Merge pull request #961 from msmakouz/feature/filters-improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
spiralbot committed Aug 14, 2023
1 parent 56e002c commit 3a9c0c1
Show file tree
Hide file tree
Showing 22 changed files with 501 additions and 29 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -40,7 +40,8 @@
"mockery/mockery": "^1.5",
"nyholm/psr7": "^1.5",
"phpunit/phpunit": "^10.1",
"vimeo/psalm": "^5.9"
"vimeo/psalm": "^5.9",
"ramsey/uuid": "^4.7"
},
"autoload": {
"psr-4": {
Expand Down
11 changes: 8 additions & 3 deletions src/Attribute/Setter.php
Expand Up @@ -6,6 +6,7 @@

use Attribute;
use Spiral\Attributes\NamedArgumentConstructor;
use Spiral\Filters\Exception\SetterException;

/**
* Use setters to typecast the incoming value before passing it to the property.
Expand All @@ -17,10 +18,10 @@
* #[\Spiral\Filters\Attribute\Setter(filter: [Foo::class, 'bar'])]
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE), NamedArgumentConstructor]
final class Setter
class Setter
{
public readonly \Closure $filter;
private array $args;
protected array $args;

public function __construct(callable $filter, mixed ...$args)
{
Expand All @@ -30,6 +31,10 @@ public function __construct(callable $filter, mixed ...$args)

public function updateValue(mixed $value): mixed
{
return ($this->filter)($value, ...$this->args);
try {
return ($this->filter)($value, ...$this->args);
} catch (\Throwable $e) {
throw new SetterException($e);
}
}
}
13 changes: 13 additions & 0 deletions src/Exception/SetterException.php
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Exception;

class SetterException extends FilterException
{
public function __construct(\Throwable $previous = null)
{
parent::__construct(message: 'Unable to set value. The given data was invalid.', previous: $previous);
}
}
6 changes: 4 additions & 2 deletions src/Model/FilterProvider.php
Expand Up @@ -5,7 +5,6 @@
namespace Spiral\Filters\Model;

use Psr\Container\ContainerInterface;
use Spiral\Core\Container;
use Spiral\Core\CoreInterface;
use Spiral\Core\ResolverInterface;
use Spiral\Filters\Model\Schema\AttributeMapper;
Expand Down Expand Up @@ -33,7 +32,7 @@ public function createFilter(string $name, InputInterface $input): FilterInterfa
\assert($attributeMapper instanceof AttributeMapper);

$filter = $this->createFilterInstance($name);
[$mappingSchema, $errors, $setters] = $attributeMapper->map($filter, $input);
[$mappingSchema, $errors, $setters, $optionalFilters] = $attributeMapper->map($filter, $input);

if ($filter instanceof HasFilterDefinition) {
$mappingSchema = \array_merge(
Expand All @@ -49,6 +48,9 @@ public function createFilter(string $name, InputInterface $input): FilterInterfa
\assert($schemaBuilder instanceof Builder);

$schema = $schemaBuilder->makeSchema($name, $mappingSchema);
foreach ($optionalFilters as $optionalFilter) {
$schema[$optionalFilter][Builder::SCHEMA_OPTIONAL] = true;
}

[$data, $inputErrors] = $inputMapper->map($schema, $input, $setters);
$errors = \array_merge($errors, $inputErrors);
Expand Down
6 changes: 5 additions & 1 deletion src/Model/Interceptor/ValidateFilterInterceptor.php
Expand Up @@ -5,7 +5,6 @@
namespace Spiral\Filters\Model\Interceptor;

use Psr\Container\ContainerInterface;
use Spiral\Core\Container;
use Spiral\Core\CoreInterceptorInterface;
use Spiral\Core\CoreInterface;
use Spiral\Filters\Model\FilterBag;
Expand Down Expand Up @@ -44,6 +43,11 @@ public function process(string $controller, string $action, array $parameters, C
);
}

if (($bag->errors ?? []) !== []) {
$errorMapper = new ErrorMapper($bag->schema);
throw new ValidationException($errorMapper->mapErrors($bag->errors), $parameters['context'] ?? null);
}

return $filter;
}

Expand Down
14 changes: 14 additions & 0 deletions src/Model/Mapper/CasterInterface.php
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

use Spiral\Filters\Model\FilterInterface;

interface CasterInterface
{
public function supports(\ReflectionNamedType $type): bool;

public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void;
}
36 changes: 36 additions & 0 deletions src/Model/Mapper/CasterRegistry.php
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

final class CasterRegistry implements CasterRegistryInterface
{
/** @var array<class-string, CasterInterface> */
private array $casters = [];

public function __construct(array $casters = [])
{
foreach ($casters as $caster) {
$this->register($caster);
}
}

public function register(CasterInterface $caster): void
{
$this->casters[$caster::class] = $caster;
}

/**
* @return array<CasterInterface>
*/
public function getCasters(): array
{
return \array_values($this->casters);
}

public function getDefault(): CasterInterface
{
return new DefaultCaster();
}
}
17 changes: 17 additions & 0 deletions src/Model/Mapper/CasterRegistryInterface.php
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

interface CasterRegistryInterface
{
public function register(CasterInterface $caster): void;

/**
* @return array<CasterInterface>
*/
public function getCasters(): array;

public function getDefault(): CasterInterface;
}
20 changes: 20 additions & 0 deletions src/Model/Mapper/DefaultCaster.php
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

use Spiral\Filters\Model\FilterInterface;

final class DefaultCaster implements CasterInterface
{
public function supports(\ReflectionNamedType $type): bool
{
return true;
}

public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
{
$property->setValue($filter, $value);
}
}
30 changes: 30 additions & 0 deletions src/Model/Mapper/EnumCaster.php
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

use Spiral\Filters\Model\FilterInterface;

final class EnumCaster implements CasterInterface
{
public function supports(\ReflectionNamedType $type): bool
{
return \enum_exists($type->getName());
}

public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
{
/**
* @var \ReflectionNamedType $type
*/
$type = $property->getType();

/**
* @var class-string<\BackedEnum> $enum
*/
$enum = $type->getName();

$property->setValue($filter, $enum::from($value));
}
}
36 changes: 36 additions & 0 deletions src/Model/Mapper/Mapper.php
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

use Spiral\Filters\Model\FilterInterface;

/**
* @internal
*/
final class Mapper
{
public function __construct(
private readonly CasterRegistryInterface $registry
) {
}

/**
* Set input data to the filter property.
*/
public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
{
$type = $property->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
foreach ($this->registry->getCasters() as $setter) {
if ($setter->supports($type)) {
$setter->setValue($filter, $property, $value);
return;
}
}
}

$this->registry->getDefault()->setValue($filter, $property, $value);
}
}
46 changes: 46 additions & 0 deletions src/Model/Mapper/UuidCaster.php
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Model\Mapper;

use Ramsey\Uuid\UuidInterface;
use Spiral\Filters\Model\FilterInterface;

final class UuidCaster implements CasterInterface
{
private ?bool $interfaceExists = null;

public function supports(\ReflectionNamedType $type): bool
{
if ($this->interfaceExists === null) {
$this->interfaceExists = \interface_exists(UuidInterface::class);
}

return $this->interfaceExists && $this->implements($type->getName(), UuidInterface::class);
}

public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
{
$property->setValue($filter, \Ramsey\Uuid\Uuid::fromString($value));
}

private function implements(string $haystack, string $interface): bool
{
if ($haystack === $interface) {
return true;
}

foreach ((array)\class_implements($haystack) as $implements) {
if ($implements === $interface) {
return true;
}

if (self::implements($implements, $interface)) {
return true;
}
}

return false;
}
}

0 comments on commit 3a9c0c1

Please sign in to comment.