Symfony2 Validation using Doctrine2 Annotations - php

I have the following Doctrine entity and I want to use its restriction also for validation.
/**
* #var string
*
* #ORM\Column(type="string", length=40)
* #Assert\Valid
*/
private $birthName;
I use the following validation, which works for symfony specific annotaions but not Doctrine set restrictions!
// Validate data
$validator = $this->get('validator');
$errors = $validator->validate($user);
if (count($errors) > 0) {
$response = new JsonResponse(array(
'error' => 'User could not be created.' . PHP_EOL . (string)$errors
));
$response->setStatusCode(400);
return $response;
}
What can I do to let symfony validator use the doctrine restrictions as settings?
Status quo:
I read [1] and [2] but so far I do not use forms because I have a controller returning JSON. If you know how to make this work with forms would also help a lot!

Doctrine mappings have nothing to do with validation.
The code #ORM\Column(type="string", length=40) only maps a property to a database field, and sets max length of a database field to be equal 40 characters, if you would create a schema using doctrine.
But thus doesn't take any part of the validation process.
So you need to set an assertion rule by something like
/**
* #Assert\Length(max = 40)
*/

Related

Change Entity assert constrains dynamically without FormType

So the problem is like this:
I am trying to save some data from API and I need to validate them with Symfony validation ex:
private $id;
/**
* #var
* #Assert\Length(max="255")
* #CustomAssert\OrderExternalCode()
* #CustomAssert\OrderShipNoExternalCode()
*/
private $code;
private $someId;
/**
* #var
* #Assert\NotBlank()
* #Assert\Length(max="255")
*/
private $number;
this works well but now I need to add some Assert Constrains dynamically from the controller and that is where I am stuck!
Does anyone knows how to do that or any suggestion that might help?
Currently I did an extra constraint which does extra query in the DB and I don't want to do that and I am not using FormType.
You can use groups and use (or leave out) the extra group you're talking about.
Using the CallbackConstraint should help I think, in your case :
use My\Custom\MyConstraint;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
// This is not tested !
class MyEntity
{
/**
* #Assert\Callback()
*/
public function validateSomeId(ExecutionContextInterface $context)
{
$constraint = new MyConstraint(['code' => $this->code]);
$violations = $context->getValidator()->validate($this->number, $constraint);
foreach ($violations as $violation) {
$context->getViolations()->add($violation);
}
}
}
See https://symfony.com/doc/current/reference/constraints/Callback.html
EDIT : I don't know what you're trying to validate so I just put some random params of your entity in there
So I wanted to dynamically validate the request data based on a condition in the controller.
I specified an extra group for that in the entity like so:
/**
* #var
* #Assert\NotBlank(groups={"extra_check"})
* #Assert\Length(max="255")
*/
private $externalId;
Then in the controller I just did the condition to validate with the extra group or not.
$groups = $order->getExternalCode() != null ? ['Default'] : ['Default', 'extra_check'];
$this->validateRequest($request, null, $groups);
The Default group is the one without group specified and the other one is the group I specified in the field

Symfony 3 + FOS REST Bundle: Normalize values before validation

I'm working on a FOS REST API. In the underlying models I'd like to be able to define Constraints representing the form appropriate for the datastore, for example, a US Phone Number should be exactly 10 digits.
/**
* #var string
*
* #Assert\NotBlank(message="Phone is required.")
* #Assert\Regex(message="Exactly 10 digits are required.", pattern="/^\d{10}$/")
*/
private $phone;
On the other hand I'd like to be able to accept liberal values, for example a phone number formatted as:
{
"phone": "603-988-6521"
}
The ideal way to implement this would be to have some type of "conversion" or "normalization" phase where select fields could be converted to all digits etc. prior to validation.
What would be the best way to accomplish this in the FOST REST paradigm and Symfony 3?
It turns out that this is very simple. You can do any type of normalization needed in the actual setters of your model. You just need to configure JMS Serializer to use setters rather than using property reflection. Example with annotations:
/**
* #var string
*
* #JMS\Accessor(getter="getPhone", setter="setPhone")
* #Assert\Regex(message="Exactly 10 digits are required.", pattern="/^\d{10}$/")
*/
private $phone;
/**
* #param string
*/
public function setPhone($phone)
{
if ($phone === null) {
$this->phone = null;
return;
}
$this->phone = preg_replace('/[^0-9]/', '', $phone);
}

