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

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"}}
* }) */

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 validation not working with Annotations on Embeddables

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;
}

how to make a property to be "merged" or initialized when deserializing with JMSSerializerBundle

I'm using JMSSerializer - along with the Doctrine constructor - in order to deserialize an object sent.
My (simplified) entities are the following. I omit the code I think is useless:
Widget
{
protected $id;
/**
* #ORM\OneToMany(
* targetEntity="Belka\Iso50k1Bundle\Entity\VarSelection",
* mappedBy="widget",
* cascade={"persist", "remove", "detach", "merge"})
* #Serializer\Groups({"o-all-getCWidget", "i-p2-create", "o-all-getWidget", "i-p3-create", "i-p2-editWidget"})
* #Type("ArrayCollection<Belka\Iso50k1Bundle\Entity\VarSelection>")
*/
protected $varsSelection;
}
/**
* #ORM\Entity()
*
* #ORM\InheritanceType("SINGLE_TABLE")
*
* #ORM\DiscriminatorColumn(
* name="vartype",
* type="string")
*
* #ORM\DiscriminatorMap({
* "PHY" = "PhyVarSelection"
* })
*
* #ORM\HasLifecycleCallbacks()
*/
abstract class VarSelection
{
/**
* #Id
* #Column(type="integer")
* #GeneratedValue("SEQUENCE")
* #Serializer\groups({"o-all-getCWidget", "o-all-getWidget", "i-p2-editWidget"})
*/
protected $id;
}
class PhyVarSelection extends VarSelection
{
/**
* #var PhyVar
*
* #ORM\ManyToOne(
* targetEntity="Belka\Iso50k1Bundle\Entity\PhyVar",
* cascade={"persist", "merge", "detach"})
*
* #ORM\JoinColumn(
* name="phy_var_sel",
* referencedColumnName="id",
* nullable=false)
*/
protected $phyVar;
}
class PhyVar extends Variable
{
/**
* #ORM\Column(type="string")
* #ORM\Id
*
* #Serializer\Groups({"o-p2-getCMeters", "o-all-getWidget"})
* #Assert\Regex("/(PHY)_\d+_\d+_\w+/")
*/
protected $id;
/**
* #ORM\Column(type="text", name="varname")
* #Serializer\Groups({"o-p2-getCMeters", "o-all-getWidget", "o-all-getCWidget"})
*/
protected $varName;
...
}
I try to deserialize an object that represents a Widget entity already persisted, along with which an array of varselection with their own id specified - if already persisted - and without their own id if they are new and to be persisted.
Deserialization works:
$context = new DeserializationContext();
$context->setGroups('i-p2-editWidget');
$data = $this->serializer->deserialize($content, $FQCN, 'json', $context);
but $data has always Widget::$varsSelection[]::$phyVar as a proxy class initialized, with only the id properly set. What I have to do so as to have it all is:
foreach ($data->getVarsSelection() as $varSel) {
$varSel->getVar();
}
why is that? How can have it initialized already? I don't want to spend time cycling and fetching data from DB again.
edit
I've added a domain of the entities so as to get the idea of what I'm deserializing
I figured out myself the hows and whys of this behavior:
since I'm sending a JSON like the following:
{
"id": <widgetID>,
"vars_selection": {
"id": <varSelectionID>,
"vartype": "PHY"
}
}
JMSSerializer's Doctrine ObjectConstructor simply tries to finds just two Entities: Widget and VarSelection by executing the following line:
$object = $objectManager->find($metadata->name, $identifierList);
in other words: Doctrine's EntityManager tries to find the Entity identified by its ID. Hence, well'get the unitialized proxy classes.
As far as I know, find cannot specify an hydration mode. Hence, two are the ways to handle this:
Specify fetch="EAGER" on PhyVarSelection::$phyVar. Quite costly, when we do not need it though;
Replace the ObjectConstructor by calling the repository and make a DQL, which will have the EAGER option properly set. Something like $query->setFetchMode("PhyVarSelection", "phyVar", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);

Doctrine uploadable: Multiple file upload on the same entity

