Polymorphic associations in doctrine 2? - doctrine

I need a concrete sample of code with doctrine 2 that uses "polymorphic associations".
Let me clarify myself. I have a Entity called Contract and a contract can have many price rules and these price rules can be different kind of classes and presisted in different tables. I suppose this is what's polymorphic associations for or am I wrong?
class contract {
private $id;
private $priceRules;
}
class discountRule implements priceRule{
function calculate() {
// calculate new price after this rule
}
}
class extraSpecialRule implements priceRule {
function calculate() {
// calculate new price after this rule
}
}
There can be new types of price rules in the future,so how can I associate these rules to the main entity and presist them in seperate tables?
Update:
This is my new code:
contract.php
namespace Entities;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #Entity #Table(name="contract")
*/
class Contract {
/**
*
* #Id #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* #Column(type="integer")
*/
private $propertyId;
/**
*
* #Column(type="integer")
*/
private $agencyId;
/**
*
* #OneToMany(targetEntity="priceRule" ,mappedBy="contract")
*
*/
private $priceRules;
public function __construct($propertyId,$agencyId){
$this->propertyId=$propertyId;
$this->agencyId=$agencyId;
$this->priceRules=new ArrayCollection();
}
public function addPriceRule(priceRule $rule){
$this->priceRules[]=$rule;
}
public function getPriceRules(){
return $this->priceRules;
}
}
pricerule.php
namespace Entities;
/**
* #Entity
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="discr" , type="string")
* #DiscriminatorMap({"discountrule"="discountRule","extradiscountrule"="extraDiscountRule"})
*/
class priceRule{
/**
*
* #Id #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* #ManyToOne(targetEntity="contract",inversedBy="availibilityRules")
* #JoinColumn("contract_id",referencedColumnName="id")
*/
private $contract;
}
discountrule.php
namespace Entities;
/**
* #Entity
*
*
*/
class discountRule extends priceRule {
/**
*
* #Id #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
public function calculatePrice(){
// calculate new price
}
}
extradiscountrule.php
namespace Entities;
/**
* #Entity
*
*
*/
class extraDiscountRule extends priceRule {
/**
*
* #Id #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
public function calculate() {
// calculate new price
}
}
sampleusage.php
$contract=new Contract(1,1);
$discount=new discountRule();
$em->persist($discount);
$contract->addPriceRule($discount);
$em->persist($contract->getPriceRules());
$em->persist($contract);
$em->flush();
But when I try to add new rule to the contract I get error message (Fatal error: Uncaught exception 'Doctrine\ORM\Mapping\MappingException' with message 'Class Doctrine\Common\Collections\ArrayCollection is not a valid entity or mapped super class.)
What am I doing wrong ?

You may be missing an #MappedSuperclass on your PriceRule parent object
Refer to: Inheritance Mapping

I don't this is possible because an interface cannot define properties in a class, so you can't guarantee there will be properties for Doctrine to manipulate.
If you could provide more details on your entities I could help better.

Related

Three level inheritance in doctrine2 not loading second level data

I have a inheritance of three levels using class table inheritance like this:
Class Test
namespace App\Entities\Test;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Test
* #package App\Entities\Test
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"TestA" = "TestA", "TestB" = "TestB"})
* #ORM\Table(name="test")
*/
abstract class Test {
/**
* #var integer
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string")
*/
protected $columnTest;
}
Class TestM extends Test
namespace App\Entities\Test;
use Doctrine\ORM\Mapping as ORM;
/**
* Class TestM
* #package App\Entities\Test
* #ORM\Entity
*/
abstract class TestM extends Test{
/**
* #var string
* #ORM\Column(type="string")
*/
protected $columnTestM;
}
Class TestA extends TestM
namespace App\Entities\Test;
use Doctrine\ORM\Mapping as ORM;
/**
* Class TestA
* #package App\Entities\Test
* #ORM\Entity
*/
class TestA extends TestM{
/**
* #var string
* #ORM\Column(type="string")
*/
private $columnTestA;
public function __construct(string $columnTest, string $columnTestM, string $columnTestA) {
$this->columnTest = $columnTest;
$this->columnTestM = $columnTestM;
$this->columnTestA = $columnTestA;
}
/**
* #return string
*/
public function getColumnTest(): string {
return $this->columnTest;
}
/**
* #param string $columnTest
*/
public function setColumnTest(string $columnTest): void {
$this->columnTest = $columnTest;
}
/**
* #return string
*/
public function getColumnTestM(): string {
return $this->columnTestM;
}
/**
* #param string $columnTestM
*/
public function setColumnTestM(string $columnTestM): void {
$this->columnTestM = $columnTestM;
}
/**
* #return string
*/
public function getColumnTestA(): string {
return $this->columnTestA;
}
/**
* #param string $columnTestA
*/
public function setColumnTestA(string $columnTestA): void {
$this->columnTestA = $columnTestA;
}
}
I'm having problem because, when I'm going to retrieve the entity from my DB, it comes with the second level with no data, only the first and last levels comes with all the data. Notice that columnTestM is blank. What am I missing? It's persisting with all three levels with data, the problem is only when I have to get it. As an example I put the column's content with it's own name
>>> print_r(\EntityManager::getRepository('App\Entities\Test\Test')->find(1));
App\Entities\Test\TestA Object
(
[columnTestA:App\Entities\Test\TestA:private] => columnTestA
[columnTestM:protected] =>
[id:protected] => 1
[columnTest:protected] => columnTest
)
EDIT:
Maybe it's a bug and I filed an issue at GitHub about a wrong query generation. I put my MariaDB to log all queries to check the query being generated when I try to retrieve the data back and that's the result:
SELECT
t0.id AS id_3,
t0.column_test AS column_test_4,
t0.discr,
t1.column_test_a AS column_test_a_5,
t2.column_test_b AS column_test_b_6
FROM
test t0
LEFT JOIN
test_as t1 ON t0.id = t1.id
LEFT JOIN
test_bs t2 ON t0.id = t2.id
WHERE
t0.id = 1
It tries to left join with TestB instead of doing the join with TestM
As stated by #LBA the problem is two abstract classes in the chain. It should work even with two abstract classes, but only the first is retrieved, but all the others in the middle stays blank. Issue already reported in doctrine's github project.

Sonata admin configureFormField

i use sonata admin and i have a 'Fonctionnare' entity. i changed the 'codeFonctionnaire' type of this entity to string but when i create the Fonctionnaire admin class and try to add new fonctionaire i got this error message:
Neither the property "codeFonctionnaire" nor one of the methods "setCodeFonctionnaire()", "_set()" or "_call()" exist and have public access in class "Examens\ExamensBundle\Entity\Fonctionnaire".
Fonctionnaire.php:
<?php
namespace Examens\ExamensBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Fonctionnaire
*/
class Fonctionnaire
{
/**
* #var string
*/
private $codeFonctionnaire;
//////
/**
* Get codeFonctionnaire
*
* #return string
*/
public function getCodeFonctionnaire()
{
return $this->codeFonctionnaire;
}
////////
FonctionnaireAdmin.php:
<?php
namespace Examens\ExamensBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Examens\ExamensBundle\Entity\Fonctionnaire;
class FonctionnaireAdmin extends Admin
{
protected $datagridValues = array(
'_sort_order' => 'ASC',
'_sort_by' => 'codeFonctionnaire'
);
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('codeFonctionnaire','text',array('label'=>'Code fonctionnaire'))
//////
}
what's wrong with the entity?
You need to regenerate the getters and setters of your Fonctionnaire class. Your IDE can do it for you.
Or at least, just add a
public function setCodeFonctionnaire($codeFonctionnaire) {
$this->codeFonctionnaire = $codeFonctionnaire;
}
EDIT
Here is what might be your complete Fonctionnaire class :
namespace TechVehi\BlogBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class Fonctionnaire {
/**
* #var string
*
* #ORM\Column(name="codeFonctionnaire", type="string", length=255, nullable=true)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $codeFonctionnaire;
/**
* #param string $codeFonctionnaire
*/
public function setCodeFonctionnaire($codeFonctionnaire) {
$this->codeFonctionnaire = $codeFonctionnaire;
}
/**
* #return string
*/
public function getCodeFonctionnaire() {
return $this->codeFonctionnaire;
}
}
You might have forgotten other informations, like the ORM annotation to link your object to your database.
i deleted this code from fonctionnaire.orm.yml:
generator:
strategy: IDENTITY
and it works.

symfony annotations validation override entities/models

I'm trying to override the entities validatation of a forum bundle.
I do it like this:
Category entity:
//src/MSD/ForoBundle/Entity/Category.php
namespace MSD\ForoBundle\Entity;
use Herzult\Bundle\ForumBundle\Entity\Category as BaseCategory;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="Herzult\Bundle\ForumBundle\Entity\CategoryRepository")
*/
class Category extends BaseCategory
{
}
Topic entity:
//src/MSD/ForoBundle/Entity/Topic.php
namespace MSD\ForoBundle\Entity;
use Herzult\Bundle\ForumBundle\Entity\Topic as BaseTopic;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Topic
*
* #ORM\Entity(repositoryClass="Herzult\Bundle\ForumBundle\Entity\TopicRepository")
*
*/
class Topic extends BaseTopic
{
/**
* #ORM\ManyToOne(targetEntity="Category")
*/
protected $category;
/**
* #Assert\NotBlank()
* #Assert\MinLength(limit=4, message="Just a little too short| ")
* #Assert\Regex(
* pattern="/^[a-zA-Z0-9\-_¿?!¡ ]{4,50}$/",
* message="El tema puede contener letras, números, guiones y espacios, interrogantes y exclamaciones. Entre 4 y 30 caracteres"
* )
*/
protected $subject;
/**
* {#inheritDoc}
*/
public function getAuthorName()
{
return $this->author;
}
/**
* #ORM\ManyToOne(targetEntity="User")
*/
private $author;
public function setAuthor(User $user)
{
$this->author = $user;
}
public function getAuthor()
{
return $this->author;
}
}
Post Entity:
//src/MSD/ForoBundle/Entity/Post.php
namespace MSD\ForoBundle\Entity;
use Herzult\Bundle\ForumBundle\Entity\Post as BasePost;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="Herzult\Bundle\ForumBundle\Entity\PostRepository")
*/
class Post extends BasePost
{
/**
* #ORM\ManyToOne(targetEntity="Topic")
*/
protected $topic;
/**
* #Assert\Regex(
* pattern="/^[^<>]{4,1000}$/",
* message="El mensaje no puede contener '<' ni '>'. Entre 4 y 1000 caracteres"
* )
*
*/
public $message;
public function getAuthorName()
{
return $this->author;
}
/**
* #ORM\ManyToOne(targetEntity="User")
*/
private $author;
public function setAuthor(User $user)
{
$this->author = $user;
}
public function getAuthor()
{
return $this->author;
}
}
And the validation works... except the message of the firt post!! that is created when a new topic is created.
I've tried many changes, but without success.
Any idea of why does it happend?
Thank you
Yeah! I got it. The solution was to add this in the Topic entity:
/**
* #Assert\NotBlank
* #Assert\Valid
*/
protected $firstPost;
Then, the message of the first post is validated.

Symfony2/Doctrine2 One-To-Many same object twice

My current Client-Entity has an unloading and a loading Area, which are both ClientArea-Entities.
namespace ACME\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sorien\DataGridBundle\Grid\Mapping as GRID;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
/**
* ACME\DemoBundle\Entity\Client
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="ACME\DemoBundle\Entity\ClientRepository")
*/
class Client
{
/**
* #ORM\OneToMany(targetEntity="ClientArea",mappedBy="client", cascade={"persist", "remove"})
*/
public $unloading_areas;
/**
* #ORM\OneToMany(targetEntity="ClientArea",mappedBy="client", cascade={"persist", "remove"})
*/
public $loading_areas;
}
class ClientArea
{
/**
* #ORM\ManyToOne(targetEntity="Client")
*/
public $client;
}
This does not work because client can only map 1 association.
How can i map the relation properly?
To create entity relations you need to have keys to use when joining tables. Your Client class should have an id key defined and you need to initialize collections, like this:
class Client
{
//....
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="ClientArea", mappedBy="client", cascade={"persist", "remove"})
*/
public $unloading_areas;
/**
* #ORM\OneToMany(targetEntity="ClientArea", mappedBy="client", cascade={"persist", "remove"})
*/
public $loading_areas;
public function __construct() {
// Initialize collections
$this->unloading_areas = new \Doctrine\Common\Collections\ArrayCollection();
$this->loading_areas = new \Doctrine\Common\Collections\ArrayCollection();
}
// ....
}
Your ClientArea class should then look something like this:
class ClientArea
{
// ....
/**
* #ORM\Column(name="client_id", type="int", nullable=false)
*/
private $clientId;
/**
* #ORM\ManyToOne(targetEntity="Client")
* #JoinColumn(name="client_id", referencedColumnName="id")
*/
public $client;
// ....
}
Now, those two entities should be mapped correctly.
To learn more about association mappings in Doctrine, read article here: http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html
Hope this helps.

