Doctrine bidirectional association error. Memory exhausted - doctrine-orm

I have two doctrine entities school and schoolLocation in zend framwork
school entity
/**
* #var schoolLocation
*
* #ORM\OneToMany(targetEntity="\School\Entity\SchoolLocation", mappedBy="school", cascade={"persist","remove"})
*
*/
protected $schoolLocation;
and
schoolLocation entity
/**
* #var school
*
* #ORM\ManyToOne(targetEntity="\School\Entity\School", inversedBy="schoolLocation")
* #ORM\JoinColumn(name="school_id", referencedColumnName="id")
*/
protected $school;
I've shown bidirectional association. Now whenever I try to get school_location
$schoolLocation = $this->entityManager->getRepository(SchoolLocation::class)->findAll();
and print_r($schoolLocation) I am getting following error message:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 65015808 bytes) in F:\projects\test\module\School\src\Service\SchoolManager.php on line 190
Could anybody please help me telling, what am I doing wrong here ?

When you use print_r on an object with a circular relation it will keep printing the relation, untill you run out of memory.
class A {
public $name = "class a";
public $b;
public function __construct(B $b) {
$this->b = $b;
}
}
class B {
public $name = "class b";
public $a;
public function __construct() {
$this->a = new A($this);
}
}
print_r(new B);
This code demonstrates what is happening. The amount of recusrion shown, depends on your PHP configuration.
I recommend using Xdebug instead of printing, this has many advantages. In your case it will allow you to easily inspect the circular relation.

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?

Custom annotation was never imported

I created a doctrine annotation
namespace Fondative\GenBundle\Front\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* #Annotation
* #Target("PROPERTY")
*/
class ReferenceAnnotation extends Annotation {
}
use Fondative\GenBundle\Front\Annotation\ReferenceAnnotation ;
/**
* A Driver.
*
*
*
* #ORM\Entity
*
*/
class Driver {
/*
* #Reference
*/
private $name;
I get this exception
[Semantical Error] The annotation "#Reference" in property
Fondative\TestBundle\Entity\Driver::$name was never imported.
A very common mistake ;)
The package Doctrine/Annotation read annotations from comments via ReflectionProperty::getDocComment().
Problem example:
class MyClassA
{
/**
* Foo
*/
private $foo;
/*
* Bar
*/
private $bar;
}
$ref = new \ReflectionClass('MyClassA');
print sprintf(
"Comment of MyClassA::foo -> \n%s\n\n",
$ref->getProperty('foo')->getDocComment()
);
print sprintf(
"Comment of MyClassA::bar -> \n%s\n\n",
$ref->getProperty('bar')->getDocComment()
);
The property foo have a doc comments, but property bar not have comment, because exist typos in declare comment (one special char *).
In PHP doc comments must be started from two chars *!
Fix this typo, and all works ;)
class Driver {
/*
* #ReferenceAnnotation
*/
private $name;
or rename the annotation class to Reference and
use Fondative\GenBundle\Front\Annotation\Reference as Reference ;
/*
* #Reference
*/
private $name;

Doctrine "reverse" orphan removal

I have two entities (simplified):
class EncryptedMasterKey {
/**
* #ORM\ManyToOne(targetEntity="ExchangeFile", inversedBy="encryptedMasterKeys")
* #ORM\JoinColumn(name="exchange_file_id", referencedColumnName="id")
*
* #var ExchangeFile
*/
protected $exchangeFile;
}
and
class ExchangeFile {
/**
* #ORM\OneToMany(targetEntity="EncryptedMasterKey", mappedBy="exchangeFile", orphanRemoval=true, cascade={"persist", "remove"})
*/
protected $encryptedMasterKeys;
}
There can be many EncryptedMasterKeys for one ExchangeFile in the database. If the ExchangeFile is deleted, all related encrypted MasterKeys are deleted (orphanRemoval=true and cascade={"persist", "remove"} make sure this is the case). So far, so good.
Now as the actual file lies encrypted on the hard disk, there must be at least one EncryptedMasterKey so that the file can be decrypted. So when a EncryptedMasterKey is deleted and I discover that it is the last one for it's ExchangeFile, I also have to delete the ExchangeFile because it cannot be decrypted any more. An ExchangeFile cannot live without at least one EncryptedMasterKey.
How do I achieve this? #ORM\PreRemove in the EncryptedMasterKey class does't really help me because I don't have access to the Entity Manager:
class EncryptedMasterKey {
//...
/** #ORM\PreRemove */
public function removeOrphanExchangeFile()
{
if ($this->exchangeFile->isTheOnlyMasterKey($this))
// I don't have access to the Entity Manager,
// so how do I delete the ExchangeFile?
}
}
Is there any elegant solution to this?
Thanks for your time.
You can use an event subscriber and create a class like following:
class MyEncryptedMasterSubscriber implements \Doctrine\Common\EventSubscriber
{
public function getSubscribedEvents()
{
return array(\Doctrine\ORM\Events::onFlush);
}
public function onFlush(\Doctrine\ORM\Events\OnFlushEventArgs $eventArgs)
{
$uow = $eventArgs->getEntityManager()->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() AS $entity) {
if (
$entity instanceof EncryptedMasterKey
&& $entity->getExchangeFile()->isTheOnlyMasterKey($entity)
) {
$uow->scheduleForDelete($entity->getExchangeFile());
}
}
}
}
You can read more about how to register subscribers in the particular case of Symfony 2 on the documentation for it.

