How to fetch doctrine2 relations conditionally - php

I have three entities like following:
1. Customer.php
<?php
//...
/**
* Customer
*
* #ORM\Table(name="customers")
* #ORM\Entity(repositoryClass="CompanyBundle\Repository\CustomerRepository")
* #ORM\HasLifecycleCallbacks
*/
class Customer
{
/**
* #ORM\OneToMany(targetEntity="CustomerAddress", mappedBy="customer")
*/
private $customerAddresses;
// ...
}
?>
2. CustomerAddress.php
<?php
//...
/**
* CustomerAddress
*
* #ORM\Table(name="customer_address")
* #ORM\Entity(repositoryClass="CompanyBundle\Repository\CustomerAddressRepository")
*/
class CustomerAddress
{
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="customerAddresses")
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $customer;
/**
* #ORM\ManyToOne(targetEntity="CustomerAddressType", inversedBy="customerAddresses")
* #ORM\JoinColumn(name="customer_address_type_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $customerAddressType;
//...
}
3. CustomerAddressType.php
<?php
//...
/**
* CustomerAddressType
*
* #ORM\Table(name="customer_address_type")
* #ORM\Entity(repositoryClass="CompanyBundle\Repository\CustomerAddressTypeRepository")
*/
class CustomerAddressType
{
/**
* #ORM\OneToMany(targetEntity="CustomerAddress", mappedBy="customerAddressType")
*/
private $customerAddresses;
//...
}
Here are the rows from table customer_address_type
I want to get all customer addresses of type either 'BA' or 'SA". So I want to remove all other type except these two. Basically I want to do something similiar like query scope in Laravel.
foreach ($customers as $customer) {
// Here I want to filter customer addresses
// Currently its giving me all
$customer_address = $customer->getCustomerAddresses();
}
Is it possible to do like so without using custom query?

You can get an ArrayCollection with all the addresses and then use filter method to get only the ones that you want. You can do it inside the Customer entity so you can reuse it for serialization.
You have to pass a closure to the filter method, and then it iterates over the collection evaluating the closure that should renturn true when you want to include the item in the result or false if not.

Related

Why is my PersistentCollection empty?

I'm using the Symfony 3 Framework with Doctrine and MongoDB.
I've two documents that are in an OneToMany relationship.
/**
* Class Waypoint
* #package AppBundle\Document
* #MongoDB\Document(collection="waypoints", repositoryClass="AppBundle\Repository\WaypointRepository")
*/
class Waypoint
{
/**
* #var int
*
* #MongoDB\Id(strategy="auto")
*/
private $id;
/**
* #var ArrayCollection
* #MongoDB\ReferenceMany(targetDocument="Comment", cascade={"delete"})
*/
private $comments;
}
**
* Class Comment
* #package AppBundle\Document
* #MongoDB\Document(collection="comments", repositoryClass="AppBundle\Repository\CommentRepository")
*/
class Comment
{
/**
* #var int
*
* #MongoDB\Id(strategy="auto")
*/
private $id;
/**
* #var Waypoint
*
* #MongoDB\ReferenceOne(targetDocument="Waypoint", inversedBy="comments")
* #Assert\NotBlank()
*/
private $waypoint;
}
Now I'm getting a part of my Waypoint entries from an repository query and want to display them with twig.
/**
* WaypointRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class WaypointRepository extends DocumentRepository
{
public function getWaypointsForCruiseByPage(Cruise $cruise, $page)
{
$displayLimit = 10;
$amountToSkip = 0;
if ($page > 1)
{
$amountToSkip = ($page -1) * $displayLimit;
}
$qb = $this->createQueryBuilder()
->select()
->field('cruise')->equals($cruise)
->field('isAutoWaypoint')->equals(false)
->sort('date', -1)
->skip($amountToSkip)
->limit($displayLimit)
;
$qb
->addOr($qb->expr()->field('hasImage')->equals(true))
->addOr($qb->expr()->field('hasAudio')->equals(true))
->addOr($qb->expr()->field('description')->notEqual(''))
;
return $qb->getQuery()->toArray();
}
}
Now, I'm trying to do {{ waypoint.comments.count }} or {{ waypoint.comments|length }} will always be 0, even if I'm having datasets in my MongoDB collection.
If I'm getting the comments over the CommentRepository by the ID of the Waypoint I'm getting the expected results.
// returns the expected results
public function getAllCommentsForWaypoint(Waypoint $waypoint)
{
return $this->createQueryBuilder()
->select()
->field('waypoint')->equals($waypoint)
->getQuery()->toArray()
;
}
The mapping is fine as far as I can tell, no flaws or errors to find.
Why is the PersistentCollection empty, event though informations are there in the collection?
I'm not sure how are you creating documents, but this is my best shot:
Waypoint::$comments is not mapped as an inverse side thus ODM expects list of references to be available in the waypoint.comments field in the database. Most probably it's not there (i.e. you're not explicitly adding new Comment to the collection in the Waypoint) and that's why you're seeing empty collection when querying for waypoints, but have results when querying for comments. Given you have inversedBy="comments" in the Comment mapping I think you forgot to set the Waypoint::$comments as the inverse side:
/**
* #var ArrayCollection
* #MongoDB\ReferenceMany(targetDocument="Comment", mappedBy="waypoint")
*/
private $comments;

