How to query related entities based on their value - php

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

Related

Symfony Doctrine does not hydrate the whole chain

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.

Symfony : many-to-many with where clause on child

Many users can have many items. But I want to get a list of specific items owned by specifics users.
I have two entities :
User Entity :
class User
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Many Users have many items
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Item", fetch="EAGER")
* #ORM\JoinTable(name="users_items",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="item_id", referencedColumnName="id")}
* )
*/
private $items;
}
Item Entity :
class Item
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=10)
*/
private $name;
}
Doctrine then created three tables :
items
users
users_items
I want to get all the items named "Pencil" owned by userId 11.
How can I achieve that ?
Thank you.
I hope that you realize that you are using an UNIdirectional many-to-many relation between User and Item. User 'knows' what items belong to him but Item does not know which users use this item. Therefore you can retrieve a single User from the database (by user-id) with a collection of items that are filtered by name. If the user-id does not exist or the user has no items that match the filter, then $user will be NULL. instead of the exception in the controller in my example you could use a if-else statement in twig to verify if user is null or not.
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
/**
* #Route("/someroute", name="some_route")
*/
public function someAction()
{
$em = $this->getDoctrine()->getManager();
// for this example two hardcoded parameters. $user might be NULL !
$user = $em->getRepository('AppBundle:User')->getItemsWithNameForUser(1, 'Pencil');
if(null === $user)
{
throw $this->createNotFoundException('This user does not have the selected items or the user does not exist');
}
return $this->render('default/index.html.twig', [
'user' => $user
]);
}
}
and the UserRepository:
namespace AppBundle\Repository;
class UserRepository extends \Doctrine\ORM\EntityRepository
{
public function getItemsWithNameForUser($userId, $itemName)
{
return $this->getEntityManager()->createQuery(
'SELECT u,i FROM AppBundle:User u JOIN u.items i WHERE u.id=:id AND i.name=:name'
) ->setParameter('id', $userId)
->setParameter('name', $itemName)
->getOneOrNullResult();
}
}
I think it should be something like:
$items= $em->getDoctrine->createQueryBuilder()
->select('[items]')
->from(User::class, 'users')
->join('users.items', 'items')
->where('users.id = :userId')
->setParameter('userId', $theId)
->getQuery()
->getResult();
I am not sure this works (syntax is maybe not perfect), but theoretically it should, the problem in your case is that the owning side only knows about the relation and you want to query from the other side.

What is the best way to set/update association lookup field inside Doctrine entity

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.

Doctrine2 ORM OneToOne not working UPDATE changed to ManyToMany but not fully working

