Symfony Doctrine validation not working with Annotations on Embeddables - symfony

I just wanted to make sure that my #Assert\NotBlank on a field in an Embeddable works inside a phpunit-test (using Doctrine 2.7 and Symfony 5.1) but it seems that no check is being made.
To make sure that it's not about the test-env I tested the same thing with an injected validator
on a route on a test-server with the same results. Violations to Assertions on the embedding entity work just fine.
Any suggestions why this might be the case?
So basically:
/**
* #package App\Entity\Embeddables
* #ORM\Embeddable
*/
class MyEmbeddable
{
/**
* #var string
* #ORM\Column(type="string", nullable=false)
* #Assert\NotBlank
*/
private string $text;
}
with
/**
* #ORM\Entity(repositoryClass=MyThingRepository::class)
*/
class MyThing
{
//..ID-stuff
/**
* #var MyEmbeddable
* #ORM\Embedded(class="App\Entity\Embeddables\MyEmbeddable")
*/
private MyEmbeddable $embeddableTestVar;
}
would result in:
$myThing = new MyThing();
$validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator();
dd($validator->validate($myThing));
printing an empty array.
Any help would be greatly appreciated!

Try to add the #Assert\Valid annotation
/**
* #ORM\Entity(repositoryClass=MyThingRepository::class)
*/
class MyThing
{
//..ID-stuff
/**
* #Assert\Valid
*
* #var MyEmbeddable
* #ORM\Embedded(class="App\Entity\Embeddables\MyEmbeddable")
*/
private MyEmbeddable $embeddableTestVar;
}

Related

SYMFONY, API PLATFORM how to add edit and show links to the serialized object

I'm working with SYMFONY and API PLATFORM to create REST API.
I have a Project Entity as an API Resource :
class Project
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $reference;
/**
* #ORM\Column(type="string", length=255, unique=true)
* #Gedmo\Slug(fields={"reference"})
*/
private $slug;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="create")
*/
private $createdAt;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="update")
*/
private $updatedAt;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="projects")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity=Type::class, inversedBy="projects")
* #ORM\JoinColumn(nullable=false)
*/
private $type;
/**
* #ORM\ManyToOne(targetEntity=Status::class, inversedBy="projects")
* #ORM\JoinColumn(nullable=false)
*/
private $status;
With postman i get :
How can i add edit and show route to get a serialized object like this :
"hydra:member": [
{
...
"status": "/api/statuses/6",
"edit": "<a href='link_to_edit'>edit</a>", // add a link to edit
"show": "<a href='link_to_show'>show</a>" // add a link to show
},
knowing that i don't want to add edit and show to the entity properties or mapped them
Thanks for the help
Technically, you already have your edit and show routes (if you didn't customize them) : you only have to make a PUT or GET request to the value of the #id field of each object.
If you want to add an extra property to your entity, that isn't mapped you can do something like this :
/**
* #SerializedName("edit_route")
*
* #Groups({"projects:read"}))
*
* #return string
*/
public function getEditRoute()
{
return 'your_edit_route';
}
I wouldn't return HTML in this kind of field though, especially if your route is anything else than GET, and apps that use you API might not use HTML, so you're better off returning the simplest value and letting them do their thing with it.

Symfony/Doctrine: Accessing unmapped(?) data within entity

in Symfony (5.3.7 at present) I've got main data-entities and settings seperated. For example there is a user entity (default stuff), a TypeUserSetting defining the different settings and UserSetting which is m:n in between with the current setting stored.
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass=TypeUserSettingRepository::class)
*/
class TypeUserSetting
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=200)
*/
private $description;
/**
* #ORM\OneToMany(targetEntity=UserSetting::class, mappedBy="setting")
*/
private $userSettings;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $default_value;
and
class UserSetting
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=4000, nullable=true)
*/
private $value;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="userSettings")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity=TypeUserSetting::class, inversedBy="userSettings",cascade={"persist"})
* #ORM\JoinColumn(nullable=false)
*/
private $setting;
Thats not that complicated so far...
My problem is, that oftenly settings don't exist, because the users did not set them. In that case I want to use the default.
class User ...
/**
* #ORM\OneToMany(targetEntity=UserSetting::class, mappedBy="user", orphanRemoval=true)
*/
private $userSettings;
...
public function getSettingById(int $id):string
{
foreach ($this->getUserSettings() as $oneSetting) {
if ($oneSetting->getId() === $id)
return $oneSetting->getValue();
}
//Not set, return the default
....
}
And here we go... If there is no setting, the loop fails and I want to get the default form the coresponding TypeUserSetting. This is not mapped, so I have to get it from the database, but I didn't find a way to access that properly.
Possible solutions I found that far:
Insert the UserSetting for all users by SQL when adding a TypeUserSetting. This would avoid the whole problem, but I simply don't like that.
Adding a static method to the TypeUserSetting-repo to get the value. I think that's ugly and somehow going back to last century...
Inject the TypeUserSetting-repo by LifeCycle-hooks which (in my oppinion) isn't the way entities should be used.
Injecting the repo from the controller that calls the function... I think this would be the opposite of encapsulation and separation of concerns. (and I think about hitting my head to the wall, just for having this kind of thoughts)
Anybody got a good idea to solve that?
Thanks in advance

