Symfony 2 - Get additional fields in Many-to-Many relationship - symfony

I have a Many to Many relationship in Symfony 2. Diagram below.
http://i.stack.imgur.com/x6AYs.png
From Tema Entity i can get all Topico records related using this ORM definition
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Application\RiesgoBundle\Entity\Topico", inversedBy="tema")
* #ORM\JoinTable(name="tema_topico",
* joinColumns={
* #ORM\JoinColumn(name="tema", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="topico", referencedColumnName="id")
* }
* )
*/
private $topico;
And using this method
/**
* Get topico
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTopico()
{
return $this->topico;
}
However, i don't know how to access to "impacto" and "ocurrencia" values stored on tema_topico table. Is there a way to do this using the Entity Manager?

On a ManyToMany association, you cant store extra fields. To achieve a "ManyToMany" with extra fields, you will have to do a OneToMany - ManyToOne. It will give you something like.
class Tema {
/*
* #ORM\OneToMany(targetEntity="temaTopico", mappedBy="tema")
*/
private $temaTopico;
}
class Topico {
/*
* #ORM\OneToMany(targetEntity="temaTopico", mappedBy="topico")
*/
private $temaTopico;
}
class TemaTopico {
/*
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Tema", inversedBy="temaTopico")
*/
private $tema;
/*
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Topico", inversedBy="temaTopico")
*/
private $topico;
/*
* #ORM\Column(name="impacto", type="string")
*/
private $impacto;
}
see this and this

Related

Api platform, react admin - create new Entity with relation

I am afraid I might have ran into some sort of XY problem...
I have an entity "Asset" with related "AssetType" entity (One AssetType can have many Asset entities)
When creating new entity with POST method, the request fails with "SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'type_id' cannot be null"
Data posted from react-admin (POST to /api/assets route):
{
"data":{
"type":"assets",
"attributes":{
"name":"asdf",
"description":"LoraWAN enabled sensor"
},
"relationships":{
"asset_type":{
"data":{
"id":"/api/asset_types/a71b47b8-b9fb-11ea-b4d5-e6b986f12daf",
"type":"asset_types"
}
}
}
}
}
I understand that there is data lost somewhere doing deserialization of object, but cannot figure out where. Also I have identical set of entities (Gateway and Location where each Location can have multiple Gateways) and the creation of new entities work as expected...
New to Symfony & api-platform, any help appreciated.
Asset entity is set tup to be visible in api-platform:
/**
* #ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "delete"},
* normalizationContext={"groups"={"read"}},
* denormalizationContext={"groups"={"write"}}
* )
* #ORM\Entity(repositoryClass="App\Repository\AssetRepository")
*/
class Asset
{
/**
* #ORM\Id
* #ORM\Column(type="uuid_binary_ordered_time", nullable=false, unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
* #Groups({"read"})
*/
private $uuid;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\AssetType", inversedBy="assets", cascade={"persist"})
* #ORM\JoinColumn(name="type_id", referencedColumnName="uuid", nullable=false)
*
* #Groups({"read", "write"})
*/
private $assetType;
}
AssetType entity:
/**
* #ApiResource(
* normalizationContext={"groups"={"read"}},
* denormalizationContext={"groups"={"write"}}
* )
* #ORM\Entity(repositoryClass="App\Repository\AssetTypeRepository")
*/
class AssetType
{
/**
* #ORM\Id()
* #ORM\Column(name="uuid", type="uuid_binary_ordered_time", nullable=false, unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
* #Groups({"read", "write"})
*/
private $uuid;
/**
* #ORM\Column(type="string", length=255, nullable=true)
* #Groups({"read", "write"})
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Asset", mappedBy="assetType")
*/
private $assets;
public function __construct()
{
$this->assets = new ArrayCollection();
}
public function getUuid()
{
return $this->uuid;
}
public function setUuid($uuid): self
{
$this->uuid = $uuid;
return $this;
}
public function getAssets(): Collection
{
return $this->assets;
}
public function addAsset(Asset $asset): self
{
...
}
public function removeAsset(Asset $asset): self
{
...
}
In case anyone sumbles across similar problem - the reason for missing values was property naming and its normalization.
Relationship data posted contains key "asset_type" which needs to be converted to camelCase "assetType" in react-admin's dataProvider (that's the approach I took).

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

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

M:N relation with more fields

So far the relations M:N I've built are simple intermediate tables where Doctrine does not need to create an entity for this table.
I have two entities Product and ingredient, they have a relationship M:N easily describe with Doctrine as follows. but the real problem is when i need store a amount field in the relation (I need to list the ingredients and also the amount).
How can solve this?
class Product {
//...
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\Ingredient", inversedBy="product")
* #ORM\JoinTable(name="product_ingredient",
* joinColumns={
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="ingredient_id", referencedColumnName="id")
* }
* )
*/
private $ingredient;
//...
class Ingredient {
// ...
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\Product", mappedBy="ingredient")
*/
private $product;
// ...
You can't do it without intermediate entity really, that's why doctrine docs says that ManyToMany relationships are rare.
It's also the easiest thing to do, just add RecipeItem entity which will store information about Ingredient and amount and link it with relationship of ManyToOne to Product
Edit
Since I was asked to provide an example:
class Product {
//...
/**
* #ORM\OneToMany(targetEntity="RecipeItem", mappedBy="product")
*/
private $ingredients;
//...
class RecipeItem {
// ...
/**
* #ManyToOne(targetEntity="Product", inversedBy="ingredients")
**/
private $product;
/**
* #ManyToOne(targetEntity="Ingridient")
**/
private $ingredient;
/**
* #Column(type="decimal")
**/
private $amount;
}
class Ingredient {
// Don't use bidirectional relationships unless you need to
// it impacts performance
}
Now having a product you can simply:
foreach($product->getIngridients() as $item){
echo "{$item->getAmount()} of {$item->getIngridient()->getName()}";
}

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