Skip to content

Commit

Permalink
Merge pull request #1016 from spiral/feature/filter
Browse files Browse the repository at this point in the history
[spiral/filters] Add error handling when performing casting
  • Loading branch information
butschster committed Nov 22, 2023
2 parents c618ac7 + a9d1024 commit e09951d
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 16 deletions.
35 changes: 35 additions & 0 deletions src/Filters/src/Attribute/CastingErrorMessage.php
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Spiral\Filters\Attribute;

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

#[\Attribute(\Attribute::TARGET_PROPERTY), NamedArgumentConstructor]
class CastingErrorMessage
{
protected ?\Closure $callback = null;

/**
* @param callable(SetterException $exception, mixed $value): string $callback
*/
public function __construct(
protected ?string $message = null,
?callable $callback = null
) {
if ($callback !== null) {
$this->callback = $callback(...);
}
}

public function getMessage(SetterException $exception, mixed $value = null): ?string
{
if ($this->callback instanceof \Closure) {
return ($this->callback)($exception, $value);
}

return $this->message;
}
}
7 changes: 5 additions & 2 deletions src/Filters/src/Exception/SetterException.php
Expand Up @@ -6,8 +6,11 @@

class SetterException extends FilterException
{
public function __construct(\Throwable $previous = null)
public function __construct(\Throwable $previous = null, ?string $message = null)
{
parent::__construct(message: 'Unable to set value. The given data was invalid.', previous: $previous);
parent::__construct(
message: $message ?? 'Unable to set value. The given data was invalid.',
previous: $previous,
);
}
}
10 changes: 9 additions & 1 deletion src/Filters/src/Model/Mapper/DefaultCaster.php
Expand Up @@ -4,6 +4,7 @@

namespace Spiral\Filters\Model\Mapper;

use Spiral\Filters\Exception\SetterException;
use Spiral\Filters\Model\FilterInterface;

final class DefaultCaster implements CasterInterface
Expand All @@ -15,6 +16,13 @@ public function supports(\ReflectionNamedType $type): bool

public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
{
$property->setValue($filter, $value);
try {
$property->setValue($filter, $value);
} catch (\Throwable $e) {
throw new SetterException(
previous: $e,
message: \sprintf('Unable to set value. %s', $e->getMessage()),
);
}
}
}
10 changes: 9 additions & 1 deletion src/Filters/src/Model/Mapper/EnumCaster.php
Expand Up @@ -4,6 +4,7 @@

namespace Spiral\Filters\Model\Mapper;

use Spiral\Filters\Exception\SetterException;
use Spiral\Filters\Model\FilterInterface;

final class EnumCaster implements CasterInterface
Expand All @@ -25,6 +26,13 @@ public function setValue(FilterInterface $filter, \ReflectionProperty $property,
*/
$enum = $type->getName();

$property->setValue($filter, $value instanceof $enum ? $value : $enum::from($value));
try {
$property->setValue($filter, $value instanceof $enum ? $value : $enum::from($value));
} catch (\Throwable $e) {
throw new SetterException(
previous: $e,
message: \sprintf('Unable to set enum value. %s', $e->getMessage()),
);
}
}
}
14 changes: 10 additions & 4 deletions src/Filters/src/Model/Mapper/UuidCaster.php
Expand Up @@ -4,7 +4,9 @@

namespace Spiral\Filters\Model\Mapper;

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

final class UuidCaster implements CasterInterface
Expand All @@ -22,10 +24,14 @@ public function supports(\ReflectionNamedType $type): bool

public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
{
$property->setValue(
$filter,
$value instanceof UuidInterface ? $value : \Ramsey\Uuid\Uuid::fromString($value)
);
try {
$property->setValue($filter, $value instanceof UuidInterface ? $value : Uuid::fromString($value));
} catch (\Throwable $e) {
throw new SetterException(
previous: $e,
message: \sprintf('Unable to set UUID value. %s', $e->getMessage()),
);
}
}

private function implements(string $haystack, string $interface): bool
Expand Down
28 changes: 20 additions & 8 deletions src/Filters/src/Model/Schema/AttributeMapper.php
Expand Up @@ -5,6 +5,7 @@
namespace Spiral\Filters\Model\Schema;

use Spiral\Attributes\ReaderInterface;
use Spiral\Filters\Attribute\CastingErrorMessage;
use Spiral\Filters\Attribute\Input\AbstractInput;
use Spiral\Filters\Attribute\NestedArray;
use Spiral\Filters\Attribute\NestedFilter;
Expand Down Expand Up @@ -43,10 +44,11 @@ public function map(FilterInterface $filter, InputInterface $input): array
/** @var object $attribute */
foreach ($this->reader->getPropertyMetadata($property) as $attribute) {
if ($attribute instanceof AbstractInput) {
$value = $attribute->getValue($input, $property);
try {
$this->setValue($filter, $property, $attribute->getValue($input, $property));
$this->setValue($filter, $property, $value);
} catch (SetterException $e) {
$errors[$property->getName()] = $e->getMessage();
$errors[$property->getName()] = $this->createErrorMessage($e, $property, $value);
}
$schema[$property->getName()] = $attribute->getSchema($property);
} elseif ($attribute instanceof NestedFilter) {
Expand All @@ -60,7 +62,7 @@ public function map(FilterInterface $filter, InputInterface $input): array
try {
$this->setValue($filter, $property, $value);
} catch (SetterException $e) {
$errors[$property->getName()] = $e->getMessage();
$errors[$property->getName()] = $this->createErrorMessage($e, $property, $value);
}
} catch (ValidationException $e) {
if ($this->allowsNull($property)) {
Expand Down Expand Up @@ -92,11 +94,7 @@ public function map(FilterInterface $filter, InputInterface $input): array
}
}

try {
$this->setValue($filter, $property, $propertyValues);
} catch (SetterException $e) {
$errors[$property->getName()] = $e->getMessage();
}
$this->setValue($filter, $property, $propertyValues);
$schema[$property->getName()] = [$attribute->class, $prefix . '.*'];
} elseif ($attribute instanceof Setter) {
$setters[$property->getName()][] = $attribute;
Expand Down Expand Up @@ -128,4 +126,18 @@ private function allowsNull(\ReflectionProperty $property): bool

return $type === null || $type->allowsNull();
}