$this->assertEquals error : Failed asserting that null matches expected

I'm doing test unit of my entity:
namespace PathtomyBundle\Tests;
require_once dirname(__DIR__).'/../../../app/AppKernel.php';
use Doctrine\ORM\Tools\SchemaTool;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* #var Symfony\Component\HttpKernel\AppKernel
*/
protected $kernel;
/**
* #var Doctrine\ORM\EntityManager
*/
protected $entityManager;
/**
* #var Symfony\Component\DependencyInjection\Container
*/
protected $container;
public function setUp()
{
// Boot the AppKernel in the test environment and with the debug.
$this->kernel = new \AppKernel('test', true);
$this->kernel->boot();
// Store the container and the entity manager in test case properties
$this->container = $this->kernel->getContainer();
$this->entityManager = $this->container->get('doctrine')->getEntityManager();
// Build the schema for sqlite
//$this->generateSchema();
parent::setUp();
}
public function tearDown()
{
// Shutdown the kernel.
$this->kernel->shutdown();
parent::tearDown();
}
protected function generateSchema()
{
// Get the metadatas of the application to create the schema.
$metadatas = $this->getMetadatas();
if ( ! empty($metadatas)) {
// Create SchemaTool
$tool = new SchemaTool($this->entityManager);
$tool->createSchema($metadatas);
} else {
throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
}
}
/**
* Overwrite this method to get specific metadatas.
*
* #return Array
*/
protected function getMetadatas()
{
return $this->entityManager->getMetadataFactory()->getAllMetadata();
}
}
and also:
namespace pathtomybundle\Tests\Entity;
use pathtomybundle\Tests\TestCase;
use pathtomybundle\Entity\Calendars;
require_once dirname(__DIR__).'/TestCase.php';
class CalendarsDbTest extends TestCase
{
protected $Calendars;
public function setUp()
{
parent::setUp();
$this->Calendars = new Calendars();
}
public function testGenerateCalendars()
{
$this->Calendars->setBeginDate(new \DateTime('now'));
$this->Calendars->setDescription('Description');
$this->Calendars->setEndDate(new \DateTime('now'));
$this->Calendars->setType('sur titre');
// Save the ExCalendars
$this->entityManager->persist($this->Calendars);
$this->entityManager->flush();
}
public function testUser(){
$this->assertEquals('Description', $this->Calendars->getDescription() );
}
So my questions are:
Why does it raise this error "Failed asserting that null matches expected"?
Why getDescription() returns NULL?
How to test two table with One-to-Many relationship for example my Table Calendars with another table in database?
Edit
For the third question :
For example I have two Tables Job and Calenders with Many-to-One relationship so I will have a Job_Id field in Calendars Table,so how I will do my test Unit with a foreign key "job_id"
In Calendars Entity :
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="job_id")
* })
*/
private $jobId;
Edit-2-
when I run my phpunit test "phpunit -c app" to test setters function and persist in database so I have a with every test a new data insered in databse, my question is it possible to do a lot of test but I insert data in database just for one time because actually I must remove data from database with every test.
2 - another question : to create a database_test i use "$this->generateSchema();
" so after create a database for the first time and when the test call "TestCase"class (the code above) again so he tried to create the database_test again then I must remove the line after the first time and it's not good,so what I can do to run this line just for one time in the first time when i run my test?
Edit-3
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
* })
*/
private $job;
it's normal?
Every test in test case creates his own CalendarsDbTest object. So, in fact, $this->Calendar is different object in each test (if you want share it between tests you need create it in setUp method)
Is the same as above (there is null because you never call setDescription with $this->Calendars - it's different object than it is in first test)
I'm not sure what exactly you mean. Can you show more precise (for example method you want test) what you mean?
edit:
The answer is: you don't test it. Why? Because unit test is UNIT test - you should test here only your entity. Persistence, keeping relations etc. are Doctrine resposibility and should be tested there - you don't worry about it.
The only thing you should test is setter/getter for $jobId property (btw. it should be "$job" rather than "$jobId" because it's object of Job class - not an integer), eg.:
class CalendarTest extends \PHPUnit_Framework_TestCase
{
(...)
public function testSetGetJob()
{
$job = new Job();
$job->setSomeProperty('someValue');
$expectedJob = clone $job; // you clone this because in setter you pass object by reference
$calendar = new Calendar();
$calendar->setJob($job);
$this->assertEquals($expectedJob, $calendar->getJob());
}
(...)
}

Doctrine 2 Can't Seem to Remove Many to Many Relationships

I have the following setup "Many Users can have Many Projects (Collaborators)"
/**
* #Entity #HasLifeCycleCallbacks
* #Table(name="projects")
*/
class Project implements \Zend_Acl_Resource_Interface {
/**
* #ManyToMany(targetEntity="User", mappedBy="projects")
* #OrderBy({"displayName" = "ASC", "username" = "ASC"})
*/
protected $collaborators;
..
}
/**
* #Entity
* #Table(name="users")
*/
class User implements \Zend_Acl_Role_Interface {
/**
* #ManyToMany(targetEntity="Project", inversedBy="collaborators")
*/
protected $projects;
...
}
I tried to remove a collaborator using the following
$user = Application_DAO_User::findById($this->_getParam('userid'));
$proj = Application_DAO_Project::getProjectById($this->_getParam('id'));
Application_DAO_Project::removeCollaborator($proj, $user); // <---
// Application_DAO_User
public static function findById($id) {
return self::getStaticEm()->find('Application\Models\User', $id);
}
// Application_DAO_Project
public static function getProjectById($id) {
return self::getStaticEm()->find('Application\Models\Project', $id);
}
public static function removeCollaborator(Project $proj, User $collaborator) { // <---
$proj->getCollaborators()->remove($collaborator);
$collaborator->getProjects()->remove($proj);
self::getStaticEm()->flush();
}
And there isn't any errors but the database stays the same ...
This may be well over due but was just experiencing the same problem myself... According to the doctrine 2 documents, the function ArrayCollection->remove($i) is for removing by array index.
What you are after is:
getCollaborators()->removeElement($collaborator);
I went round in circles trying to figure this out until I realised that for this to work:
getCollaborators()->removeElement($collaborator);
$collaborator would have to be the actual object from the collaborators ArrayCollection. That is, if you pass in a new Collaborator object with the same parameters it won't remove it. That's because ArrayCollection uses array_search to look for the object you want to remove.
Hope that saves someone else a few hours...