I have 2 tables called Advert and User. The User Id gets written into the Advert Table. I have now created a third table called Bookmark. The table got 2 columns, advert_id and user_id. When I see an Advert I like, I can add a Bookmark to be able to find it easier in my Private Section. When I am in my Private Section to see my Bookmarks, I want to see the Advert Details straight away, so in my Repository I want to create a Join to read the information from the Advert Table. I thought this will be a OneToOne Relationship. I also have to make sure that if the Advert gets deleted, then all the Bookmarks need to be deleted, so I thought its a Bi-directional relationship. So I have below:
Entity/Bookmark.php
/**
* Bookmark
*
* #ORM\Table(name="bookmark")
* #ORM\Entity(repositoryClass="Advert\Repository\BookmarkRepository")
*/
class Bookmark
{
/**
* #var integer
* #ORM\Id
* #ORM\Column(name="advert_id", type="integer", nullable=false)
* #ORM\OneToOne(targetEntity="Advert", mappedBy="bookmark")
* #ORM\JoinColumn(name="advert_id", referencedColumnName="id")
*/
private $advertId;
/**
* #var integer
* #ORM\Id
* #ORM\Column(name="user_id", type="integer", nullable=false)
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $userId;
public function setAdvertId($advertId)
{
$this->advertId = $advertId;
return $this;
}
public function getAdvertId()
{
return $this->advertId;
}
public function setUserId($userId)
{
$this->userId = $userId;
return $this;
}
public function getUserId()
{
return $this->userId;
}
Entity\Advert.php
/** Advert
*
* #ORM\Table(name="advert")
* #ORM\Entity(repositoryClass="Advert\Repository\AdvertRepository")
*/
class Advert
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="Bookmark", inversedBy="advert")
* #ORM\JoinColumn(name="id", referencedColumnName="advert_id")
**/
private $bookmark;
public function setBookmark($bookmark)
{
$this->bookmark = $bookmark;
return $this;
}
public function getBookmark()
{
return $this->bookmark;
}
public function addBookmark($bookmark)
{
$this->bookmark->add($bookmark);
}
public function removeBookmark($bookmark)
{
$this->bookmark->removeElement($bookmark);
}
Advert\Repository\Advert\Repository.php
class BookmarkRepository extends EntityRepository
{
public function getBookmarksByUserIds($userId)
{
$query =$this->_em->getRepository($this->getEntityName())->createQueryBuilder('b')
->join('b.advertId', 'a')
->andWhere('a.userId=:userid')
->setParameter('userid',$userId)
;
return $query->getQuery()->getResult();
}
What do I do wrong or where is my missunderstanding? I get the Error Message:
Advert\Entity\Bookmark has no association named advertId
As I said, the table Bookmark gets only filled, when I click on "Add Advert to Bookmarks". I need a Join to be able to display the Advert Details when I click on "Show my Bookmarks" and if an Advert or a User gets deleted, all Bookmarks need to be removed from the Bookmark table. Is this a OneToOne Bi-directional relationship and what is wrong?
UPDATE 1 BELOW NOT WORKING
I have updated the 2 Files below, but I do not get any Bookmarks shown. Instead I should see a list of Bookmarked adverts plus the advert details. I have not even tried yet to get my Service to "Bookmark Advert" or the method to check if an Advert is bookmarked working again. I got it working before, but I guess I am just really confused now.
AdvertController.php
public function watchlistAction()
{
$user_id = $this->zfcUserAuthentication()->getIdentity()->getId();
$adverts = $this->getEntityManager()->getRepository('Advert\Entity\User')->findBookmarksByUserId($user_id);
return new ViewModel(array(
'adverts' => $adverts,
));
}
Repository\UserRepository.php
class UserRepository extends EntityRepository
{
public function findBookmarksByUserId($userId)
{
$query =$this->_em->getRepository($this->getEntityName())->createQueryBuilder('b')
->join('b.bookmarks', 'a')
->join('b.adverts', 'c')
->andWhere('a.user=:userid')
->setParameter('userid',$userId)
;
return $query->getQuery()->getResult();
}
UPDATE 2 BELOW WORKING
You were right, I don't need the UserRepository Query see the List of Bookmarked Adverts. I just had to change the
AdvertController.php
public function watchlistAction()
{
$user_id = $this->zfcUserAuthentication()->getIdentity()->getId();
// get User by reference (no queries executed)
$user = $this->getEntityManager()->getReference('Advert\Entity\User' , $user_id);
$adverts = $user->getBookmarks();
return new ViewModel(array(
'adverts' => $adverts,
));
}
Good news also, in the moment I delete an Advert, the Bookmark gets automatically removed in the Bookmark Database table. Now I only have to find out how to add the Bookmark, so I will have to change my Service. As soon I get this working I will update this post for others to see.
UPDATE 3 BELOW NOT WORKING
Unfortunately I do not get the below 3 Methods in my Service working. I obviously have to now pick 1 record, to either check the Status (Bookmarked already or not), remove the Bookmark (defined by advertId) or add a Bookmark (defined by advertId)
public function checkAdvertBookmarkStatus($advertId)
{
$userId = $this->getUserEntity()->getId();
// get User by reference (no queries executed)
$user = $this->getEntityManager()->getReference('Advert\Entity\User' , $userId);
$bookmarkStatus = $this->getEntityManager()->getRepository('Advert\Entity\User')
->findOneBy(array('advert' => $advertId, 'userId' => $userId));
return $bookmarkStatus;
}
public function saveAdvertBookmark($advertId)
{
$bookmark = new UserEntity();
$userId = $this->getUserEntity()->getId();
// $bookmark->addBookmark($advertId);
$bookmark->setAdvertId($advertId);
$bookmark->setUserId($userId);
# write new bookmmark to database tbl bookmark
$this->getEntityManager()->persist($bookmark);
$this->getEntityManager()->flush();
}
public function removeAdvertBookmark($advertId)
{
$bookmark = new UserEntity();
$userId = $this->getUserEntity()->getId();
$bookmark = $this->getEntityManager()->getRepository('Advert\Entity\Bookmark')
->findOneBy(array('advertId' => $advertId, 'userId' => $userId));
# remove bookmmark from tbl bookmark
$this->getEntityManager()->remove($bookmark);
$this->getEntityManager()->flush();
}
I suppose the answer is in the Tutorial, which I keep reading, but I do not understand it fully. I was able to add Bookmarks before, when I was using the BookmarkEntity, but I have no idea how to do it via the UserEntity
A OneToOne relationship would be the wrong choice here, this would mean that a user can bookmark only one advert and that an advert can only be bookmarked by one user. Because a user should be able to bookmark many adverts and an advert should be bookmarked by many users, you need a ManyToMany relationship.
You idea to create a mapping table bookmarks is not wrong if you work with a database. However, you don't need to create it as an entity in Doctrine. You can simply add Adverts in an association called bookmarks in User to display the bookmarked Adverts and vice versa:
User Entity:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Application\Entity\Advert", inversedBy="bookmarks", cascade={"persist"})
* #ORM\JoinTable(name="bookmarks",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="advert_id", referencedColumnName="id")}
* )
*/
private $bookmarks;
Advert Entity
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Application\Entity\User", mappedBy="bookmarks", cascade={"persist"})
* #ORM\JoinTable(name="bookmarks",
* joinColumns={#ORM\JoinColumn(name="advert_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")}
* )
*/
private $bookmarks;
You might want to read this article, too:
http://doctrine-orm.readthedocs.org/en/latest/reference/working-with-associations.html
Edit: How to add and remove bookmarks
Associations in Doctrine are something entirely different from fields, although both are properties in your Entity. To handle bookmarks you add or remove the Advert Entity in your User Entity directly. For example:
$bookmarks = $user->getBookmarks();
$bookmarks[] = $advert;
This would add a bookmark to the user and will be stored as soon as you persist and flush. To make this even easier, you can define remover and adder:
Use statements:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
and the code:
/**
* #param Collection $bookmarks
*/
public function addBookmarks(Collection $bookmarks)
{
foreach ($bookmarks as $bookmark) {
$this->bookmarks->add($bookmark);
}
}
/**
* #param Collection $bookmarks
*/
public function removeBookmarks(Collection $bookmarks)
{
foreach ($bookmarks as $bookmark) {
$this->bookmarks->removeElement($bookmark);
}
}
You can now remove and add adverts given in collection like this:
$user->addBookmarks(new ArrayCollection(array($advert)));
It is always recommended to define adder and remover in toMany relationships, because many Doctrine components will need them, for example the very useful DoctrineObject, a hydrator used by DoctrineModule for Zend 2

Filtering on many-to-many association with Doctrine2

I have an Account entity which has a collection of Section entities. Each Section entity has a collection of Element entities (OneToMany association). My problem is that instead of fetching all elements belonging to a section, I want to fetch all elements that belong to a section and are associated with a specific account. Below is my database model.
Thus, when I fetch an account, I want to be able to loop through its associated sections (this part is no problem), and for each section, I want to loop through its elements that are associated with the fetched account. Right now I have the following code.
$repository = $this->objectManager->getRepository('MyModule\Entity\Account');
$account = $repository->find(1);
foreach ($account->getSections() as $section) {
foreach ($section->getElements() as $element) {
echo $element->getName() . PHP_EOL;
}
}
The problem is that it fetches all elements belonging to a given section, regardless of which account they are associated with. The generated SQL for fetching a section's elements is as follows.
SELECT t0.id AS id1, t0.name AS name2, t0.section_id AS section_id3
FROM mydb.element t0
WHERE t0.section_id = ?
What I need it to do is something like the below (could be any other approach). It is important that the filtering is done with SQL.
SELECT e.id, e.name, e.section_id
FROM element AS e
INNER JOIN account_element AS ae ON (ae.element_id = e.id)
WHERE ae.account_id = ?
AND e.section_id = ?
I do know that I can write a method getElementsBySection($accountId) or similar in a custom repository and use DQL. If I can do that and somehow override the getElements() method on the Section entity, then that would be perfect. I would just very much prefer if there would be a way to do this through association mappings or at least by using existing getter methods. Ideally, when using an account object, I would like to be able to loop like in the code snippet above so that the "account constraint" is abstracted when using the object. That is, the user of the object does not need to call getElementsByAccount() or similar on a Section object, because it seems less intuitive.
I looked into the Criteria object, but as far as I remember, it cannot be used for filtering on associations.
So, what is the best way to accomplish this? Is it possible without "manually" assembling the Section entity with elements through the use of DQL queries? My current (and shortened) source code can be seen below. Thanks a lot in advance!
/**
* #ORM\Entity
*/
class Account
{
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="MyModule\Entity\Section")
* #ORM\JoinTable(name="account_section",
* joinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="section_id", referencedColumnName="id")}
* )
*/
protected $sections;
public function __construct()
{
$this->sections = new ArrayCollection();
}
// Getters and setters
}
/**
* #ORM\Entity
*/
class Section
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="MyModule\Entity\Element", mappedBy="section")
*/
protected $elements;
public function __construct()
{
$this->elements = new ArrayCollection();
}
// Getters and setters
}
/**
* #ORM\Entity
*/
class Element
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var Section
* #ORM\ManyToOne(targetEntity="MyModule\Entity\Section", inversedBy="elements")
* #ORM\JoinColumn(name="section_id", referencedColumnName="id")
*/
protected $section;
/**
* #var \MyModule\Entity\Account
* #ORM\ManyToMany(targetEntity="MyModule\Entity\Account")
* #ORM\JoinTable(name="account_element",
* joinColumns={#ORM\JoinColumn(name="element_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")}
* )
*/
protected $account;
// Getters and setters
}
If I understand correctly, you want to be able to retrieve all Elements of all Sections of an Account, but only if those Elements are associated with that Account, and this from a getter in Account.
First off: An entity should never know of repositories. This breaks a design principle that helps you swap out the persistence layer. That's why you cannot simple access a repository from within an entity.
Getters only
If you only want to use getters in the entities, you can solve this by adding to following 2 methods:
class Section
{
/**
* #param Account $accout
* #return Element[]
*/
public function getElementsByAccount(Account $accout)
{
$elements = array();
foreach ($this->getElements() as $element) {
if ($element->getAccount() === $account) {
$elements[] = $element->getAccount();
}
}
return $elements;
}
}
class Account
{
/**
* #return Element[]
*/
public function getMyElements()
{
$elements = array()
foreach ($this->getSections() as $section) {
foreach ($section->getElementsByAccount($this) as $element) {
$elements[] = $element;
}
}
return $elements;
}
}
Repository
The solution above is likely to perform several queries, the exact amount depending on how many Sections and Elements are associated to the Account.
You're likely to get a performance boost when you do use a Repository method, so you can optimize the query/queries used to retrieve what you want.
An example:
class ElementRepository extends EntityRepository
{
/**
* #param Account $account [description]
* #return Element[]
*/
public function findElementsByAccount(Account $account)
{
$dql = <<< 'EOQ'
SELECT e FROM Element e
JOIN e.section s
JOIN s.accounts a
WHERE e.account = ?1 AND a.id = ?2
EOQ;
$q = $this->getEntityManager()->createQuery($dql);
$q->setParameters(array(
1 => $account->getId(),
2 => $account->getId()
));
return $q->getResult();
}
}
PS: For this query to work, you'll need to define the ManyToMany association between Section and Account as a bidirectional one.
Proxy method
A hybrid solution would be to add a proxy method to Account, that forwards the call to the repository you pass to it.
class Account
{
/**
* #param ElementRepository $repository
* #return Element[]
*/
public function getMyElements(ElementRepository $repository)
{
return $repository->findElementsByAccount($this);
}
}
This way the entity still doesn't know of repositories, but you allow one to be passed to it.
When implementing this, don't have ElementRepository extend EntityRepository, but inject the EntityRepository upon creation. This way you can still swap out the persistence layer without altering your entities.

Categories