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.
Related
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
Using Doctrine, I am being presented the following error:
[2016-09-14 21:24:44] request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\DBALException: "Unknown column type "varchar" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the known types with \Doctrine\DBAL\Types\Type::getTypeMap(). If this error occurs during database introspection then you might have forgot to register all database types for a Doctrine Type. Use AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement Type#getMappedDatabaseTypes(). If the type name is empty you might have a problem with the cache or forgot some mapping information." at /var/www/project/apps/ProjectName/trunk/vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php line 114 {"exception":"[object] (Doctrine\\DBAL\\DBALException(code: 0): Unknown column type \"varchar\" requested. Any Doctrine type that you use has to be registered with \\Doctrine\\DBAL\\Types\\Type::addType(). You can get a list of all the known types with \\Doctrine\\DBAL\\Types\\Type::getTypeMap(). If this error occurs during database introspection then you might have forgot to register all database types for a Doctrine Type. Use AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement Type#getMappedDatabaseTypes(). If the type name is empty you might have a problem with the cache or forgot some mapping information. at /var/www/project/apps/ProjectName/trunk/vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php:114)"} []
the relevant class looks as such
<?php
namespace Project\DBALBundle\Entity\Url;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMSS;
/**
* Profile
*
* #ORM\Table(name="Profile")
* #ORM\Entity(repositoryClass="Project\DBALBundle\Entity\Url\ProfileRepository")
*/
class ProfileRepository {
/**
* #var string $id
*
* #ORM\Column(name="id", type="integer", length=11, nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $label
*
* #ORM\Column(name="label", type="string", length=50, nullable=false)
*/
private $label;
/**
* #return string
*/
public function getId()
{
return $this->id;
}
/**
* #param string $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return string
*/
public function getLabel()
{
return $this->label;
}
/**
* #param string $label
*/
public function setLabel($label)
{
$this->label = $label;
}
public function __toString()
{
return $this->label;
}
}
With the above class and annotation-defined mappings, I receive the error. However, if I change the field private $label to private $labelField and update the associated references, everything works just fine and the data is accessed as expected.
Insofar as I have been able to search, there is nothing special about the field private $label. It is not a reserved keyword, and I can find nothing mentioning anything special about it either with PHP itself or Doctrine specifically. So why does this break?
My guess will be it's a caching problem. You had probably tried at some point this code,
/**
* #var string $label
*
* #ORM\Column(name="label", type="varchar", length=50, nullable=false)
*/
private $label;
and this class has got cached by opcache (or similar). Opcache pays no attention to annotations (it's just a comments for it), so no matter what you change in annotations, it's still using this cached version of the code.
But when you change a property name, it realizes that it's a newer version of class and parses annotations once again (that's why with labelField code worked).
But that's just a speculation. I would try to debug it with Xdebug to find out an exact problem.
P.S. Doctrine version 2.3 is quite old, isn't it?
I have the main entity
/**
* #ORM\Entity()
*/
class Document
{
/**
* #var int
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var DocumentStatus
* #ORM\ManyToOne(targetEntity="DocumentStatus")
*/
private $status;
/**
* #var string
* #ORM\Column(type="text")
*/
private $text;
}
and the lookup "enum" entity (seeding on application deploy)
/**
* #ORM\Entity(repositoryClass="DocumentStatusRepository");
*/
class DocumentStatus
{
const DRAFT = 'draft';
const PENDING = 'pending';
const APPROVED = 'approved';
const DECLINED = 'declined';
/**
* #var int Surrogate primary key
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string Natural primary key (name for developers)
* #ORM\Column(type="string", unique=true)
*/
private $key;
/**
* #var string Short name for users
* #ORM\Column(type="string", unique=true)
*/
private $name;
/**
* #var string Full decription for users
* #ORM\Column(type="string", nullable=true, unique=true)
*/
private $description;
}
with simple repository
class DocumentStatusRepository extends EntityRepository
{
public function findOneByKey($key)
{
return parent::findOneBy(['key' => $key]);
}
}
I want to encapsulate domain logic of document lifecycle by intoducing methods like
public function __construct($text)
{
$this->text = $text;
$this->status = $something->getByKey(DocumentStatus::DRAFT);
}
public function approve()
{
try {
$this->doSomeDomainActions();
$this->status = $something->getByKey(DocumentSatus::DRAFT);
} catch (SomeDomainException($e)) {
throw new DocumentApproveException($e);
}
}
...
or
public function __construct($text)
{
$this->text = $text;
$this->status = $something->getDraftDocumentStatus()
}
public function approve()
{
$this->status = $something->getApprovedDocumentStatus()
}
...
without public setters. Also I want keep Document loose coupling and testable.
I see the next ways:
Permanent inject DocumentStatusRepository (or service that encapsulates it) into every instance by constructor on entity creation and by using public setter in postLoad subscriber
Permanent inject DocumentStatusRepository by static Document method on application bootstrap or on loadClassMetadata
Temporary inject DocumentStatusRepository in constructor and methods like Document::approve
Use setStatus() method with complex logic based on $status->key value
Encapsulate document domain logic in some DocumentManager and use Document entity like simple data storgae or DTO :(
Are there other ways? Which way is easier and more convenient to use in the long term?
Using generated Identities for Document.
Now you generate the identity on the side of the database. So you save Document from the domain perspective in inconsistent state. Entity/Aggregate should be identified, if it has no id it shouldn't exists.
If you really want to keep to database serials, add method to the repository which will generate id for you.
Better way is to use uuid generator for example ramsey/uuid.
And inject the id to the constructor.
DocumentStatus as Value Object
Why Document Status is Entity? It does look like a simple Value Object.
Then you can use Embeddable annotation. So it will leave within same table in the database, no need for doing inner joins.
DocumentStatus gets behaviour for example ->draftDocumentStatus(), which returns NEW DocumentStatus with draft status, so you can switch the old instance one with new one. ORM will do the rest.
DocumentStatusRepository
If you really want to keep DocumentStatus as entity, which in my opinion is wrong you shouldn't have DocumentStatusRepository.
Document is your aggregate root and the only entrance to the DocumentStatus, should be by aggregate root.
So you will have DocumentRepository only, which will be responsible for rebuilding the whole aggregate and saving it.
Also you should change the mapping.
It should have FETCH=EAGER type, so it will retrieve DocumentStatus with Document together.
Secondly you should do mapping with CASCADE=ALL and ORPHANREMOVAL=TRUE.
Otherwise, if you remove Document, DocumentStatus will stay in the database.
I have Ads on my website. For an Ad to be publicly displayed, it must be paid for. An ad is public or paid for if it has no payment that is unpaid, or all its payments are paid for.
What I want is to retrieve all public ads, but that requires a large number of queries using this method. I'm looking for a better way.
This has more fields, but let's focus on the payments field. It's a OneToMany.
class Ad
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255,nullable=true)
*/
private $adTitle;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=255,nullable=true)
*/
private $description;
/**
* #ORM\ManyToOne(targetEntity="Application\FOS\UserBundle\Entity\User")
* #ORM\JoinColumn(name="advertiser")
*/
private $advertiser;
/**
* #ORM\OnetoMany(targetEntity="MyBundle\Entity\Payment",mappedBy="ad",cascade={"persist"})
* #ORM\JoinColumn(name="payments", referencedColumnName="id",nullable=true)
*/
private $payments;
}
For payments, I'm using PayumBundle, so the entity looks like so:
use Payum\Core\Model\Payment as BasePayment;
class Payment extends BasePayment
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer $id
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\Ad",inversedBy="payments",cascade={"persist"})
* #ORM\JoinColumn(name="ad", referencedColumnName="id",nullable=true)
*/
protected $ad;
}
Currently I'm using a service that does something like this:
public function getAllPublicAds()
{
$allAds = $this->em->getRepository('MyBundle:Ad')->findAll();
$publicAds=array();
foreach($allAds as $ad){
if($this->isPublicAd($ad)) array_push($publicAds,$ad);
}
return $publicAds;
}
I'm getting all the ads, and if they are public, I put them into an array which will later go to the twig view.
An Ad is public if all of its payments are paid, therefore:
public function isPublicAd(Ad $ad)
{
$payments = $ad->getPayments();
if (!count($payments)) {
return false;
} else {
foreach ($payments as $payment) {
if (!$this->paidPayment($payment)) return false;
}
return true;
}
}
And finally, a paid Payment is paid if its details field 'paid' in the table is true. This is how PayumBundle works. It creates a token, then after you use that token to pay for your object, it fills that object's payment field details with details about the payment. You can find the class here. My Payment class from above extends it. So I'm checking whether those details have been filled, or if the 'paid' field is true.
public function paidPayment(Payment $payment)
{
$details = $payment->getDetails();
if (array_key_exists("paid", $details) && $details["paid"]) return true;
else return false;
}
The problem is that this creates a very big and unnecessary number of queries to the database. Is it possible to shorten this out? I'm not sure I can use normal queries without Doctrine, because I need to check the details['paid'] field, whcih I can only do in a controller or service, after I've received the database results. If that is even possible to do inside a query, I don't know either nor seem to find a way to do it.
You should use QueryBuilder instead of findAll method. In such case you can specify conditions which need to be match (just like in plain sql).
So:
$paidAds = $this->em->getRepository('MyBundle:Ad')
->createQueryBuilder('a')
->innerJoin('a.Payments', 'p')
->getQuery()
->getResult();
I didn't analyze deep your db structure by in fact innerJoin might be only condition which you need since you will get only those Ads which have relation to payments
To read more about QueryBuilder check doc
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!