Symfony Doctrine One to Many does not insert foreign key

I am having annoying problems with persisting an entity with one or more OneToMany-Childs.
I have a "Buchung" entity which can have multiple "Einsatztage" (could be translated to an event with many days)
In the "Buchung entity I have
/**
* #param \Doctrine\Common\Collections\Collection $property
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung", cascade={"all"})
*/
private $einsatztage;
$einsatztage is set to an ArrayCollection() in the __constructor().
Then there is the "Einsatztag" Entity which has a $Buchung_id variable to reference the "Buchung"
/**
* #ORM\ManyToOne(targetEntity="Buchung", inversedBy="einsatztage", cascade={"all"})
* #ORM\JoinColumn(name="buchung_id", referencedColumnName="id")
*/
private $Buchung_id;
Now If I try to persist an object to the database the foreign key of the "Einsatztag" Table is always left empty.
$buchung = new Buchung();
$buchung->setEvent( $r->request->get("event_basis"));
$buchung->setStartDate(new \DateTime($r->request->get("date_from")));
$buchung->setEndDate(new \DateTime($r->request->get("date_to")));
$von = $r->request->get("einsatz_von");
$bis = $r->request->get("einsatz_bis");
$i = 0;
foreach($von as $tag){
$einsatztag = new Einsatztag();
$einsatztag->setNum($i);
$einsatztag->setVon($von[$i]);
$einsatztag->setBis($bis[$i]);
$buchung->addEinsatztage($einsatztag);
$i++;
}
$em = $this->getDoctrine()->getManager();
$em->persist($buchung);
foreach($buchung->getEinsatztage() as $e){
$em->persist($e);
}
$em->flush();
Firstly, you have to understand that Doctrine and Symfony does not work with id's within your entities.In Einsatztag entity, your property should not be called $Buchung_id since it's an instance of buchung and not an id you will find out there.
Moreover, in your loop, you add the Einsatztag to Buchung. But do you process the reverse set ?
I do it this way to always reverse the set/add of entities.
Einsatztag
public function setBuchung(Buchung $pBuchung, $recurs = true){
$this->buchung = $pBuchung;
if($recurs){
$buchung->addEinsatztag($this, false);
}
}
Buchung
public function addEinsatztag(Einsatztag $pEinsatztag, $recurs = true){
$this->einsatztages[] = $pEinsatztag;
if($recurs){
$pEinsatztag->setBuchung($this, false);
}
}
Then, when you will call
$buchung->addEinsatztag($einsatztag);
Or
$einsatztag->set($buchung);
The relation will be set on both side making your FK to be set. Take care of this, you'll have some behavior like double entries if you do not use them properly.
SImplier , you can use default getter/setters and call them on both sides of your relation, using what you already have, like following:
$einsatztag->set($buchung);
$buchung->addEinsatztag($einsatztag);
Hope it helped ;)
First of all, don't use _id properties in your code. Let it be $buchung. If you want it in the database, do it in the annotation. And this also the reason, why it's not working. Your are mapping to buchung, but your property is $Buchung_id
<?php
/** #ORM\Entity **/
class Buchung
{
// ...
/**
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung")
**/
private $einsatztage;
// ...
}
/** #ORM\Entity **/
class Einsatztag
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="einsatztage")
* #JoinColumn(name="buchung_id", referencedColumnName="id")
**/
private $buchung;
// ...
}
You don't have to write the #JoinColumn, because <propertyname>_id would the default column name.
I'm going to ignore the naming issue and add a fix to the actual problem.
You need to have in the adder method a call to set the owner.
//Buchung entity
public function addEinsatztage($einsatztag)
{
$this->einsatztags->add($einsatztag);
$ein->setBuchung($this);
}
And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.
Here is the documentation:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

ManyToOne JoinColumn (not nullable) not showing form error if `0` submitted with custom form field

