I have a problem with lazy loading in symfony2/doctrine2.
I have a normal object (for example: type item) and this object has an id. If I look at the object at runtime I see that the id is set. Every other parameters like icon and amount are empty. I know, this is how lazy loading works but when I call the getters (getIcon) nothing happens. The icon attribute is still empty. I also tried to call the __load method but it doesn't help.
Sorry, forgot the code
class Character {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
/*********************************************************************
* Custom methods
*/
public function getItem() {
return $this->item;
}
}
And this is the object where the lazy loading not works.
class Item {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="integer")
*/
protected $amount;
/**
* #ORM\Column(type="string")
*/
protected $icon;
}
EDIT2:
Constructor of character class
public function __construct()
{
$this->item = new \Doctrine\Common\Collections\ArrayCollection();
}
So what the previos comments to your initial post are pointing at, is, that you need to implemend a ManyToOne relation in your Item entity to get all your stuff working.
In yout Character Entity you have this lines of code
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
This says you have a relation to an Entity Item which mappes the relation in the attribute "character". In this attribute the relation is stored. If you look into the database, you won't find any stored relations, because you class Item does not have the described mapping attribute character. Like gp_sflover pointed out, a OneToMany relations needs to be Bidirectional an required a ManyToOne relation in the "owning" side. So what you have to do is, add the following code to your Item Entity
/**
* #ORM\ManyToOne(targetEntity="Entity\Character", inversedBy="item")
*/
protected $character;
The inversedBy attribute creates a bidirectional relation. Without this statement, you wouldn't be able to load getItems from your Character entity.
If you have changed your code you have to update your database and to restore the elements. After this, everything will work fine.
Related
I simplified my 3 entities as much as possible below, it shows a simple relationship of Currency <- 1:1 -> Balance <- 1:N -> BalanceLog
Entity/Currency.php
/**
* #ORM\Entity(repositoryClass=CurrencyRepository::class)
*/
class Currency
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=3)
*/
private ?string $code;
/**
* #ORM\OneToOne(targetEntity="Balance", mappedBy="currency")
**/
private ?Balance $balance;
// ...
}
Entity/Balance.php
/**
* #ORM\Entity(repositoryClass=BalanceRepository::class)
*/
class Balance
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="Currency", inversedBy="balance")
* #ORM\JoinColumn(name="currency", referencedColumnName="code", nullable=false)
**/
private ?Currency $currency;
/**
* #ORM\OneToMany(targetEntity="App\Entity\BalanceLog", mappedBy="balance")
*/
private Collection $balance_logs;
// ...
}
Entity/BalanceLog.php
/**
* #ORM\Entity(repositoryClass=BalanceLogRepository::class)
*/
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Balance", inversedBy="balance_logs")
* #ORM\JoinColumn(name="balance_currency", referencedColumnName="currency")
**/
private ?Balance $balance;
// ...
}
The issue happens when I call:
$balanceLog = $this->getDoctrine()
->getRepository('App:BalanceLog')->findAll();
This hydrates the BalanceLog::$balance to the proper instance of Balance type, but it does not hydrate the BalanceLog::$balance->currency to Currency instance. Instead it wants to use string only
Resulting in error:
Typed property App\Entity\Balance::$currency must be an instance of App\Entity\Currency or null, string used
The dirty fix is to make Balance::$currency without fixed type of ?Currency. Then it will accept string and the code "works". But it is not correct. The Balance::$currency should be of Currency type, not sometimes string, sometimes currency.
I tried to make my own method in BalanceLogRepository, and for whatever reason this works just fine:
public function findByBalance(Balance $balance) : iterable
{
$query = $this->createQueryBuilder('bl');
$query->andWhere('bl.balance = :balance')
->setParameter('balance', $balance);
return $query->getQuery()->getResult();
}
So I am even more perplexed as to why the default findAll or findBy does not do recursive hydration
After further investigation I found a very weird behavior:
if I prepend this code:
$balance = $this->getDoctrine()->getRepository('App:Balance')->find('USD');
in front of
$balanceLog = $this->getDoctrine()->getRepository('App:BalanceLog')->findAll();
in my controller, then the error is gone. Its as if the App:Balance ORM schema of Balance with dependencies were not properly loaded until I try to fetch the Balance object directly apriori.
I did some debugging and it looks that BalanceLog does not create a full Balance Entity instance, but instead a Proxy. The solution was to add eager loading to the BalanceLog class
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Balance", inversedBy="balance_logs", fetch="EAGER")
* #ORM\JoinColumn(name="balance_currency", referencedColumnName="currency")
**/
private ?Balance $balance;
// ...
}
The UnitOfWork.php then does not use Proxy but instead loads the Entity as a whole.
If somebody wonders why querying Balance beforehand made the code work, its because of sophisticated caching mechanism of Doctrine. It saved Balance instance for primary key USD and then when BalanceLog was populated, it used this instance instead of creating a Proxy.
I still think that Proxy should not enforce strictly typed property from Entity though, but this is something for Doctrine developers to decide.
Why I can't delete entry in database using Doctrine 2 Entity manager?
I have next controller and entity with whom I have a problem.
I get in controller object form entity manager and i can't delete this object. Why?
// /Controller/Controller.php
/**
* Handler delete checkbox
* #Route("/administrator/services/delete/{id}", requirements={"id" = "\d+"}, defaults={"id" = 0}, name="service_delete")
* #Template()
*/
public function serviceDeleteAction(Request $request, $id){
$em = $this->getDoctrine()->getEntityManager();
$repoServices = $em->getRepository(CoworkingService::class);
$services = $repoServices->findOneBy(['id' => $id]);
$em->remove($services);
$em->persist($services);
$em->flush();
return [];//$this->redirectToRoute('administrator');
}
// /Entity/CoworkingService.php
class CoworkingService
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=50)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="SentviBundle\Entity\Language")
* #ORM\JoinColumn(name="language_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $language;
/**
* #ORM\Column(name="common_identifier", type="text")
*/
private $commonIdentifier;
Thanks!
#Matteo’s comment has already solved the issue, but let me explain what has happened.
You’re executing 3 entity manager operations:
$em->remove($services);
$em->persist($services);
$em->flush();
You must know that, before you call $em->flush(), all operations are registered in a service called the “unit of work” (UOW).
The UOW keeps track of all modifications in your entities (including adding/deleting entities), and only applies them to the database when you call flush().
When calling $em->remove($services), you told the UOW that you want to delete the entity. However, when calling $em->persist($services) directly afterwards, you told the UOW that you want to create (or, effectively: keep) the entity. (Note that, in Doctrine, “persist” doesn’t mean that a connection is made to the database, but instead, you pass an entity to the EM/UOW to calculate the modifications.)
So, in conclusion, the persist operation cancelled the remove out, and, at that point, flush had nothing to do.
For more details on the entity lifecycle and EM states see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html
I have an entity Project that is related with some files via one-to-many relationship like this:
class projects{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="files", mappedBy="project", cascade= {"persist","merge","remove"},orphanRemoval=true)
*/
protected $project_files;
}
And File entity:
class files{
private $files;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="projects", inversedBy="project_files")
*/
protected $project;
}
So when I edit a project, I want to be able to add some new files to it or delete some olds. I found a workaround to implement this but I can't figure out the way Symfony and Doctrine really handle this kind of operations. More precisely, my code (that works fine) that handles the deletion of some old files on edit is:
private function persistProject(Form $form) {
$project = $form->getData();
$raw_files = $project->getProjectFiles()->toArray();
$ready_files = self::setUpFiles($raw_files, $project);
$project->setProjectFiles($ready_files);
$em->merge($project);
$em->flush();
}
private function setUpFiles($project_files, $project) {
$ready_files = new ArrayCollection();
foreach ($project_files as $project_file){
if ((!empty($project_file->getId())) && ($project_file->getDescription() != '--del')){
$ready_files->add($project_file);
}
else if ((!empty($project_file->getId())) && ($project_file->getDescription() == '--del')){
unlink($this->uploadDir . $project_file->getPath());
// --- controversial line of code --- //
$project->removeProjectFile($project_file);
}
}
return $ready_files;
}
So in brief the situation is about this: if I remove line "$project->removeProjectFile($project_file);", the files are not deleted, even if my ArrayCollection $ready_files, that is ultimately stored to the object in line "$project->setProjectFiles($ready_files);", is exactly as it should be and does not contain the deleted files.
So my question is dual:
Shouldn't symfony persist the exact ArrayCollection (and only this) that I set and delete all the others files stored? Why does not the code work properly without the line "$project->removeProjectFile($project_file);"?
How does the line "$project->removeProjectFile($project_file);" affects the $project object in function persistProject() since the object is passed as a simple argument and not by reference? Isn't it obvious to expect that everything that happens to $project object in function setUpFiles() will do nothing to the original $project in function persistProject() as it is in different scope?
Can anyone help me to clear this up?
I have two entities - User and UserSettings. In User entity, I want to have UserSettings as an attribute. That would be OK, I would add a OneToOne relation but there's a problem - because UserSettings is an owning side of the relation, every time I load User entity, Doctrine has to load the UserSettings entity too.
Is there a way how to load User but not UserSettings?
I made maybe a weird solution - there's no relation between these entities and the settings are loaded by method of Facade. For example:
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $name;
/** #var UserSettings */
private $settings;
public function __construct()
{
$this->settings = new UserSettings();
}
public function setSettings(UserSettings $settings)
{
$this->settings = $settings;
}
}
/**
* #ORM\Entity
*/
class UserSettings
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(name="user_id", type="integer")
*/
private $userId;
}
class UserFacade
{
/**
* #var EntityManager
*/
private $em; // is injected automatically by DI
public function loadSettings(User $user)
{
$settings = $this->em->getRepository("UserSettings")->findOneBy(array("userId" => $user->id));
$user->setSettings($settings);
}
}
$user = $em->find("User", 1);
// if I want user's settings
$userFacade->loadSettings($user); // now I can use $user->getSettings()->something;
Side note: UserFacade is a service class that manipulates with users' data like adding new user, editing, deleting etc. In my MVC application, controller classes communicate with Facades, not with EntityManager directly.
That's OK - settings are loaded only when I want to. However, there are two possible problems:
a) I don't think this is a clear way
b) When I want a list of users, I cannot JOIN a table where settings are, because entities are not associated, so I have to make an extra SQL for each user.
My question is - how to solve the problem with OneToOne relation? I don't have much experience with Doctrine, so it may be a stupid question - sorry for that.
Thanks!
My problem is exactly that described in the Strategy Pattern article in Doctrine documentation :
A Page entity
A page can have some blocks
A block might be a text, an image, a form, a calendar, ... (strategy)
A page knows about blocks it contains, but doesn't know about their behaviours
Block inheritance is not possible
Described solution (Strategy pattern) seems exactly what I need (read article for further information) :
Page:
<?php
namespace Page\Entity;
class Page
{
/**
* #var int
* #Id #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #var string
* #Column
*/
protected $title;
/**
* #var string
* #Column(type="text")
*/
protected $body;
/**
* #var Collection
* #OneToMany(targetEntity="Block", mappedBy="page")
*/
protected $blocks;
// ...
}
Block:
<?php
namespace Page\Entity;
class Block
{
/**
* #Id #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #ManyToOne(targetEntity="Page", inversedBy="blocks")
*/
protected $page;
/**
* #Column
*/
protected $strategyClass;
/**
* Strategy object is instancied on postLoad by the BlockListener
*
* #var BlockStrategyInterface
*/
protected $strategyInstance;
// ...
}
Strategy interface:
<?php
namespace Page\BlockStrategy;
interface BlockStrategyInterface
{
public function setView($view);
public function getView();
public function setBlock(Block $block);
public function getBlock();
public function renderFrontend();
public function renderBackend();
}
I can easily imagine what would be my strategy if I would display a form or a calendar;
but what if my strategy is to display content of an other entity ?
The block needs to know about entity class/id and has to be deleted when related entity is removed.
I imagined to add entityClass and entityId properties in Block and load related entity on the postLoad event in a BlockListener.
But what if related entity doesn't exist ? I can't remove the block in postLoad.
So, I imagined to create an other listener watching for removal of related entity and remove refering Block in that listener.
But it means that I need to add a listener for every entity that can be put in a block.
It could work, but it seems very complicated... maybe someone has a better idea ?
I'm not sure if I understood well your question, but if what you want is to have entities inside entities and delete the child entities when the father is deleted, you could also treat the entity as a block and use a Composite pattern.
You basically could use the same interface on the entity and the blocks, and on the entity the display function could be something like:
foreach ($block in $blocks) {
$block->display();
}
For deleting all the children when you delete the parent entity, you could simply do it on the destructor of the entity.
function __destruct() {
foreach ($block in $blocks) {
/* call a common interface function that does all that need to be done, implemented on each block */
}
}
For more on Composite pattern:
http://en.wikipedia.org/wiki/Composite_pattern