Dynamic relationship and the SonataAdminBundle - doctrine-orm

I use the SonataAdminBundle for the first time in my life and I have some
problems with it.
At first, I had a PageBundle which has a Page and a Author entity. Then I
started using SonataAdminBundle and used the sonata_type_model to nicely
display the Authors of a Page:
// ...
protected function configureFormFields(FormMapper $mapper)
{
$mapper
->add('title')
->add('slug', null, array('required' => false))
->add('published', null, array(
'label' => 'publish',
'required' => false,
))
->add('author', 'sonata_type_model')
->add('content')
;
}
But then I discovered the SonataUserBundle. I started using it and when I
finally get it working I thought it would be nice to make use of this User
entity, instead of the Author entity in the PageBundle. To make this possible,
I used the technique where the documentation talks about in
"How to Define Relationships with Abstract Classes and Interfaces".
This worked, but not with the SonataAdminBundle. It seems like the
sonata_type_model doesn't work with the ResolveTargetEntityListener and I
can't make it working.
The relevant Page entity code in the PageBundle:
namespace Wj\PageBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table
* #ORM\Entity
* #ORM\HasLifeCycleCallbacks
*/
class Page
{
/**
* #var integer $authorId
*
* #ORM\ManyToOne(targetEntity="Wj\PageBundle\Model\AuthorInterface")
*/
private $author;
}
The Wj\PageBundle\Model\AuthorInterface:
namespace Wj\PageBundle\Model;
interface AuthorInterface
{
public function getId();
}
And the User entity in the UserBundle:
namespace Wj\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser;
use Wj\PageBundle\Model\AuthorInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser implements AuthorInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __toString()
{
return $this->getFirstName().' '.$this->getLastName();
}
}
The FormMapper configuration is the same as I posted above.
How can I use the ResolveTargetEntityListener technique in combination with
the SonataAdminBundle and his sonata_type_model?

Related

Doctrine prevent error on OneToOne association where record doesn't exist in database

Problem
I'm trying to create a OneToOne association in a Laravel app using Doctrine. When trying to access the association I'm getting this error.
Entity of type 'Status' for IDs clientId(1) was not found
Versions:
Doctrine: 2.7.5
Laravel: 7.30.4
Code:
Client Class
<?php
namespace App\Client;
use App\Person\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Client
* #package App\Client
*
* #ORM\Entity(repositoryClass="ClientRepository")
* #ORM\Table(name="client")
*/
class Client extends Person
{
/**
* #var Status
*
* #ORM\OneToOne(targetEntity="Status", mappedBy="client")
* #ORM\JoinColumn(name="id", referencedColumnName="client_id", nullable=true)
*/
protected $status;
/**
* #return Status
*/
public function getStatus()
{
return $this->status;
}
}
Status Class:
<?php
namespace App\Client\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Status
* #package App\Client\Entity
*
* #ORM\Entity(readOnly=true)
* #ORM\Table(name="status_view")
*/
class Status
{
/**
* #var int
*
* #ORM\Id()
* #ORM\Column(name="client_id", type="integer")
*/
protected $clientId;
/**
* #var \App\Client\Client
*
* #ORM\OneToOne(targetEntity="App\Client\Client", inversedBy="staus")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id", nullable=true)
*/
protected $client;
/**
* #var string
*
* #ORM\Column(name="status", type="string")
*/
protected $status;
/**
* #return string
*/
public function getStatus()
{
return $this->status;
}
}
Calling Code
$client->getStatus()->getStatus()
What I've tried/Answers I've looked at
Entity of type 'AppBundle\Entity\User' for IDs id(155) was not found - I'm not using a Doctrine filter, nor DQL.
https://stackoverflow.com/a/49416542/9530790 - This works, with a few tweaks, by swallowing up the exception, but it feels more like a hack when the docs say nullable should work.
https://stackoverflow.com/a/21887344/9530790 - This states nullable should work but it doesn't.
https://stackoverflow.com/a/15744449/9530790 - Same question different ans. States that Doctrine doesn't support zero-to-one associations, but nullable I believe should be what solves that, but for my problem it's not working. Also there's no link to the docs stating where Zero to one is not supported.
I believe that adding fetch="EAGER" should fix the null issue as elsewhere in our app that works, but when I add that I get an different Doctrine error spl_object_hash() expects parameter 1 to be object, null given, which again has to do with the association not existing.
"Well why aren't you experiencing the above error with your other associations". Great question! After a deep underwater excursion into the Doctrine code, I believe the reason is because those associations are nested and for some reason (I'm not sure why), when nested, the spl_object_hash function, in class UnitOfWork is not called.
Additional Notes:
This is what the object looks like when calling $client->getStatus(), before it errors on the next ->getStatus() call.
DoctrineProxies\__CG__\App\Client\Entity\Status {#2011
+__isInitialized__: false
#clientId: 4
#client: null
#status: null
…2
}
You can see it's a Client Proxy object that's created not a 'true' object, this is why it errors (with Entity of type 'Status' for IDs clientId(1) was not found) when not using fetch="EAGER", since eager loads a true object. See here
This code below in the Proxy object is the what causes the above error. Which is why I can't do a try catch in the parent ('true' Client class), since it errors before calling the parent.
/**
* {#inheritDoc}
*/
public function getStatus()
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getStatus', []);
return parent::getStatus();
}
Question:
Why is nullable=true not working as expected, and what should/can I do to make it work?