doctrine 2, how do get data from the inverse side (many to one)

I have two entities, entry and comments.
comments:
/**
* #Entity(repositoryClass="\Entities\Blog\CommentRepository")
* #Table(name="blog_comment")
* #HasLifecycleCallbacks
*/
class Comment extends \Entities\AbstractEntity
{
/**
* #Id #Column(name="id", type="integer")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ManyToOne(targetEntity="Entry", inversedBy="comments")
* #JoinColumn(name="entry_id", referencedColumnName="id")
*/
protected $entry;
/** #Column(name="approved", type="string", length=255) */
protected $approved;
/** #Column(name="title", type="string", length=255) */
protected $title;
/** #Column(name="content", type="text") */
protected $content;
/** #Column(name="pub_date", type="datetime") */
protected $pub_date;
/** #Column(type="datetime") */
private $created_at;
/** #Column(type="datetime") */
private $updated_at;
/** #PreUpdate */
public function updated()
{
$this->updated_at = new \DateTime("now");
}
public function __construct()
{
$this->created_at = $this->updated_at = new \DateTime("now");
}
}
class CommentRepository extends \Entities\PaginatedRepository
{
protected $_entityClassName = 'Entities\Blog\Comment';
}
and entry:
<?php
namespace Entities\Blog;
/**
* #Entity(repositoryClass="\Entities\Blog\EntryRepository")
* #Table(name="blog_entry")
* #HasLifecycleCallbacks
*/
class Entry extends \Entities\AbstractEntity
{
/**
* #Id #Column(name="id", type="integer")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #Column(name="permalink", type="string", length=255) */
protected $permalink;
/** #Column(name="title", type="string", length=255) */
protected $title;
/** #Column(name="pub_date", type="datetime") */
protected $pub_date;
/** #Column(name="content", type="text") */
protected $content;
/** #OneToMany(targetEntity="Comment", mappedBy="entry") */
protected $comments;
/** #Column(type="datetime") */
private $created_at;
/** #Column(type="datetime") */
private $updated_at;
/** #PreUpdate */
public function updated()
{
$this->updated_at = new \DateTime("now");
}
public function __construct()
{
$this->comments = new \Doctrine\Common\Collections\ArrayCollection();
}
I can get the collection of all comments belonging to each entry via:
foreach ($comments as $comment){
$comment-$commentId;
}
but how can I get the entry information from the comments side. for example, I would like to get the entry id from a specific comment
Each time you create a #OneToMany relation, you create a Collection of proxy objects in class on "One"-side of relation, and single proxy object in class on "Many"-side of relation. Proxy classes are automatically generated by Doctrine2 from your mapping information.
To allow Doctrine2 filling proxy object with real data from DB it's important to declare it protected or private. I'm not sure about that, but seems like Doctrine tracks down any requests to proxy objects inside your entity class and ensures that proxies are populated before first usage.
To access the associated object you have to define accessor function in your Comment class:
class Comment extends \Entities\AbstractEntity{
/** other definitions */
function getEntity(){
return $this->entity;
}
}
And use it like
$comment = $em->find("Entities\Comment",1);
$entity = $comment->getEntity();
Doctrine2 will automatically populate $comment->entity proxy with actual Entity object.
See "Workin with Objects" chapter of Doctrine documentation and "Can you explain me what is a Proxy in Doctrine 2?" on details of proxies.

Resources