How to validate PUT input in ApiPlatform? - api-platform.com

suppose I have an ApiResource like this:
class MyResource {
const STATUS_A = 'A';
const STATUS_B = 'B';
const STATUS_C = 'C';
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=50)
* #Assert\Choice(
* choices = {MyResource::STATUS_A, MyResource::STATUS_B, MyResource::STATUS_C}
* )
* #Assert\NotNull()
*/
private $status;
/**
* #ORM\Column(type="integer")
* #Assert\NotNull()
*/
private $counter;
...
}
As you can see, I setup validation constraints so the status value can be STATUS_A, STATUS_B or STATUS_C.
Now, I'd like to also validate the data provided in PUT requests, so that status can only be set to STATUS_A.
For example, the following PUT payload should be accepted:
{
"status": "A"
}
{
"status": "A",
"counter: 5
}
{
"counter": 5
}
Instead, the following one should NOT be accepted:
{
"status": "B"
}
{
"status": "C",
"counter": 5
}
What is the best point to perform validation against data provided by the API client?
I know that I could use validation inside DTOs DataTransformers, but I'm not using DTOs this time.
Thank you. Any advice would be really appreciated.

Related

Api platoform ManyToMany unidirectional relation with JoinTable error: attribute must be an array string given

So I have two entites :
NextInfusionMashStepWithoutGrainAdjunct
// [...]
/**
* #ORM\ManyToMany(targetEntity=WaterGrainRatio::class)
* #ORM\JoinTable(name="next_infusion_mash_step_without_grain_adjunct_water_grain_ratio",
* joinColumns={#ORM\JoinColumn(name="next_infusion_mash_step_without_grain_adjunct_water_grain_ratio_id", referencedColumnName="id", nullable=false)},
* inverseJoinColumns={#ORM\JoinColumn(name="water_grain_ratio_id", referencedColumnName="id", nullable=false)}
* )
* #Groups("post:nextInfusionStepWithoutGrainAdjunct")
*/
private ?WaterGrainRatio $waterGrainRatioId;
//[...]
And WaterGrainRatio
//[...]
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
//[...]
I have added an WaterGrainRatioId with id: 1
And when I try to add a NextInfusionStepWithoutGrainAdjunct with postman with this body:
{
"waterGrainRatioId": "/dashboard/water_grain_ratios/1",
"...": "..."
}
I got this error: "The type of the \"waterGrainRatioId\" attribute must be \"array\", \"string\" given.",
I can't figure out why The IRI is not understood and why it needs an array.
What I tried:
#[ApiProperty(readableLink: false, writableLink: false)] but it did not works.
EDIT: I figure it out with this post, I just have to pass an array of IRI like this:
"waterGrainRatioId": [
"/dashboard/water_grain_ratios/1",
"/dashboard/water_grain_ratios/2"
]
Like I said in my edit I solve my problem by passing an array of IRIs:
"waterGrainRatioId": [
"/dashboard/water_grain_ratios/1",
"/dashboard/water_grain_ratios/2"
]

Store JSON data into TEXT mysql column with doctrine

I have an entity with one TEXT (MySQL) attributes
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Index;
use ApiPlatform\Core\Annotation\ApiProperty;
/**
* #ApiResource(
* attributes={},
* collectionOperations={
* "get"={},
* "post"={
* "access_control"="is_granted('ROLE_COMPANY')"
* },
* },
* itemOperations={
* "get"={},
* "put"={"access_control"="is_granted('ROLE_COMPANY')"},
* }
* )
* #ORM\Entity(
* repositoryClass="App\Repository\SettingRepository",
* )
* #ORM\Table(
* indexes={#Index(name="domain_idx", columns={"domain"})}
* )
*/
class Setting
{
/**
* #var Uuid
* #ApiProperty(identifier=true)
* #ORM\Id
* #ORM\Column(type="string")
* #ORM\GeneratedValue(strategy="NONE")
*/
private $identifier;
/**
* #ORM\Column(type="text", nullable=true)
*/
private $data = array();
/**
* #ORM\Column(type="string", nullable=true)
*/
private $domain = array();
public function getData()
{
if($this->data == null) return array();
$data = unserialize($this->data);
return $data;
}
public function setData($data): self
{
$this->data = serialize($data);
return $this;
}
/**
* #return mixed
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* #param mixed $key
*/
public function setIdentifier($identifier): self
{
$this->identifier = $identifier;
return $this;
}
/**
* #return mixed
*/
public function getDomain()
{
return $this->domain;
}
/**
* #param mixed $domain
*/
public function setDomain($domain): self
{
$this->domain = $domain;
return $this;
}
}
If I try to invoke the service with the following parameter structure it works fine:
{
"data": "testData",
"identifier": "testIdentifier",
"domain": "domain1"
}
But If I would like to store an embedded JSON string, for example:
"data": {"temp": 123}
I receive the following error:
hydra:description": "The type of the \"data\" attribute must be \"string\", \"array\" given.",
I tried to convert the object into an string in the method setData. But this method will not be invoked. It seams, that the API-Platform detects the wrong type and throws the exception.
I found some comments, that it is necessary to decorate the property:
https://api-platform.com/docs/core/serialization/#decorating-a-serializer-and-adding-extra-data
Can anyone give me an example? It does not work!
Where is the right place to serialise and unserialise the property data?
Does anyone have an idea?
Kind regards
You need to set the column type to json in MySQL. It should behave as expected.
/**
* #var array Additional data describing the setting.
* #ORM\Column(type="json", nullable=true)
*/
private $data = null;
I think null is more consistent than an empty array, but that's your choice.