Symfony OneToMany with associative array : new row inserted instead of update

I have to internationalize an app and particularly an entity called Program. To do so, I created an other entity ProgramIntl which contains a "locale" attribute (en_GB, fr_FR, etc) and strings which must be internationalized. I want the programIntl attribute in Program to be an associative array (with locale as key).
We have an API to read/write programs. GET and POST works fine but when I want to update data (PUT), the programIntl is not updated: an insert query is launched (and fails because of the unique constraint, but that's not the question).
Here is the code:
In Program.php:
/**
* #var
*
* #ORM\OneToMany(targetEntity="ProgramIntl", mappedBy="program", cascade={"persist", "remove", "merge"}, indexBy="locale", fetch="EAGER")
* #ORM\JoinColumn(nullable=false, onDelete="cascade")
* #Groups({"program_read", "program_write"})
*/
private $programIntl;
public function addProgramIntl($programIntl)
{
$this->programIntl[$programIntl->getLocale()] = $programIntl;
$programIntl->setProgram($this);
return $this;
}
public function setProgramIntl($programIntls)
{
$this->programIntl->clear();
foreach ($programIntls as $locale => $programIntl) {
$programIntl->setLocale($locale);
$this->addProgramIntl($programIntl);
}
}
public function getProgramIntl()
{
return $this->programIntl;
}
In ProgramIntl.php:
/**
* #ORM\Entity(repositoryClass="App\Repository\ProgramIntlRepository")
* #ORM\Table(name="program_intl",uniqueConstraints={#ORM\UniqueConstraint(name="program_intl_unique", columns={"program_id", "locale"})})
*/
class ProgramIntl
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups({"program_read", "program_write"})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Program", inversedBy="programIntl")
* #ORM\JoinColumn(nullable=false)
*/
private $program;
/**
* #ORM\Column(type="string", length=5, options={"fixed" = true})
*/
private $locale;
/**
* #ORM\Column(type="string", length=64)
* #Assert\NotBlank()
* #Groups({"program_read", "program_write"})
*/
private $some_attr;
/* ... */
}
Any idea of what could be the reason of the "insert" instead of "update" ?
Thanks
I forgot to mention that we use api-platform.
But I found the solution myself. In case anyone is interested, adding the following annotation to classes Program and ProgramIntl solved the problem:
/* #ApiResource(attributes={
* "normalization_context"={"groups"={"program_read", "program_write"}},
* "denormalization_context"={"groups"={"program_read", "program_write"}}
* }) */

Doctrine doesn't update/generate fields of ManyToOne and OneToMany

I have a superclass that currently works fine (all relations and properties are updating to the database)
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\JoinColumn;
use JMS\Serializer\Annotation as JMS;
/**
* Document
*
* #Table(name="document")
* #Entity(repositoryClass="AcmeBundleDocumentRepository")
*/
class Document
{
/**
* #var string
*
* #Column(name="id", type="string")
* #Id
* #GeneratedValue(strategy="UUID")
*/
protected $id;
/**
* #var string
* #Column(name="name", type="string", length=255)
*/
protected $name;
/**
* #var string
* #Column(name="type", type="string", length=255)
*/
protected $type;
/**
* #var boolean
* #Column(name="has_attachments", type="boolean")
*/
protected $hasAttachments;
/**
* #ManyToOne(targetEntity="Delivery")
* #JoinColumn(name="delivery_id", referencedColumnName="id", nullable=false)
* #JMS\Exclude()
*/
protected $delivery;
/**
* #OneToMany(targetEntity="Extension", mappedBy="document", cascade={"persist","remove"})
**/
protected $extensions;
public function __construct()
{
$this->extensions = new ArrayCollection();
}
/* getter and setters */
}
Now I've created a entity called Note that extends to Document entity
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Entity;
/**
* Note
*
* #Table(name="note")
* #Entity(repositoryClass="NoteRepository")
*/
class Note extends Document
{
}
I am suppose that the table/entity note should generate the same things of the class that extends. But not do it
I run php bin/console doctrine:schema:update -f
this only generates properties and not FK (foreing Keys), in this case #ManyToOne and #OneToMany.
Additionally maybe help us, i have those entities on the same database
I am doing something wrong ?
As per docs I think you're missing the #MappedSuperclass annotation or you're using Doctrine inheritance in the wrong way. Be aware that a MappedSupperClass is not an entity by itself instead is just a class for share common methods and properties among it is children classes (same inheritance concept that you should already know).
/**
* #MappedSuperclass
*/
class DocumentSuperClass
{
...
}
/**
* #Table(name="document")
* #Entity(repositoryClass="AcmeBundleDocumentRepository")
*/
class Document extends DocumentSuperClass
{
...
}
/**
* #Table(name="note")
* #Entity(repositoryClass="NoteRepository")
*/
class Note extends DocumentSuperClass
{
...
}

