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

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;

Related

How to automatically filter embedding relations data?

I have 2 simple entity related between themselves:
<?php
declare(strict_types=1);
namespace App\Entity;
// use ... ;
/**
* #ApiResource(
* subresourceOperations={
* // ...
* },
* // ... ,
* normalizationContext={"groups" = {"category:read"}}
* )
* # ...
*/
class Category
{
// ...
/**
* #ORM\OneToMany(targetEntity="Object", mappedBy="category")
* #ApiSubresource
* #Groups({"category:read"})
*/
private $objects;
// ...
public function getObjects(): Collection
{
return $this->objects;
}
}
And
<?php
declare(strict_types=1);
namespace App\Entity;
// use ... ;
/**
* #ApiResource(
* // ...
* )
* # ...
*/
class Object
{
// ...
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category")
* #ORM\JoinColumn(nullable=false)
* #Groups({"object:read", "object:write"})
*/
private $category;
/**
* #ORM\Column(type="boolean")
*/
private $isActive = true;
/**
* #ORM\Column(type="string", length=255, nullable=true)
* #Groups({"object:read", "object:write", "category:read"})
*/
private $name;
// ...
}
I want to get by /api/сategories/{id} related data Objects, where isActive = true. Now I get all the records.
I tried to use QueryItemExtensionInterface andQueryCollectionExtensionInterface, but I still get all the records. fetch ="EAGER" also didn't help me
I clarify, I do not want to get this by subresource /api/categories/{id}/objects. I want to get it using /api/сategories/{id}
I also want this to depend on the role of the user. So that the admin can get all the records and the user only isActive = true
Any ideas how to do this?
Use the Boolean Filter with categories?objects.isActive=1. This filter must be set up on the relation property as described here.

Symfony3 Edit Entity : Error Missing value for primary key