create method to generate default value with prefix and auto-increlmentation directly in entity

In my Symfony project I have this entity named Order.php:
<?php
namespace myApp\EntityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Datetime;
/**
*
* #ORM\Table(name="`order`")
* #ORM\Entity(repositoryClass="myApp\EntityBundle\Repository\OrderRepository")
*
*/
class Order
{
/**
* #var integer
*
* #ORM\Column(name="order_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* Command name ref for right accounting
*
* #var string
*
* #Assert\Type(
* type="string",
* message="The value {{ value }} is not a valid {{ type }}."
* )
* #Assert\NotNull()
* #Assert\NotBlank()
* #ORM\Column(name="order_name", type="string", length=255, nullable=true, unique=true)
*/
protected $orderName;
// etc other associations, object and getters setters...
/**
* Constructor
*/
public function __construct()
{
$this->generateOrderName();
}
/**
* generate order_name with base ref
*
* #return Order
*/
public function generateOrderName()
{
// trying to make the good practice for generated orderName value
//$now = new Datetime();
//$year = $now->format("Y");
//$month = $now->format("m");
//$orderNameCommon = $year."_".$month."_";
$this->setOrderName();
return $this;
}
/**
* Set orderName
*
* #param string $orderName
*
* #return Order
*/
public function setOrderName($orderName)
{
$this->orderName = $orderName;
return $this;
}
/**
* Get orderName
*
* #return string
*/
public function getOrderName()
{
return $this->orderName;
}
}
The purpose is to generate an orderName like this: '2016_09_00001' which means that I registered order by date Year_Month_orderNumber.
I have to take care that the orderName have to be unique, and for example if I have already an order named '2016_09_00001', I have to register the orderName like this '2016_09_00002'. Like an auto-incrementation for example.
As you can see, I have made this code directly in entity class Order:
/**
* Constructor
*/
public function __construct()
{
// call the order name generation function
$this->generateOrderName();
}
/**
* generate order_name with base ref
*
* #return Order
*/
public function generateOrderName()
{
$now = new Datetime();
$year = $now->format("Y");
$month = $now->format("m");
$prefix = $year."_".$month."_";
// That I had planned:
// to create a prefix with Datetime argument, year and month
// Then check all order in database with this prefix
// Finally count the results and add +1 on the final value
$this->setOrderName();
return $this;
}
As you can see, I have called the generateOrderName() function in the constructor, and then I would like to make this process:
That I had planned:
to create a prefix with Datetime argument, year and month
Then check all order in database with this prefix
Finally count the results and add +1 on the final value
What is the best way to make this properly in the entity directly? I think it's not a good thing to have a repository dql query call in the entity class itself.
I have create this one for example:
public function searchOrderName($prefix)
{
return
$this->getEntityManager()
->createQuery(
"SELECT o.orderName FROM EntityBundle:Order o
WHERE o.orderName
LIKE :prefix
ORDER BY o.id ASC"
)
->setParameter("prefix", $prefix.'%')
->getResult();
}
How can I proceed to create the generateOrderName() function in the entity to make the generated process I would like directly in entity class ?
You will need a service for it. You cannot access DB from your Entity. So make OrderFactory, inject there OrderRepository and you will easily reach your goal

Failed to set up a ManyToMany