Best practice - Check if Entity exist before presist

Whats the best practice to check if entity fields exist before persisting it.
Here's the example
Entity
class Pile{
/**
* #var \ABC\CoreBundle\Entity\Record
*
* #ORM\OneToMany(targetEntity="Record")
*
*/
private $records;
/**
* #var \CSC\CoreBundle\Entity\Project
*
* #ORM\ManyToOne(targetEntity="Project")
*
*/
private $project;
/**
* #var string
*
* #ORM\Column(name="Block", type="string", length=255)
*/
private $block;
/**
* #var string
*
* #ORM\Column(name="Type", type="string", length=255)
*/
private $type;
}
class Record{
/**
* #var \CSC\CoreBundle\Entity\Pile
*
* #ORM\ManyToOne(targetEntity="Pile")
*
*/
private $records;
}
There are two controllers that handle the CRUD of Pile and Records.
To create Pile there must not be any duplicate fields [project, block, type]
In Record Controllers I could create Pile together with Record.
Here's the problem where and when do I check the db if a similar Pile entity is created?
Whats the Best Practice?
Copy and paste the query checker in both controller?
Can I use $form->valid() to perform any check in PileType class?
Must I use a service and have both controller to call the service?
In entity life-cycle use pre-insert?
Thanks
Therefore, the fields must be unique?
If so, then it is very simple: UniqueEntity
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
...
/**
* #ORM\Entity
* #UniqueEntity(
* fields={"project", "block", "type"}
* )
*/
class Pile{
/**
* #var \ABC\CoreBundle\Entity\Record
*
* #ORM\OneToMany(targetEntity="Record")
*
*/
private $records;
/**
* #var \CSC\CoreBundle\Entity\Project
*
* #ORM\ManyToOne(targetEntity="Project")
*
*/
private $project;
/**
* #var string
*
* #ORM\Column(name="Block", type="string", length=255, unique=true)
*/
private $block;
/**
* #var string
*
* #ORM\Column(name="Type", type="string", length=255, unique=true)
*/
private $type;
}
You can use a custom validation constraint in your form, so that $form->isValid() will do the check.
Follow this documentation entry on How to create a Custom Validation Constraint to create the custom validator and then inject doctrine into it to do the check.
UPDATE: Well, I didn't know there was an UniqueEntity Constraint already included in Symfony.
To inject doctrine do the following:
services:
validator.unique.unique_pile:
class: ABC\CoreBundle\Validator\Constraints\UniquePileValidator
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: validator.constraint_validator, alias: unique_pile }
The validator class might then look like this:
// src/ABC/CoreBundle/Validator/Constraints/UniquePileValidator.php
namespace ABC\CoreBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class UniquePileValidator extends ConstraintValidator
{
protected $em;
function __construct($em) {
$this->em = $em;
}
public function validate($value, Constraint $constraint)
{
$repo = $this->em->getRepository('ABC\CoreBundle\Entity\Record');
$duplicate_project = $repo->findByProject($value);
$duplicate_block = $repo->findByBlock($value);
$duplicate_type = $repo->findByType($value);
if ($duplicate_project || $duplicate_block || $duplicate_type) {
$this->context->addViolation(
$constraint->message,
array('%string%' => $value)
);
}
}
}
And to be complete, the constraint class:
// src/ABC/CoreBundle/Validator/Constraints/ContainsAlphanumeric.php
namespace ABC\CoreBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class ContainsAlphanumeric extends Constraint
{
public $message = 'This Pile already exists!';
public function validatedBy()
{
return 'unique_pile';
}
}
Should be nearly copy/pasteable...

Resources