-
-
Save webdevilopers/93b82fedd25a9d43d2af64de2fa0c57f to your computer and use it in GitHub Desktop.
<?php | |
use Webmozart\Assert\Assert; | |
final class Address | |
{ | |
/** @var string|null $street */ | |
private $street; | |
/** @var Postcode|null $postcode */ | |
private $postcode; | |
/** @var string|null $city */ | |
private $city; | |
/** @var CountryCode $countryCode */ | |
private $countryCode; | |
private function __construct(?string $aStreet, ?Postcode $aPostcode, ?string $aCity, CountryCode $aCountryCode) | |
{ | |
Assert::nullOrNotEmpty($aStreet); | |
Assert::nullOrNotEmpty($aCity); | |
$this->street = $aStreet; | |
$this->postcode = $aPostcode; | |
$this->city = $aCity; | |
$this->countryCode = $aCountryCode; | |
} | |
public static function fromArray(array $array): Address | |
{ | |
Assert::keyExists($array, 'street'); | |
Assert::nullOrString($array['street']); | |
Assert::keyExists($array, 'postcode'); | |
Assert::nullOrString($array['postcode']); | |
Assert::keyExists($array, 'city'); | |
Assert::nullOrString($array['city']); | |
Assert::keyExists($array, 'countryCode'); | |
Assert::nullOrString($array['countryCode']); | |
return new self( | |
$array['street'], | |
null !== $array['postcode'] ? Postcode::fromString($array['postcode']) : null, | |
$array['city'], | |
null !== $array['countryCode'] ? CountryCode::fromString($array['countryCode']) : null | |
); | |
} | |
public function toArray(): array | |
{ | |
return [ | |
'street' => $this->street, | |
'postcode' => null !== $this->postcode ? $this->postcode->toString() : null, | |
'city' => $this->city, | |
'countryCode' => null !== $this->countryCode ? $this->countryCode->toString() : null | |
]; | |
} | |
public function street(): ?string | |
{ | |
return $this->street; | |
} | |
public function postcode(): ?Postcode | |
{ | |
return $this->postcode; | |
} | |
public function city(): ?string | |
{ | |
return $this->city; | |
} | |
public function countryCode(): CountryCode | |
{ | |
return $this->countryCode; | |
} | |
} |
<?php | |
final class AgencyController | |
{ | |
/** @var MessageBusInterface */ | |
private $commandBus; | |
public function hireAction(Request $request): Response | |
{ | |
$command = new HireAgency(json_decode($request->getContent(), true)); | |
$this->commandBus->dispatch($command); | |
return new JsonResponse(null, Response::HTTP_CREATED); | |
} | |
} |
<?php | |
use Symfony\Component\Validator\Constraints as Assert; | |
final class HireAgency | |
{ | |
/** | |
* @var AgencyId | |
* @Assert\NotNull | |
* @Assert\Uuid | |
*/ | |
private $agencyId; | |
/** | |
* @var string | |
* @Assert\NotNull | |
* @Assert\NotBlank | |
*/ | |
private $name; | |
/** | |
* @var ValueAddedTaxIdentificationNumber | |
* @Assert\NotNull | |
* @Assert\NotBlank | |
*/ | |
private $vatId; | |
/** | |
* @var string | |
* @Assert\Type(type="string") | |
* @Assert\NotNull | |
* @Assert\NotBlank | |
*/ | |
private $contactPerson; | |
/** | |
* @var ContactInformation | |
* @Assert\NotNull | |
* @Assert\Type(type="array") | |
* @Assert\Collection( | |
* fields = { | |
* "email" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Email, | |
* }, | |
* "phone" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string") | |
* }, | |
* "mobile" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string") | |
* }, | |
* "fax" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string") | |
* }, | |
* }, allowExtraFields=false | |
* ) | |
*/ | |
private $contactInformation; | |
/** | |
* @var Address | |
* @Assert\NotNull | |
* @Assert\Type(type="array") | |
* @Assert\Collection( | |
* fields = { | |
* "street" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string"), | |
* }, | |
* "postcode" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string") | |
* }, | |
* "city" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string") | |
* }, | |
* "countryCode" = { | |
* @Assert\NotBlank(allowNull=true), | |
* @Assert\Type(type="string") | |
* }, | |
* }, allowExtraFields=false | |
* ) | |
*/ | |
private $address; | |
public function __construct(array $payload) | |
{ | |
$this->agencyId = $payload['agencyId']; | |
$this->name = $payload['name']; | |
$this->vatId = $payload['vatId']; | |
$this->contactPerson = $payload['contactPerson']; | |
$this->contactInformation = $payload['contactInformation']; | |
$this->address = $payload['address']; | |
} | |
public function agencyId(): AgencyId | |
{ | |
return AgencyId::fromString($this->agencyId); | |
} | |
public function name(): string | |
{ | |
return $this->name; | |
} | |
public function vatId(): ValueAddedTaxIdentificationNumber | |
{ | |
return ValueAddedTaxIdentificationNumber::fromString($this->vatId); | |
} | |
public function contactPerson(): string | |
{ | |
return $this->contactPerson; | |
} | |
public function contactInformation(): ContactInformation | |
{ | |
return ContactInformation::fromArray($this->contactInformation); | |
} | |
public function address(): Address | |
{ | |
return Address::fromArray($this->address); | |
} | |
} |
If I understand it correctly, you’ll like the validator to catch any extra JSON fields. If so, that’s just not possible with the validator in the normal setup. The problem is the order in which things happen:
A) you use the JSON to create the DTO
B) THEN you validate the DTO
By the time you get to step B, all you have is the finished object - the validator has no idea if there were originally extra fields in the JSON.
The only way to do this would be to turn the JSON into an array, then validate the array, and THEN turn it into a DTO. But that’s a lot of work just to prevent extra fields. For me, extra fields in an API are fine - just ignore them. If you have a reason to believe that clients might accidentally pass a specific extra field, then add that to your DTO and validate that it IS null (if it’s not null - tell them via the validation error that they are not allowed to send this field).
Cheers!
@weaverryan That makes sense. We used to structure commands differently before. We had an additional "payload" attribute that could be validated by the Collection
constraint. But we wanted to simplify our DTOs. In the end you are right that this is not crucial to the actual application and maybe "over-engineering".
Thank you for your feedback!
Came from:
Here is an example of a valid JSON payload request made to the controller:
Adding extra fields to
address
would violate theCollection
constraint.This field was not expected.
Currently there is no danger from the client input since it will not get passed:
But it would be nice to have this feature out of the box for the entire command when dispatching it.
This would restrict API clients to provide the correct JSON payload of the current API version and warn them otherwise.
Can the
Collection
constraint be applied to the complete DTO?