I have a DTO input that needs to be transformed into Entity object. DTO needs to be validated first and may be in invalid state.
There is section in manual that talks about validation of DTO https://api-platform.com/docs/core/dto/#validating-data-transfer-objects but it does not say what to to with validation result, whether it should be passed somewhere or thrown directly.
final class MyDataDransformer implements DataTransformerInterface
{
private ValidatorInterface $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function transform($dto, string $to, array $context = []): MyEntityObject
{
/** #var \Symfony\Component\Validator\ConstraintViolationList */
$validationResult = $this->validator->validate($dto);
if ($validationResult->count() > 0) {
// how to throw exception with validation result here?
// is this right place to throw this exception?
}
// if no validation errors, construct entity object and return as normal
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
return true; // for simplicity
}
}
Api Platform has mechanism for handling validation exceptions for entity object, it will format ConstraintViolationList object and output all errors as error type response. I need same for DTO.
There's a service in api platform that throws an exception if there's violations.
It is not the ValidatorInterface from symfony , but the api platform one
use ApiPlatform\Core\Validator\ValidatorInterface;
Related
I have an API Platform DataTransformer that uses an injected ValidatorInterface to verify the JSON POST body of an incoming request. It is basically a copy of the example in the docs:
<?php
namespace App\DataTransformer;
use ApiPlatform\Core\Validator\ValidatorInterface;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use App\Entity\Ticket;
use App\DTO\TicketInput;
class InputDataTransformer implements DataTransformerInterface
{
public function __construct(
private readonly ValidatorInterface $validator,
) {}
/**
* #param TicketInput $object
*/
public function transform($object, string $to, array $context = []): Ticket
{
// Validation
$this->validator->validate($object);
$ticket = new Ticket($object->id, $object->content);
return $ticket;
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
return (Ticket::class === $to) && (($context['input']['class'] ?? null) === TicketInput::class);
}
}
Via a ContextBuilder and the supportsTransformation() I ensure that the incoming $object is of type TicketInput. This class is as easy as that:
<?php
namespace App\DTO;
class TicketInput
{
public int $id;
public ?string $content;
}
What I am wondering about: The validator validates via the TicketInput::class that $id must be an integer and is not nullable. $content must be a string and is allowed to be null.
So these are valid inputs:
{
"id": 123,
"content": null
},
{
"id": 123,
"content": "testtest"
}
And this is invalid and gets rejected (400 - Bad request):
{
"id": null,
"content": "foobar"
}
So far so good. But one of my unit tests uses this JSON as request body:
{
"content": "foobar"
}
This broke my code as $id is a required value for the resulting Ticket::class.
I am pretty surprised that the last case was able to pass the validator, as I explicitly define the $id as (not nullable) class variable in TicketInput::class. One could argue that the validator is not obliged to validate properties that are uninitialized at time of validation (which is the case here, according to a var_dump) but nevertheless I am interested in how to define that this property is mandatory and has to be part of the JSON POST body.
The best what I could come up with was the Symfony Validation Constraint #[Assert\NotBlank(allowNull: false)] (see docs), which checks against is_null() and indirectly isset(), but I can't believe that this is the solution and I have to flag every single property like that (the actual TicketInput::class is way more huge and complex than this MWE).
Isn't there any validator config flag that tells it to assure the properties in a DTO are mandatory in a JSON body?
Every simple JSON Schema validator is capable of this and I am wondering why I don't find any hints for this either in the Symfony Docs nor the API Platform docs.
Using:
PostgreSQL 11 with uuid_generate_v4 type
Symfony 4.4.11
Api Platform 2.5.6
I have an Entity with the following Id :
/**
* #ORM\Entity(repositoryClass="App\Repository\ContractRepository")
* #ORM\HasLifecycleCallbacks
*/
class Contract
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="UUID")
* #ORM\Column(name="id", type="guid", unique=true)
*/
private $id;
[...]
I generate the following route with Api Platform :
App\Entity\Contract:
itemOperations:
get:
So I get a generated route like /contracts/{id}
Currently, if I do /contracts/TEST, it will try to do the SQL request with "TEST" in a where clause and so will fail as a 500.
I would like to prevent this behaviour by asserting that the {id} parameter is a UUID_v4 and return a 400 if not.
This behaviour is DBMS specific, so you have to add your own logic.
The API-Platform component which retrieve an entity given an ID is the ItemDataProviderInterface.
First, I will declare a new exception MalformedUuidException.
Next, I will convert this exception to a 400 error.
Finally, I will create a new ItemDataProviderInterface implementation, wrapping the ORM one and adding some checks to the ID:
class ContractDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface
{
/** #var ItemDataProviderInterface */
private $realDataProvider;
public function __construct(ItemDataProviderInterface $realDataProvider)
{
$this->realDataProvider = $realDataProvider;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
$uuidPattern = '/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i';
if (preg_match($uuidPattern, $id) === 1) {
return $this->realDataProvider->getItem($resourceClass, ['id' => $id], $operationName, $context);
} else {
throw new MalformedUuidException("the given ID \"$id\" is not a valid UUID.");
}
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return $resourceClass === Contract::class;
}
}
# config/services.yaml
App\DataProvider\ContractDataProvider:
arguments:
$realDataProvider: '#api_platform.doctrine.orm.default.item_data_provider'
However, note that the getItem() method's contract does not specify the MalformedUuidException exception, so this implementation breaks the Liskov substitution principle.
Consider returning null instead and be satisfied with a 404 error.
I'm currently building a web application, and went for Symfony 4 along with API Platform.
I built a custom data provider in order to pull data from a XML file, for an entity. Since it's all one-way operations, I only enabled GET operations for both items and collections.
I am trying to tie the entity served by this custom data provider to a Doctrine entity, but I'm getting an error saying that the entity is not a valid one or mapped super class.
How do I create a relationship between these two? Is it even possible?
Thanks!
This is a snippet from the aforementioned entity:
<?php
//src/Entity/Sst.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
/**
* #ApiResource(
* collectionOperations={"get"={"method"="GET"}},
* itemOperations={"get"={"method"="GET"}}
* )
*/
class Sst
{
public $code_urssaf;
/**
* #ApiProperty(identifier=true)
*/
public $code_sst;
// ...and a few others
public function getCodeUrssaf(): ?string
{
return $this->code_urssaf;
}
public function setCodeUrssaf(string $code_urssaf): self
{
$this->code_urssaf = $code_urssaf;
return $this;
}
public function getCodeSst(): ?string
{
return $this->code_sst;
}
public function setCodeSst(string $code_sst): self
{
$this->code_sst = $code_sst;
return $this;
}
// and so on; this is generated then tuned with Symfony's MakerBundle.
Here's a bit from the collection data provider, with imports omitted (but everything works when querying the API directly):
final class SstCollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
public function __construct(FilesystemInterface $extdataStorage, SerializerInterface $serializer)
{
$this->serializer = $serializer;
$this->storage = $extdataStorage;
}
public function supports(string $resourceClass, string $operationName = null, array $context = [] ): bool
{
return Sst::class === $resourceClass;
}
public function getCollection(string $resourceClass, string $operationName = null): \Generator
{
$sstfile = $this->storage->read('SST_29072019.xml');
$sstlist = $this->serializer->deserialize($sstfile, SstCollection::class, 'xml', array('object_to_populate' => $sstlist = new SstCollection()));
foreach($sstlist as $sstObject)
{
yield $sstObject;
}
}
}
The Doctrine entity has this, mirroring other relationships:
/**
* #var Sst[]
*
* #ORM\ManyToOne(targetEntity="App\Entity\Sst")
*/
private $sst;
I expect to be able to tie the custom entity to the Doctrine one, but I cannot even start the Symfony app, I'm getting:
In MappingException.php line 346:
Class "App\Entity\Sst" is not a valid entity or mapped super class.
I have this entity:
AppBundle\Entity\Ciudad
class Ciudad{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ComunidadAutonoma")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_ccaa", referencedColumnName="id")
* })
*/
private $ccaa;
....
public function getCcaa()
{
return $this->ccaa;
}
public function setCcaa(ComunidadAutonoma $ccaa)
{
$this->ccaa = $ccaa;
}
}
And the other entity is:
AppBundle\Entity\ComunidadAutonoma
class ComunidadAutonoma{
properties
getters
setters
}
In a controller, I get data from a form, and I´m triying to deserialize the data into a Ciudad entity, but is getting me allways the same error:
Expected argument of type "AppBundle\Entity\ComunidadAutonoma", "integer" given
In the form data I send to the action in the controller, the value of the comunidadautonoma is the id of the selected option in a combo:
{
parameters...
ccaa:7,
parameters...
}
In my controller I have this:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use AppBundle\Entity\Ciudad;
class CiudadController extends Controller
{
public function procesarAction(Request $request)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$this->serializer = new Serializer($normalizers, $encoders);
$ciudad= $this->serializer->deserialize($parametros['parametros'], Ciudad::class, 'json');
}
}
Am I missing something?Do I need any special configuration to deserializer an entity with a relation?
You dont have to do anything if you properly configured a type. While creating a Form Type for your entity please add class name to your type like:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ciudad::class,
]);
}
And please use english naming for your projects.
First of all, since you are sending form data to your controller you could use Form Type classes to leverage all the power of the Symfony Form Component that will all this job for you.
Answering your specific question (and assuming you cannot/don't want to use Symfony Form Component) this error is absolutely expected. As you can see in your setCcaa function declaration inside Ciudad class:
public function setCcaa(ComunidadAutonoma $ccaa)
Because of the type-hinting (ComunidadAutonoma $ccaa) setCcaa function expects an argument of type ComunidadAutonoma. Now when Symfony serializer tries to denormalize your json object it calls setCcaa function with argument the ccaa value provided in your json (in your example is 7) which happens to be an integer. So Symfony complains that you provide an integer instead of ComunidadAutonoma type.
In order to solve this problem you have to create and use your own normalizer so that you can transform this integer to the corresponding entity object from your database. Something like this:
class EntityNormalizer extends ObjectNormalizer
{
/**
* Entity manager
* #var EntityManagerInterface
*/
protected $em;
public function __construct(
EntityManagerInterface $em,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
// Entity manager
$this->em = $em;
}
public function supportsDenormalization($data, $type, $format = null)
{
return strpos($type, 'App\\Entity\\') === 0 && (is_numeric($data) || is_string($data));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->em->find($class, $data);
}
}
What this normalizer does is that it checks if your data type (in this case $ccaa) is type of an entity and if the data value provided (in this case 7) is an integer, it transforms this integer to the corresponding entity object from your database (if existing).
To get this normalizer working you should also register it in your services.yaml configuration, with the appropriate tags like this:
services:
App\Normalizer\EntityNormalizer:
public: false
autowire: true
autoconfigure: true
tags:
- { name: serializer.normalizer }
You could also set the normalizer's priority but since the default priority value is equal to 0 when Symfony's built-in normalizers' priority is by default negative, your normalizer will be used first.
You could check a fully explained example of this in this fine article.
File structure
customerService.PHP
include 'vo/VOCustomer.php';
include 'mydb.php';
class customerService
{
public function createCustomer(VOCustomer $cus)
{
$db = new mydb();
$db->connect();
$query = sprintf("insert into customer (CusId, CusName, CusContact,idcompany) values ('%s','%s','%s','%s')",
mysql_real_escape_string($cus->CusId),
mysql_real_escape_string($cus->CusName),
mysql_real_escape_string($cus->CusContact),
mysql_real_escape_string($cus->idcompany));
$rs = mysql_query($query) or die ("Unable to complete query.");
return 'success';
}
}
vo/VOCustomer.php
class VOCustomer {
public $CusId;
public $CusName;
public $CusContact;
public $idcompany;
}
When importing the customerService.php to a flex zend project Its possible that the data type may not return as VOCustomer sometimes it will show Object as type
How to make the passing object as VOcustomer object ?
I'm not sure the 'Connect to PHP' wizard understands type hinting.
Even if it does Zend AMF will pass an Objet not a VOCustomer to the method.
It's safer to add a PHPDoc comment:
/**
* #param VOCustomer $cus
*/
public function createCustomer($cus)
Second add dummy function to your service that returns VOCustomer. The 'Connect to PHP' wizard generates a value object only if it's returned by a service method.
/**
* #return VOCustomer
*/
public function getCustomer() {
//Do nothing
}