I'm using the excellent doctrine extension uploadable. I can upload one file per entity just fine, but how can I upload two different files on the same entity?
* #Gedmo\Uploadable(path="uploads/articles", appendNumber=true, filenameGenerator="SHA1")
class Article
{
* #ORM\Column(name="photo", type="string", length=255)
* #Gedmo\UploadableFilePath
private $photo
* #ORM\Column(name="pdf", type="string", length=255)
* #Gedmo\UploadableFilePath
private $pdf
On my controller I have:
$uploadableManager->markEntityToUpload($article, $article->getPhoto());
$uploadableManager->markEntityToUpload($article, $article->getPdf());
Only the last file is uploaded and saved to the database. How can I do this?
You probably confused something.
You have Article entity with two fields: photo and pdf, but there is no $materia entity. You probably should change $materia to $article. But this won't work because #Uploadable cannot upload multiple files for the same entity.
Hint: use VichUploaderBundle for Doctrine file uploads handling
UPD: Here is example class.
<?php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity
* #ORM\Table(name="article")
* #Vich\Uploadable
*/
class Article
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// ..... other fields
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="article_photo", fileNameProperty="photoName")
*
* #var File
*/
private $photoFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $photoName;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="article_pdf", fileNameProperty="pdfName")
*
* #var File
*/
private $pdfFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $pdfName;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* #param \DateTime $updatedAt
* #return Article
*/
public function setUpdatedAt(\DateTime $updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $photo
*
* #return Article
*/
public function setPhotoFile(File $photo = null)
{
$this->photoFile = $photo;
if ($photo) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTime('now');
}
return $this;
}
/**
* #return File
*/
public function getPhotoFile()
{
return $this->photoFile;
}
/**
* #param string $photoName
*
* #return Article
*/
public function setPhotoName($photoName)
{
$this->photoName = $photoName;
return $this;
}
/**
* #return string
*/
public function getPhotoName()
{
return $this->photoName;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $pdf
*
* #return Article
*/
public function setPdfFile(File $pdf = null)
{
$this->pdfFile = $pdf;
if ($pdf) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTime('now');
}
return $this;
}
/**
* #return File
*/
public function getPdfFile()
{
return $this->pdfFile;
}
/**
* #param string $pdfName
*
* #return Article
*/
public function setPdfName($pdfName)
{
$this->pdfName = $pdfName;
return $this;
}
/**
* #return string
*/
public function getPdfName()
{
return $this->pdfName;
}
}
And you need to configure VichUploader this way:
# app/config/config.yml
vich_uploader:
db_driver: orm
mappings:
article_photo:
uri_prefix: /images/articles/photos
upload_destination: %kernel.root_dir%/../web/images/articles/photos
article_pdf:
uri_prefix: /images/articles/pdfs
upload_destination: %kernel.root_dir%/../web/images/articles/pdfs
Be attentive. You can get confused with configuration, mappings, methods... just read manual carefully and thoughtly. https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/usage.md

Symfony2/Doctrine2 Inheritance