I have two classes with a bidirectional relationship: Player and Team Each player must have exactly one team and each team can have many players.
When I use the default form field (select) and I submit 0 (by manually editing the HTML) the form error shows correctly. However if I use a custom form field type and submit 0 there's no form error but an exception:
Catchable Fatal Error: Argument 1 passed to
...\Entity\Player::setTeam() must be an instance of ...\Entity\Team,
null given, called in
...\vendor\symfony\symfony\src\Symfony\Component\PropertyAccess\PropertyAccessor.php
on line 360 and defined in ...\Entity\Player.php line 58
How do I show a normal form error instead of this exception?
My classes are straighforward (only the relevant parts posted):
Team.php
class Team {
/**
* #ORM\OneToMany(targetEntity="...\Entity\Player", mappedBy="team")
*/
protected $players;
}
Player.php
class Player {
/**
* #var Team
*
* #ORM\ManyToOne(targetEntity="...\Entity\Team", inversedBy="players")
* #ORM\JoinColumn(name="team_id", referencedColumnName="id", nullable=false)
* #Assert\Valid
*/
protected $team;
/**
* Set team
*
* #param Team $team
* #return Player
*/
public function setTeam(Team $team) {
$this->team = $team;
return $this;
}
/**
* Get team
*
* #return Team
*/
public function getTeam() {
return $this->team;
}
}
The reverseTransform function of my DataTransformer looks like this:
public function reverseTransform($value)
{
if(!$value)
{
return $this->multiple ? array() : 0;
}
//... get entity
}
If you have created a custom form field with a custom data transformer, it is your responsibility to validate client datas. To get the generic message (This value is not valid), you need to throw a TransformationFailedException in your data transformer. Then, everything will be handled by the form component.
EDIT: By default majority of the data transformers in the Symfony core, converts a blank value to null. The responsibility of the data transformer is to convert client data to model data and eventually to detect major error like non-acceptable value for a choice list or missing data in case of datetime field, etc. It is not to validate the data which is the resposibility of the validator component. Then, it lets the validator component make the final validation which is often more complex than a simple data transformer. IMHO, you're currently mixing data transfomer and validation concern.
EDIT: Additionally, you need to allow in your domain object what the data transformer return itself (here, the null value).

Extract constraints form Doctrine 2 entity in symfony 2

To keep the field level constraints at a central place (not replicate it in each form), I added the constraints in the entity. Like below (lets say its one of the fields of a user entity):
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=255, nullable=false)
*
* #Constraints\NotBlank(
* groups={"register", "edit"},
* message="email cannot be blank."
* )
* #Constraints\Email(
* groups={"register", "edit"},
* message="Please enter a valid email address."
* )
*
* #Expose
* #Groups({"list", "details"})
*/
private $email;
Now I need a way to expose this validation constraints for each field which is an annotation of "Symfony\Component\Validator\Constraints". Is there a way that I can get all the constraints for all fields in the entity, like:
$em->getValidationConstraints('MyBundle:EntityUser'); //em is the entity manager
//and it returns me all the fields with its name, type and any constraints
//attached to it as any array
Thanks in advance.
Gather Information
Before fixing a problem, it's good to know what you are talking about and gather some information.
Doctrine is an ORM, something that does nice things between a database and an object. It has nothing to do with validation, that is done by the Symfony2 Validator Component. So you need something else than the $em.
All constraints of a class are called 'metadata' and they are usually stored in Symfony\Component\Validator\Mapping\ClassMetadata. We have to find a class which accepts the name of a class and returns a ClassMetadata instance.
To load the constraints, the Symfony2 Validator component uses loaders.
The Solution
We can see that there is a Symfony\Component\Validator\Mapping\ClassMetadataFactory. A factory is always used to build a class from a specific argument. In this case, we know it will create a ClassMetadata and we can see that it accepts a classname. We have to call ClassMetadataFactory::getMetadataFor.
But we see it needs some loaders. We aren't going to do the big job of initializing this factory, what about using the service container? We can see that the container has a validator.mapping.class_metadata_factory service, which is exactly the class we need.
Now we have all of that, let's use it:
// ... in a controller (maybe a seperated class is beter...)
public function someAction()
{
$metadataFactory = $this->get('validator.mapping.class_metadata_factory');
$metadata = $metadataFactory->getMetadataFor('Acme\DemoBundle\Entity\EntityUser');
}
Now we have the metadata, we only need to convert that to an array:
// ...
$propertiesMetadata = $metadata->properties;
$constraints = array();
foreach ($propertiesMetadata as $propertyMetadata) {
$constraints[$propertyMetadata->name] = $property->constraints;
}
Now, $constraints is an array with all fields and their constraint data, something like:
Array (
...
[email] => Array (
[0] => <instance of NotBlank>
[1] => <instance of Email>
),
)

Categories