I try to create a tree structure for a catalog of products.
A catalog can have multiple levels and levels can contain multiple products.
I manage to save my structure in database but when I want to edit it, I have this error :
Error : Missing value for primary key catalogCode on AppBundle\Entity\CatalogLevel
at OutOfBoundsException ::missingPrimaryKeyValue ('AppBundle\Entity\CatalogLevel', 'catalogCode')
in vendor\doctrine\common\lib\Doctrine\Common\Proxy\AbstractProxyFactory.php at line 125
when I do this in my CatalogController :
$form = $this->createForm(CatalogTreeType::class, $catalog);
But, just before that line, I verify if I get my levels correctly and it's looking like that's the case :
// Create an ArrayCollection of the current levels
$originalLevels = new ArrayCollection();
foreach ($catalog->getLevels() as $level) {
var_dump($level->getCatalogCode());
$originalLevels->add($level);
}
// returns
AppBundle\Controller\CatalogController.php:337:string 'TT-FTEST' (length=8)
AppBundle\Controller\CatalogController.php:337:string 'TT-FTEST' (length=8)
CatalogLevel entity has a composite key : levelId + catalogCode.
Considering the primary key catalogCode isn't empty, I don't understand this error...
Catalog Entity
/**
* #ORM\Table(name="catalogue")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogRepository")
* #UniqueEntity(fields="code", message="Catalog code already exists")
*/
class Catalog
{
/**
* #ORM\Column(name="Catalogue_Code", type="string", length=15)
* #ORM\Id
* #Assert\NotBlank()
* #Assert\Length(max=15, maxMessage="The code is too long ({{ limit }} characters max)")
*/
private $code;
/**
* #ORM\OneToMany(targetEntity="CatalogLevel", mappedBy="catalog", cascade={"persist", "remove"})
* #Assert\Valid
*/
private $levels;
/**
* Constructor
*/
public function __construct()
{
$this->levels = new ArrayCollection();
}
/**
* Get levels
*
* #return ArrayCollection
*/
public function getLevels()
{
return $this->levels;
}
/**
* Add level
*
* #param \AppBundle\Entity\CatalogLevel $level
*
* #return Catalog
*/
public function addLevel(\AppBundle\Entity\CatalogLevel $level)
{
$level->setCatalogCode($this->getCode());
$level->setCatalog($this);
if (!$this->getLevels()->contains($level)) {
$this->levels->add($level);
}
return $this;
}
/**
* Remove level
*
* #param \AppBundle\Entity\CatalogLevel $level
*/
public function removeLevel(\AppBundle\Entity\CatalogLevel $level)
{
$this->levels->removeElement($level);
}
}
CatalogLevel Entity
/**
* #ORM\Table(name="catalogue_niveau")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogLevelRepository")
*/
class CatalogLevel
{
/**
* #ORM\Column(name="Niveau_ID", type="string", length=15)
* #ORM\Id
*/
private $id;
/**
* #ORM\Column(name="Catalogue_Code", type="string", length=15)
* #ORM\Id
*/
private $catalogCode;
/**
* #ORM\ManyToOne(targetEntity="Catalog", inversedBy="levels")
* #ORM\JoinColumn(name="Catalogue_Code", referencedColumnName="Catalogue_Code")
*/
private $catalog;
/**
* Set id
*
* #param string $id
*
* #return CatalogLevel
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get id
*
* #return string
*/
public function getId()
{
return $this->id;
}
/**
* Set catalogCode
*
* #param string $catalogCode
*
* #return CatalogLevel
*/
public function setCatalogCode($catalogCode)
{
$this->catalogCode = $catalogCode;
return $this;
}
/**
* Get catalogCode
*
* #return string
*/
public function getCatalogCode()
{
return $this->catalogCode;
}
}
I would like to remind you that this error occured on the editAction (it works very well on the addAction) when I display the pre-filled form.
Thanks for your help !
I think that is because you haven't autoincrement id in entity CatalogLevel. Try add to id this code:
#ORM\GeneratedValue(strategy="AUTO")
You have some problems in the way you've created you Entities. You should use auto generate strategy. Also the "#ORM\Id" annotation is the unique identifier.
Also, your "JoinColumn" is incorrect. You need to refer back to the "Catalog" Entity, and it's id (identifier). There is no need for 2 "#ORM\Id" entries in the class CatalogLevel.
So make these changes:
/**
* #ORM\Table(name="catalog")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogRepository")
*/
class Catalog
{
/**
* #ORM\Column(name="cat_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $cat_id;
/**
* #ORM\OneToMany(targetEntity="CatalogLevel", mappedBy="catalog", cascade={"persist", "remove"})
* #Assert\Valid
*/
private $levels;
...
/**
* #ORM\Table(name="catalog_level")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogLevelRepository")
*/
class CatalogLevel
{
/**
* #ORM\Column(name="cat_level_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $cat_level_id;
/**
* #ORM\ManyToOne(targetEntity="Catalog", inversedBy="levels")
* #ORM\JoinColumn(name="local_cat_id", referencedColumnName="cat_id")
*/
private $catalog;
...

Symfony2 conditional form validation

I have some difficulties about applying validation for only one associated entity.
So I have two entities, News and NewsTranslation. A news could be translated in multiple languages. But I would like to apply validation only if locale is en.
// AppBundle/Entity/News.php
class News
{
use ORMBehaviors\Translatable\Translatable;
use ORMBehaviors\Timestampable\Timestampable;
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="status", type="smallint")
* #Assert\NotBlank
*/
private $status;
...
}
// AppBundle/Entity/NewsTranslation.php
class NewsTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=true)
* #Assert\NotBlank
* #Assert\Length(max = 255)
*/
private $title;
/**
* #var string
*
* #ORM\Column(name="text", type="string", nullable=true)
* #Assert\NotBlank
*/
private $text;
}
# AppBundle/Resources/config/validation.yml
AppBundle\Entity\News:
properties:
translations:
- Valid: ~
I tried to use a Closure for the validation_groups form option. But it looks like Symfony do validation on News entity and Valid constraint apply the same groups on NewsTranslation.
I know I could use Callback constraint but that's mean to redo NotBlank, Length and other exiting constraints by myself. And I would like to avoid it if possible.
EDIT:
I'm using Symfony 2.8.*
I try using an en validation group. But looks like the validation is launch on News entity with validation_groups. And with Valid constraint the en validation group is given to validate NewsTranlation. So even it's the en or fr translation the group change nothing in this case.
I also try using the validation medatada through an #Assert\Callback or by using loadValidatorMetadata method into NewsTranslation entity. And the problem stay similar. I can't apply an constraint for a specific entity of collection.
I finally found a way by creating a custom validator.
Like this I could use core constraints easily.
In the translation entity, I could use my validator like this:
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=true)
* #Assert\Length(max = 255)
* #AppAssert\ValidTranslation(locales = {"fr"}, constraints = {
* #Assert\NotBlank
* })
*/
private $title;
And the validator:
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraints\Composite;
/**
* #Annotation
* #Target({"PROPERTY", "METHOD", "ANNOTATION"})
*
* #author Nicolas Brousse
*/
class ValidTranslation extends Composite
{
public $locales = array();
public $constraints = array();
public function getCompositeOption()
{
return 'constraints';
}
public function getRequiredOptions()
{
return array('locales', 'constraints');
}
}
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* #author Nicolas Brousse
*/
class ValidTranslationValidator extends ConstraintValidator
{
/**
* If property constraint
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidTranslation) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\ValidTranslation');
}
if (false) { // #todo check by interface or trait
throw new UnexpectedTypeException($value, 'not a translation entity');
}
$context = $this->context;
$entity = $this->context->getObject();
if (in_array($entity->getLocale(), $constraint->locales)) {
$context = $this->context;
if ($context instanceof ExecutionContextInterface) {
$validator = $context->getValidator()->inContext($context);
$validator->validate($value, $constraint->constraints);
} else {
// 2.4 API
$context->validateValue($value, $constraint->constraints);
}
}
}
}
you form need to return 2 validations_groups, "Default" and the validation group corresponding to the "en" locale

Symfony validation: First property constraint then class constraint

I'm using Symfony 2.5 and my Model class is the following:
/**
* #UserAssert\UserPasswordReset
*/
class ResetPassword {
/**
* #var string
* #Assert\NotBlank()
*/
public $username;
/**
* #var string
* #Assert\NotBlank()
* #Assert\Date
*/
public $birthday;
/**
* #var string
* #Assert\NotBlank()
*/
public $plainSecurityAnswer;
function __toString()
{
return $this->username . $this->birthday->format('Y-m-d H:i:s') . $this->plainSecurityAnswer;
}
}
This Model is mapped to a ResetFormType.
Now my intention: How can i say / configure, that i first want the property constraints to be passed. And if all property constraints are passed (e.g. no field is blank), i want the #UserAssert\UserPasswordReset to be called.
At the moment, it always validates the property AND the class constraints.
Regards ++
I think you can do it using a GroupSequence Validator like this:
/**
* #UserAssert\UserPasswordReset(groups={"PasswordReset"})
* #Assert\GroupSequence({"Default", "PasswordReset"})
*/
class ResetPassword
{
//----
}
In this mode UserPasswordReset will be validated only after the Defaults Asserts.
In the docs you will find some implementations example to use groups sequences..

Assert unique validation in Sonata Admin

I'm using Symfony 2.1 for a project. I use SonataAdminBundle for administration usage.
i want to add an assert to my slug property in my admin class.. how can i do this?
in my entity i had set the assertion but it seems that it doesn't work here :(
related codes:
the entity :
/*
* #ORM\Table(name="default_doctor_specialty")
* #UniqueEntity("uniqueSlugName")
* #ORM\Entity
*/
class Test {
//..
/**
* #var string
* #Gedmo\Slug(fields={"name"},unique=false)
*
* #ORM\Column(name="unique_slug_name", type="string", length=255, nullable=false , unique=true)
*/
private $uniqueSlugName;
}
in admin class:
class TestAdmin extends Admin {
protected $formOptions = array(
'validation_groups' => 'Default'
);
//...
}
why the default validation doesn't work???
& even if doesn't work like this how can i set the unique validation inside admin class ???
thanks for your answers :)
finally i defined a validation group for my entity:
use Symfony\Bridge\Doctrine\Validator\Constraints as DoctrineAssert;
/*
* #ORM\Table(name="default_doctor_specialty")
* #DoctrineAssert\UniqueEntity(fields="uniqueSlugName", message="A Speciality with same slug already exists", groups={"test"})
* #ORM\Entity
*/
class Test {
//..
/**
* #var string
* #Gedmo\Slug(fields={"name"},unique=false)
*
* #ORM\Column(name="unique_slug_name", type="string", length=255, nullable=false , unique=true)
*/
private $uniqueSlugName;
}
and in admin class i used test validation group instead of default!
thanks to AHWEBDEV on github!
From this link
This is the full exemple , it depend on your symfony and sonata version.
// src/AppBundle/Entity/Service.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity
* #UniqueEntity(
* fields={"host", "port"},
* errorPath="port",
* message="This port is already in use on that host."
* )
*/
class Service
{
/**
* #ORM\ManyToOne(targetEntity="Host")
*/
public $host;
/**
* #ORM\Column(type="integer")
*/
public $port;
}
I prefer not to mess my entities with hundreds of lines of such low level details like validation. One can define validation rules inside the Admin class. Usually the validation rules are different for admins than for clients as well.
final class TestAdmin
{
// … skipped for brevity
public function validate(ErrorElement $errorElement, $object)
{
$errorElement->addConstraint(new UniqueEntity(['fields' => ['slug']]));
}
}

Resources