I’m currently working on an integration with a third party API for an application, for which I’m using Spatie’s data transfer object library. I’m currently looking to set up validation for some fields, and have run into an issue.
I’ve written the following validator attribute, to validate that a string contains an email address:
<?php declare(strict_types=1); namespace AppHttpIntegrationsFooDataValidators; use Attribute; use SpatieDataTransferObjectValidationValidationResult; use SpatieDataTransferObjectValidator; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class ValidEmail implements Validator { public function validate(mixed $value): ValidationResult { if (filter_var($value, FILTER_VALIDATE_EMAIL)) { return ValidationResult::valid(); } return ValidationResult::invalid("Value should be a valid email address"); } }
And it’s used in this DTO implementation:
<?php declare(strict_types=1); namespace AppHttpIntegrationsFooData; use AppHttpIntegrationsFooDataValidatorsValidEmail; use CarbonCarbonImmutable; /** * DTO class for a child in the birthday request results * * @psalm-immutable */ final class ChildBirthday extends DTO { #[ValidEmail] public readonly string $email; public readonly CarbonImmutable $dob; public readonly bool $purchased; public function __construct(string $email, string $dob, bool $purchased) { $this->email = $email; $this->dob = CarbonImmutable::parse($dob); $this->purchased = $purchased; } public static function fromResponseItem(string $email, string $dob, bool $purchased): self { return new static($email, $dob, $purchased); } public function __toString(): string { return $this->email; } }
NB: The DTO class extends my own bespoke DTO base class, but that is relatively simple – it just extends the Spatie one to apply strict mode, set a default cast for CarbonImmutable
and set the psalm-immutable
and psalm-seal-properties
annotations.
I’ve also written a Pest test for it:
<?php declare(strict_types=1); namespace TestsFeatureHttpIntegrationsFooData; use AppHttpIntegrationsFooDataChildBirthday; use CarbonCarbonImmutable; use SpatieDataTransferObjectExceptionsValidationException; use function PestFakerfaker; it('throws an error if the email is not valid', function () { ChildBirthday::fromResponseItem( email: 'invalid', dob: faker()->date(), purchased: faker()->boolean() ); })->throws(ValidationException::class);
However, the test fails to throw the required exception. Instantiating the class manually with an invalid email address also doesn’t throw the exception.
My validation attribute looks broadly consistent with the example given in the documentation, but this is actually the first time I’ve used PHP 8 attributes, so I’m wondering if there’s something I’ve got wrong about the syntax. I’ve tried setting a breakpoint in the attribute with PsySh and it never triggers when populating a new instance of the DTO in Laravel Tinker, so it looks like it’s never calling the attribute in the first place.
Advertisement
Answer
Found the problem. It’s due to my setting a constructor in the DTO class – it’s fine to set a named constructor as I have done, but not the __construct()
method. Removing the constructor method resolves the issue.