What is the reason why OneToOne relationship in Doctrine cannot be lazy loaded?

I have a Person and Admin entity. One Person can only have one Admin, but I do not need admin all the time. When I list all people with admin associated, doctrine will load all admins from DB. Why is this happening?
In a case of one-to-one association (and in similar cases, e.g. while lazy loading one-to-many association) Doctrine generates so called proxy objects for associated entities. These proxy objects mimics interface of target entity but only triggers actual data loading from database upon access to non-id field.
Because of this in your case when you're fetching list of Person entities - you doesn't get list of Admin entities fetched from database, but receiving list of Admin proxies instead. Unless you will (occasionally or intentionally) try to access some of properties of Admin entity (with exception of its id which can be safely accessed) Doctrine will not try to fetch any Admin information from database.
Consider following simplified setup of A and B entities with one-to-one association:
// A.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class A
{
/**
* #var integer
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var B
* #ORM\OneToOne(targetEntity="App\Entity\B")
*/
private $b;
/**
* #return int
*/
public function getId(): int
{
return $this->id;
}
/**
* #return B
*/
public function getB(): B
{
return $this->b;
}
}
// B.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class B
{
/**
* #var integer
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string|null
* #ORM\Column(type="string", nullable=true)
*/
private $name;
}
You can proof that A::$b contain proxy object by either looking into debugger or by using reflection:
$entities = $this->getEntityManager()->getRepository(\App\Entity\A::class)->findAll();
/** #var \App\Entity\A $a */
$a = array_shift($entities);
$class = (new \ReflectionObject($a->getB()))->getName();
In this case value of $class variable will be Proxies\__CG__\App\Entity\B (Doctrine proxy object for App\Entity\B) and not App\Entity\B as it would be in a case of normal entity object.

how to add/remove an entity's mapping programmatically