private function createErrorMessage(
SetterException $exception,
\ReflectionProperty $property,
mixed $value = null
): string {
$attribute = $this->reader->firstPropertyMetadata($property, CastingErrorMessage::class);

if ($attribute === null) {
return $exception->getMessage();
}

return $attribute->getMessage($exception, $value) ?? $exception->getMessage();
}
}
16 changes: 16 additions & 0 deletions src/Filters/tests/Mapper/DefaultCasterTest.php
Expand Up @@ -5,6 +5,7 @@
namespace Spiral\Tests\Filters\Mapper;

use PHPUnit\Framework\TestCase;
use Spiral\Filters\Exception\SetterException;
use Spiral\Filters\Model\Mapper\DefaultCaster;
use Spiral\Tests\Filters\Fixtures\AddressFilter;

Expand All @@ -24,4 +25,19 @@ public function testSetValue(): void
$setter->setValue($filter, $property, 'foo');
$this->assertSame('foo', $property->getValue($filter));
}

public function testSetValueException(): void
{
$setter = new DefaultCaster();
$filter = $this->createMock(AddressFilter::class);
$property = new \ReflectionProperty($filter, 'city');

$this->expectException(SetterException::class);
$this->expectExceptionMessage(\sprintf(
'Unable to set value. Cannot assign %s to property %s::$city of type string',
\stdClass::class,
AddressFilter::class
));
$setter->setValue($filter, $property, new \stdClass());
}
}
11 changes: 11 additions & 0 deletions src/Filters/tests/Mapper/EnumCasterTest.php
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Spiral\Filters\Exception\SetterException;
use Spiral\Filters\Model\Mapper\EnumCaster;
use Spiral\Tests\Filters\Fixtures\Status;
use Spiral\Tests\Filters\Fixtures\UserFilter;
Expand All @@ -28,6 +29,16 @@ public function testSetValue(): void
$this->assertEquals(Status::Active, $property->getValue($filter));
}

public function testSetValueException(): void
{
$setter = new EnumCaster();
$filter = $this->createMock(UserFilter::class);
$property = new \ReflectionProperty($filter, 'status');

$this->expectException(SetterException::class);
$setter->setValue($filter, $property, 'foo');
}

public static function supportsDataProvider(): \Traversable
{
$ref = new \ReflectionClass(UserFilter::class);
Expand Down
12 changes: 12 additions & 0 deletions src/Filters/tests/Mapper/UuidCasterTest.php
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Spiral\Filters\Exception\SetterException;
use Spiral\Filters\Model\Mapper\UuidCaster;
use Spiral\Tests\Filters\Fixtures\UserFilter;

Expand All @@ -27,6 +28,17 @@ public function testSetValue(): void
$this->assertSame('11111111-1111-1111-1111-111111111111', $property->getValue($filter)->toString());
}

public function testSetValueException(): void
{
$setter = new UuidCaster();
$filter = $this->createMock(UserFilter::class);
$ref = new \ReflectionProperty($filter, 'friendUuid');

$this->expectException(SetterException::class);
$this->expectExceptionMessage('Unable to set UUID value. Invalid UUID string: foo');
$setter->setValue($filter, $ref, 'foo');
}

public static function supportsDataProvider(): \Traversable
{
$ref = new \ReflectionClass(UserFilter::class);
Expand Down
29 changes: 29 additions & 0 deletions tests/Framework/Filter/Model/CastingErrorMessagesTest.php
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Framework\Filter\Model;

use Spiral\App\Request\CastingErrorMessages;
use Spiral\Filters\Exception\ValidationException;
use Spiral\Tests\Framework\Filter\FilterTestCase;

final class CastingErrorMessagesTest extends FilterTestCase
{
public function testValidationMessages(): void
{
try {
$this->getFilter(CastingErrorMessages::class, [
'uuid' => 'foo',
'uuidWithValidationMessage' => 'foo',
'uuidWithValidationMessageCallback' => 'foo',
]);
} catch (ValidationException $e) {
$this->assertSame([
'uuid' => 'Unable to set UUID value. Invalid UUID string: foo',
'uuidWithValidationMessage' => 'Invalid UUID',
'uuidWithValidationMessageCallback' => 'Invalid UUID: foo. Error: Unable to set UUID value. Invalid UUID string: foo',
], $e->errors);
}
}
}
29 changes: 29 additions & 0 deletions tests/app/src/Request/CastingErrorMessages.php
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Spiral\App\Request;

use Ramsey\Uuid\UuidInterface;
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Attribute\CastingErrorMessage;
use Spiral\Filters\Model\Filter;

final class CastingErrorMessages extends Filter
{
#[Post]
public UuidInterface $uuid;

#[Post]
#[CastingErrorMessage('Invalid UUID')]
public UuidInterface $uuidWithValidationMessage;

#[Post]
#[CastingErrorMessage(callback: [self::class, 'validationMessageCallback'])]
public UuidInterface $uuidWithValidationMessageCallback;

public static function validationMessageCallback(\Throwable $e, mixed $value): string
{
return \sprintf('Invalid UUID: %s. Error: %s', $value, $e->getMessage());
}
}

0 comments on commit e09951d

Please sign in to comment.