Filter children in Doctrine Entity as property and mutate/hydrate form input

I'm developing an article database with API endpoints using Symfony 3.2 and Doctrine 2.5. In the request body, I have an Author:
{
"article": {
"title": "Some article",
"author": {
"email": "someone#something.com",
"name": "Howard Wedothis"
}
}
}
I'd like to store an Update entity with a datetime, article ID and the Author ID. In order to do this currently, I'm having to post the following:
{
"article": {
"title": "Some article",
"updates": [{
"author": {
"email": "someone#something.com",
"name": "Howard Wedothis"
}
}]
}
}
This isn't as elegant IMO.
First question: What's the best way to mutate/hydrate the simple author fields and store an Update against the article and author? Authors and Articles are ManyToMany but I'd like to avoid posting an array of one for readability reasons mostly.
Second question: Is it possible to add properties to the article entity for created and lastUpdated to display the first and last update respectively from the article without injecting a repository into the entity?
i.e.
$article->getCreated() // returns first Update
$article->getLastUpdated() // returns last Update
In the end I solved this by adding the Author and overriding the Author on updates and using the Lifecycle Events to create a new Update entity on PrePersist and PreUpdated as suggested by #Matko.
Request Data:
{
"article": {
"title": "Some article",
"author": {
"email": "someone#something.com",
"name": "Howard Wedothis"
}
}
}
Article.php:
/**
* #ORM\Entity
* #ORM\Table(name="articles")
* #ORM\HasLifecycleCallbacks
*/
class Article
{
/**
* #return Update
*/
public function getCreated(): Update
{
return $this->updates->first();
}
/**
* #return Update
*/
public function getLastUpdated(): Update
{
return $this->updates->last();
}
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function addUpdate(): void
{
$update = new Update();
$update->setAuthor($this->getAuthor());
$update->setArticle($this);
$this->updates->add($update);
}
}
This method does sort of invalidate the Author of the entity but keeps the article history.
Answer to the second question - use Lifecycle Callbacks
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\HasLifecycleCallbacks
*/
class Author
{
...
/**
* #ORM\Column(type="datetime")
*/
protected $createdAt;
/**
* #ORM\Column(type="datetime")
*/
protected $updatedAt;
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set createdAt.
*
* #param \DateTime $createdAt
*
* #return this
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt.
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set modifiedAt.
*
* #param \DateTime $updatedAt
*
* #return Article
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt.
*
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
...
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updatedTimestamps()
{
$this->setUpdatedAt(new \DateTime(date('Y-m-d H:i:s')));
if ($this->getCreatedAt() == null) {
$this->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
}
}
}
http://symfony.com/doc/current/doctrine/lifecycle_callbacks.html
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events
Regarding your request examples, insert request and update request should have the same format. You are complicating your life with that updates key.
Use POST method for inserting article and PUT (you send complete article object) or PATCH (you send only fields that you want to update).
https://knpuniversity.com/screencast/rest/put-versus-post
https://knpuniversity.com/screencast/symfony-rest/patch-programmer

Symfony API Entity Validation