I have a handler which uses the same Entity for two different kind of queries:
/**
* #ORM\Entity
* #ORM\Table(name="app_iso50k1.meter", schema="app_iso50k1")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="mettype", type="string")
* #ORM\DiscriminatorMap({"phy" = "PhyMeter"})
*/
abstract class Meter
{
const TYPE_PHYSICAL = 'PHY';
const TYPE_VIRTUAL = 'VIR';
const TYPE_LOGICAL = 'LOG';
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
protected $id;
<methods>
}
/**
* #ORM\Entity
* #ORM\Entity(repositoryClass="PhyMeterRepository")
* #HasLifecycleCallbacks
*/
class PhyMeter extends Meter
{
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $idInstrum;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $idDevice;
/**
* #ORM\Column(type="integer")
*/
protected $instrumType;
...<methods>
}
The first handler's method is performed on a legacy DB table and it would need to map all the fields annotated with #ORM\Column (id, idInstrum, idDevice, instrumType). To do that, I use a primitive query and I map the data by means of a ResultSetMapping
$rsm = new ResultSetMapping;
$rsm->addEntityResult('Belka\Iso50k1Bundle\Entity\PhyMeter', 'mt');
$rsm->addFieldResult('mt', 'id', 'id');
...
and it works like a charm. The problem is on the the other handler's methods which need to persist data on app.meter table: what I really would like to persist are a small part of the properties (i.e. just the id, idInstrum but not instrumType so as not to have that column in my new table's schema.
I was thinking about using StaticPHPDriver but I'm not sure if it is the right way: what I really would like is manually adding/removing some ORM mapping according to my needs (i.e. different handler's functions)
Is that possible? I could remove the mappings (#ORM\column annotation) I don't need to persist, but that way I cannot map the extra properties by using ResultSetMapping, unless I can add it programmatically.
Any hint is more than welcome

Doctrine inheritance in Flow

I’m trying to set up a Base-class for my Typo3 Flow projects. It should contain the “created at” and the “updated at” date.
Since Doctrine allows you to use inheritance mapping, I want to make my baseclass a “MappedSuperclass”.
BaseClass.php:
/**
* #Flow\Entity
* #ORM\MappedSuperclass
*/
class BaseClass {
/**
* #var \DateTime
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #var \DateTime
* #ORM\Column(type="datetime")
*/
protected $updated;
...
Component.php:
/**
* #Flow\Entity
* #ORM\InheritanceType("SINGLE_TABLE")
*/
class Component extends BaseClass{
If i try to use the "flow doctrine:update" command the following error message pops up:
Uncaught Exception
Entity '...\Domain\Model\BaseClass' has no method
'Flow_Aop_Proxy_fixMethodsAndAdvicesArrayForDoctrineProxies' to be
registered as lifecycle callback.
So is it possible to use model inheritance in TYPO3 Flow?
I found out one way to do it.
Just make your BaseClass abstract and add all the additional annotations like this:
/**
* #Flow\Entity
* #ORM\MappedSuperclass
*/
abstract class BaseClass {
And extend your models like that:
/**
* #Flow\Entity
* #ORM\InheritanceType("SINGLE_TABLE")
*/
class SomeModel extends BaseClass{
The table of SomeModel will now have the attributes from the BaseClass. But BaseClass itself is not represented in the database schema.
Maybe you are also able to use traits for more complex solutions.

Table already exist

I am using doctrine 2 in zend framework 2. Below is my entity file. The problem is, when I tried to validate schema using,
./vendor/bin/doctrine-module orm:validate-schema
command.
I am getting error,
[Doctrine\DBAL\Schema\SchemaException]
The table with name 'database.opportunitycriteria' already exists.
What should I do?
namespace Administration\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* OpportunityCriteria
*
* #ORM\Table(name="OpportunityCriteria")
* #ORM\Entity
*/
class Criteria
{
/**
* #var integer
* #ORM\Id
* #ORM\Column(name="criteria_id", type="integer", nullable=false)
*/
private $criteria_id;
/**
* #var string
*
* #ORM\Column(name="description", type="string", nullable=false)
*/
private $description;
}
and appropriate getter and setter methods..
I finally figured it out. OP's use case may be different, but in my case, this was because of a misconfigured bidirectional many-to-many relationship.
I had the following entities:
class Cuisine {
/**
* #ManyToMany(targetEntity="Dish")
* #ORM\JoinTable(name="CuisineDish", ...)
*/
protected $dishes;
}
class Dish {
/**
* #ORM\ManyToMany(targetEntity="Cuisine")
* #ORM\JoinTable(name="CuisineDish", ...)
*/
protected $cuisines;
}
What was missing was the inversedBy and mappedBy properties of the #ManyToMany annotations. These are only required when the association is bi-directional.
So now the correctly mapped entities look like:
class Cuisine {
/**
* #ManyToMany(targetEntity="Dish", inversedBy="cuisines")
* #ORM\JoinTable(name="CuisineDish", )
*/
protected $dishes;
}
class Dish {
/**
* #ORM\ManyToMany(targetEntity="Cuisine", mappedBy="dishes")
* #ORM\JoinTable(name="CuisineDish", ...)
*/
protected $cuisines;
}
And orm:validate-schema does not exit with an exception any more.
The exception message is just misleading, as the database is not altered by this operation. Furthermore, this issue is only spotted when validating the sync with the database, not when validating the mapping only (--skip-sync), where it should.
I just reported this bug.
it can cause this error message if you want to use a table name which is already used by one of the installed bundles.