I'm attempting to accomplish BASIC inheritance in Doctrine 2, but I'm running into several major issues. Such a task should not be so complicated. Let's get down to business...
I have three classes, BaseFoodType, Drink, and Snack. My BaseFoodType has the following class definition:
/** #ORM\MappedSuperclass */
class BaseFoodType {
/**
* #ORM\Column(type="integer", length=7)
*/
public $budget = 0;
}
Which follows the instructions for inheritance on the doctrine website: http://docs.doctrine-project.org/en/2.0.x/reference/inheritance-mapping.html
Here is what the sub-classes look like prior to generating my entities:
namespace MySite\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* MySite\MainBundle\Entity\EventDrink
*
* #ORM\Table(name="drink")
* #ORM\Entity
*/
class Drink extends BaseFoodType {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="integer", length=5, nullable=true)
*/
public $people_count;
}
Both Drink, and Snack inherit from this base class but I'm running into numerous issues when attempting to build my entities using the doctrine:generate:entities command. First, Symfony inserts a private "budget" property into each subclass, along with getters and setters (THIS DEFEATS THE PURPOSE INHERITANCE)
/**
* #var integer
*/
private $budget;
/**
* Set budget
*
* #param integer $budget
*/
public function setBudget($budget)
{
$this->budget = $budget;
return $this;
}
/**
* Get budget
*
* #return integer
*/
public function getBudget()
{
return $this->budget;
}
Second, I'm getting a fatal error:
Fatal error: Access level to MySite\MainBundle\Entity\Drink::$budget
must be public (as in class MySite\MainBundle\Entity\BaseFoodType) in
C:\xampp\htdocs\MySite\src\MySite\MainBundle\Entity\Drink.php on line
197
I could probably make the generated properties public and be on my way, but again, that defeats the purpose of inheritance!
Thanks in advance for any insight.
Doctrine provides the means to specify the visibility of generated fields. Either protected or private. The default is private.
The problem is that the Symfony command that invokes Doctrine offers no way to change this.
Creating your own subclass of the standard Symfony command will allow you more control over the generation process. This might help you along.
namespace Foo\Bundle\FooBundle\Command;
use Doctrine\Bundle\DoctrineBundle\Command as DC;
use Doctrine\ORM\Tools\EntityGenerator;
class GenerateEntitiesDoctrineCommand extends DC\GenerateEntitiesDoctrineCommand
{
protected function configure()
{
parent::configure();
$this->setName('foo:generate:entities');
}
/**
* get a doctrine entity generator
*
* #return EntityGenerator
*/
protected function getEntityGenerator()
{
$entityGenerator = new EntityGenerator();
$entityGenerator->setGenerateAnnotations(true);
$entityGenerator->setGenerateStubMethods(true);
$entityGenerator->setRegenerateEntityIfExists(false);
$entityGenerator->setUpdateEntityIfExists(true);
$entityGenerator->setNumSpaces(4);
$entityGenerator->setAnnotationPrefix('ORM\\');
$entityGenerator->setFieldVisibility($entityGenerator::FIELD_VISIBLE_PROTECTED);
return $entityGenerator;
}
}
This does two things. It sets the property visibility to protected. This prevents php errors.
$entityGenerator->setFieldVisibility($entityGenerator::FIELD_VISIBLE_PROTECTED);
It also copies the annotations from mapped super class into the entity class.
$entityGenerator->setGenerateAnnotations(true);
Here's some example code where properties are inherited from a base class and their visibility and annotations copy correctly into the inheriting class
/** #ORM\MappedSuperclass */
class DataSuper {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Campaign", inversedBy="data")
* #ORM\JoinColumn(name="campaign_id", referencedColumnName="id")
* #Exclude
*/
protected $campaign;
/**
* #ORM\Column(type="text", nullable=true, name="data")
*/
protected $data;
/**
* #ORM\Column(type="datetime")
*/
protected $createdDate;
}
/**
* #ORM\Entity(repositoryClass="Foo\Bundle\FooBundle\Entity\DataRepository")
* #ORM\Table(name="data")
* #ExclusionPolicy("none")
*/
class Data extends DataSuper
{
}
After generation the Data class looks like:
class Data extends DataSuper
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="data", type="text", precision=0, scale=0, nullable=true, unique=false)
*/
protected $data;
/**
* #var \DateTime
*
* #ORM\Column(name="createdDate", type="datetime", precision=0, scale=0, nullable=false, unique=false)
*/
protected $createdDate;
/**
* #var \Foo\Bundle\FooBundle\Entity\Campaign
*
* #ORM\ManyToOne(targetEntity="Foo\Bundle\FooBundle\Entity\Campaign", inversedBy="data")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="campaign_id", referencedColumnName="id", nullable=true)
* })
*/
protected $campaign;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set data
*
* #param string $data
* #return Data
*/
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* Get data
*
* #return string
*/
public function getData()
{
return $this->data;
}
/**
* Set createdDate
*
* #param \DateTime $createdDate
* #return Data
*/
public function setCreatedDate($createdDate)
{
$this->createdDate = $createdDate;
return $this;
}
/**
* Get createdDate
*
* #return \DateTime
*/
public function getCreatedDate()
{
return $this->createdDate;
}
/**
* Set campaign
*
* #param \Foo\Bundle\FooBundle\Entity\Campaign $campaign
* #return Data
*/
public function setCampaign(\Foo\Bundle\FooBundle\Entity\Campaign $campaign = null)
{
$this->campaign = $campaign;
return $this;
}
/**
* Get campaign
*
* #return \Foo\Bundle\FooBundle\Entity\Campaign
*/
public function getCampaign()
{
return $this->campaign;
}
}
And the table structure is correct once you do:
php app/console doctrine:schema:update --force
The exception is being thrown because BaseFoodType::budget is a public property and doctrine:generate:entities created a private property in your Drink / Snack classes extending BaseFoodType ( which is not correct but the way the command works by now ).
Property visibility in a subclass can only be the same level or more liberate ( private -> protected -> public ) but never more restrictive.
doctrine:generate:entities did not take superclass's public property into account when generating the getters/setters as the implementation with a public property is non-standard.
Therefore you will have to adjust the generated class manually.
I recommend using private/protected properties combined with getters & setters.

Resources