I want to be able to select a school (that has its own entity) while creating a mission (also has its entity)
Since a school can have several missions, and you can select several schools at the mission's creation, I used a ManyToMany.
The problem is that after creating this "ManyToMany", generating the entities and updating my schema, Symfony created a table, but left it totally empty, without the two columns that I asked for. I'm not really used to Symfony nor to the ManyToMany system, so I might have done some mistake without noticing it, still I find this weird.
Here's the interesting part of my ecole (school) entity:
class Ecole{
// ...
/**
* #ORM\ManyToMany(targetEntity="MissionBundle\Entity\Mission", mappedBy="ecolesDispo")
*/
protected $missionsDispos;
// ...
/**
* Add missionsDispo
*
* #param \MissionBundle\Entity\Mission $missionsDispo
*
* #return Ecole
*/
public function addMissionsDispo(\MissionBundle\Entity\Mission $missionsDispo)
{
$this->missionsDispos[] = $missionsDispo;
return $this;
}
/**
* Remove missionsDispo
*
* #param \MissionBundle\Entity\Mission $missionsDispo
*/
public function removeMissionsDispo(\MissionBundle\Entity\Mission $missionsDispo)
{
$this->missionsDispos->removeElement($missionsDispo);
}
/**
* Get missionsDispos
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMissionsDispos()
{
return $this->missionsDispos;
}
And here is the interesting part of my mission entity:
/**
* #ORM\ManyToMany(targetEntity="EcoleBundle\Entity\Ecole", inversedBy="missionsDispo")
* #ORM\JoinTable(name="Mission2Ecole",
* joinColumns={#ORM\JoinColumn(name="em_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="me_id", referencedColumnName="id")}
* )
*/
protected $ecolesDispo;
// ...
/**
* Constructor
*/
public function __construct()
{
$this->ecolesDispo = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add ecolesDispo
*
* #param \EcoleBundle\Entity\Ecole $ecolesDispo
*
* #return Mission
*/
public function addEcolesDispo(\EcoleBundle\Entity\Ecole $ecolesDispo)
{
$this->ecolesDispo[] = $ecolesDispo;
return $this;
}
/**
* Remove ecolesDispo
*
* #param \EcoleBundle\Entity\Ecole $ecolesDispo
*/
public function removeEcolesDispo(\EcoleBundle\Entity\Ecole $ecolesDispo)
{
$this->ecolesDispo->removeElement($ecolesDispo);
}
/**
* Get ecolesDispo
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getEcolesDispo()
{
return $this->ecolesDispo;
}
After all this was created, I was supposed to get a multi selector with the list of all the schools saved in the database (I already added it to the missionType file), but I get absolutely nothing.
I don't really know if I inverted the annotations, or if the "joinTable" part is correct, but I'm completely lost here.
Does anyone have an idea?
Thank you in advance
Just wrong typo "s"? inversedBy="missionsDispo" >>> inversedBy="missionsDispos"
PS. Official doc here
http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/association-mapping.html#many-to-many-bidirectional

Add brands through company, it's possible? How?

I have this two tables (see pics below) mapped as follow:
class Brand
{
...
/**
* #var Company
*
* #ORM\ManyToOne(targetEntity="Company")
* #ORM\JoinColumn(name="companies_id", referencedColumnName="id")
*/
protected $company;
}
class Company
{
...
}
I need to add support for add a new Brand from Company but I have not idea in how to achieve this. This are handled through SonataAdminBundle but I think I need to add something else to entities in order to create brands from company but I am not sure what this would be, can I get some help? I am stucked
1st attempt
After get an answer this is how I modify Company entity:
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Company
{
...
/**
* #var Brand
* #ORM\OneToMany(targetEntity="Brand", mappedBy="company", cascade={"persist"})
**/
protected $brands;
public function __construct()
{
$this->brands = new ArrayCollection();
}
...
public function getBrands()
{
return $this->brands;
}
/**
* Add brands
*
* #param Brand $brand
* #return Brands
*/
public function addBrand( Brand $brand)
{
$this->brands[] = $brand;
return $this;
}
/**
* Remove brands
*
* #param Brand $brand
*/
public function removeBrand( Brand $brand)
{
$this->brands->removeElement($brand);
}
}
But I am getting this error:
No entity manager defined for class
Doctrine\Common\Collections\ArrayCollection
Why is that?
You could try setting up your entities like this:
class Brand
{
/**
* #var Company
*
* #ORM\ManyToOne(targetEntity="Company", inversedBy="brands")
* #ORM\JoinColumn(name="companies_id", referencedColumnName="id")
*/
protected $company;
}
class Company
{
/**
* #var ArrayCollection
*
* #OneToMany(targetEntity="Brand", mappedBy="company", cascade={"persist"})
**/
protected $brands;
}
What we're defining here is that new Brands can be created from the Company entity with cascade={"persist"}.
It's recommended you implement addBrand and removeBrand in Company for direct interaction with the ArrayCollection.
A simple example of the final functionality:
$company = $service->getCompany(1); // our company entity
$brand = new Brand();
$brand->set...
...
$company->addBrand($brand);
$entityManager->persist($company);
EDIT
This is just an example, you may choose not to add with keys or even implement a remove function, but this is a starting point:
public function addBrand(Brand $brand)
{
// key needs to be something that can uniquely identify the brand
// e.g. name
$this->getBrands()->set(*key*, $brand);
return $this;
}
public function removeBrand($key)
{
$this->getBrands()->remove($key);
return $this;
}

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