I have a class with annotations for the validation.
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serialize;
use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Annotation\Link;
/**
* #Serialize\ExclusionPolicy("all")
* #Serialize\AccessType(type="public_method")
* #Serialize\AccessorOrder("custom", custom = {"id", "name", "awardType", "nominations"})
* #ORM\Entity(repositoryClass="AppBundle\Repository\AwardRepository")
* #ORM\Table(name="awards")
* #Link("self", route = "api_awards_show", params = { "id": "object.getId()" })
*/
class Award extends Entity
{
/**
* #Serialize\Expose()
* #Serialize\Type(name="string")
* #Assert\Type(type="string")
* #Assert\NotBlank(message="Please enter a name for the Award")
* #Assert\Length(min="3", max="255")
* #ORM\Column(type="string")
*/
private $name;
/**
* #Serialize\Expose()
* #Serialize\Type(name="AppBundle\Entity\AwardType")
* #Serialize\MaxDepth(depth=2)
* #Assert\Valid()
* #ORM\ManyToOne(
* targetEntity="AppBundle\Entity\AwardType",
* inversedBy="awards"
* )
*/
private $awardType;
/**
* #Serialize\Expose()
* #Serialize\Type(name="ArrayCollection<AppBundle\Entity\Nomination>")
* #Serialize\MaxDepth(depth=2)
* #ORM\OneToMany(
* targetEntity="AppBundle\Entity\Nomination",
* mappedBy="award"
* )
*/
private $nominations;
}
I then validate that entity with the following:
$validator = $this->get('validator');
$errors = $validator->validate($entity);
if (count($errors) > 0) {
$apiProblem = new ApiProblem(
400,
ApiProblem::TYPE_VALIDATION_ERROR
);
$apiProblem->set('errors', ['testing', 'array']);
throw new ApiProblemException($apiProblem);
}
$this->save($entity);
This works fine the problem is that i cant get the information on which fields have errors and their error message. $errors in this case seems to be of an unknown type which i cant seem to get the error messages for any fields.
How do i get the error messages of that object?
You can get the exact messages of the errors like this:
$errors = $validator->validate($entity);
if (count($errors) > 0) {
$formattedErrors = [];
foreach ($errors as $error) {
$formattedErrors[$error->getPropertyPath()] = [
'message' => sprintf('The property "%s" with value "%s" violated a requirement (%s)', $error->getPropertyPath(), $error->getInvalidValue(), $error->getMessage());
];
}
return new \Symfony\Component\HttpFoundation\JsonResponse($formattedErrors, 400);
}
For example, that could output:
[
'field1' => 'The property "field1" with value "" violated a requirement (Cannot be null)',
// ...
]

Symfony2+Doctrine - Validating one-to-many collection of entities

I have a form to create a new entity. That entity has a collection of other entities that are also entered in that form.
I want to use the validation options of the entity in the collection to validate those entities but it does not work. The validation rules of the "main" entity (Person) are checked, but the validation rules of the entities in the addressList collection (Address) are not checked. When I input invalid information in the fields, the submitted form is successfully validated.
In this example, the annotation for street is not used on validation.
class Person
{
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
*/
protected $addressList;
....
}
class Address
{
...
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="addressList")
* #ORM\JoinColumn(name="person_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $owner;
/**
* #ORM\Column(type="string", length=75)
* #Assert\MinLength(
* limit=3,
* message="Street must have atleast {{ limit }} characters."
* )
*/
protected $street;
...
}
How can I get the form to validate the supplied Address entities?
I had the same problem but was solved with:
/**
* #ORM\OneToMany(
* targetEntity="Entity",
* mappedBy="mappedEntity",
* cascade={"persist" , "remove"}
* )
* #Assert\Valid
*/
I use this:
use Symfony\Component\Validator\ExecutionContextInterface;
class Person
{
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
*/
protected $addressList;
....
/**
* #Assert\Callback
*/
public function validate(ExecutionContextInterface $context)
{
if (!$this->getAddressList()->count()) {
$context->addViolationAt(
'addressList',
'You must add at least one address',
array(),
null
);
}
}
}
http://symfony.com/doc/current/reference/constraints/Callback.html
Just add annotation assert like following
/**
* #Assert\Count(
* min = "1",
* minMessage = "You must specify at least one"
* )
* #Assert\Valid
*
*/
protected $name_of_collection_property;
You could also use the "Valid" constraint with the "All" constraint :
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
* #Assert\All({
* #Assert\Valid
* })
*/
protected $addressList;

Resources