List entities belonging to an association in Doctrine - symfony

Let's say I have a "Person" entity. A person can belong to a "Group". They are associated through a ManyToMany, Join Table strategy.
The general code looks like this:
/**
* Vendor\AcmeBundle\Entity\Person
*
* #ORM\Entity(repositoryClass="Vendor\AcmeBundle\Entity\PersonRepository")
*/
class Person extends BaseUser
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Vendor\AcmeBundle\Entity\Group")
*/
protected $groups;
}
and the group entity
/**
* #ORM\Entity
*/
class Group extends BaseGroup
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", nullable=true)
*/
protected $publicName;
}
What do I want to achieve?
Given a group, list users belonging to that group in a consistent way, including pagination options (aka limit and offset)
Something like this:
function getUserFromGroup(Group $group, $criteria, $limit, $offset){};
Considerations:
The entities are mutable, they can be adapted to achieve this requisite (e.g. the association could be changed from unidirectional to bidirectional)
The amount of person entities is in the thousands (2000~8000)
The amount of groups is less than 10

This is explained in the Symfony2 Book, chapter on Doctrine
For your case, I would suggest using the findBy() method.
From the official doctrine documentation:
function getUserFromGroup($group, $criteria, $limit, $offset){
// You should probably build the criteria into a paramaters array,
// but I'll just asume it's "fieldName" => "valueToFilterBy"
$criteria['groups'] = $group;
$users = $em->getRepository('AppBundle\Entity\User')
->findBy(
$criteria, // Filter by columns
array('name' => 'ASC'), // Sorting
$limit, // How many entries to select
$offset // Offset
);
return $users;
};

I would not use the associations to list group's members, but a custom repository call. This should be close enough:
class PersonRepository extends EntityRepository
{
public function findPeopleInGroup(Group $group, $criteria, $limit, $offset){
$qb = $this->createQueryBuilder('p');
$qb->join('p.groups', 'g')
->where(':group MEMBER OF p.groups')
->setParameter('group', $group)
->orderBy('p.'.$criteria);
$qb->setFirstResult($offset);
$qb->setMaxResults($limit);
return $qb->getQuery()->getResult();
}
}

Related

Sort a doctrine's #OneToMany ArrayCollection by field

Close question was enter link description here but I need to more deep sorting:
/**
* #var ArrayCollection[SubjectTag]
*
* #ORM\OneToMany(targetEntity="SubjectTag", mappedBy="subject")
* #ORM\OrderBy({"position" = "ASC"})
* #Assert\Valid()
*/
protected $subjectTags;
In subjectTag I have:
/**
* #var ArrayCollection[tag]
*
* #ORM\OneToMany(targetEntity="Tag", mappedBy="subject")
* #ORM\OrderBy({"name" = "ASC"})
* #Assert\Valid()
*/
protected $tags;
Now I want to sort by SubjectTag.tags. How can I do that?
EDIT:
Entity1.php:
/**
* #ORM\ManyToOne(targetEntity="Entity2", referencedColumnName="id", nullable=false)
* #Assert\Valid()
*/
protected $entity2;
Entity2.php:
/**
* #ORM\ManyToOne(targetEntity="Entity3", referencedColumnName="id", nullable=false)
* #Assert\Valid()
*/
protected $entity3;
Entity3.php:
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $position;
And now.. I want have in Entity1 Entity2 sorted by position. How can I do that by default?
As explained in my previous comment, you should do a custom query in your repository class corresponding to your base Entity (You didn't give the name of it).
So in your App\Repository\"YourBaseENtityName"Repository class, you do something like this.
public function findOrderByTags()
{
return $this
->createQueryBuilder('baseEntityAlias')
->addSelect('st')
->addSelect('t')
->leftJoin('baseEntityAlias.subjectTags', 'st')
->leftJoin('st.tags', 't')
->orderBy('st.position', 'ASC')
->addOrderBy('t.name', 'ASC')
->getQuery()
->getResult();
}
Moreover, I'm not sure about what kind of order you want to perform based on your question. Here the baseEntity->subjectTags will be ordered by their positions and then the baseEntity->subjectTags->tags will be ordered by name.
Now you can call this method from your base entity repository class
Hope it will be helpful for you.
EDIT:
Here is a way to introduce a default behavior for your queryBuilder and reuse it.
/**
* In your EntityRepository add a method to init your query builder
*/
public function createDefaultQueryBuilder(string $alias = 'a')
{
return $this
->createQueryBuilder($alias)
->addSelect('st')
->addSelect('t')
->leftJoin('baseEntityAlias.subjectTags', 'st')
->leftJoin('st.tags', 't')
->orderBy('st.position', 'ASC')
->addOrderBy('t.name', 'ASC');
}
/**
* In this example, I override the default find method. I don't recommend it thought
*/
public function find($id, $lockMode = null, $lockVersion = null)
{
return $this
->createDefaultQueryBuilder()
->where('a.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
As you can see, I reuse the createDefaultQueryBuilder method in order to get a default behavior with subjectTags and tags init in the relation and ordered in the right way.

Many to many relation can get associated object

I have a user and a school entity. This both entities have a many to many relation.
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\School")
* #ORM\JoinTable(name="user_school",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="school_id", referencedColumnName="id")}
* )
*/
private $school;
public function __construct()
{
$this->school = new ArrayCollection();
}
}
class School
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function getId()
{
return $this->id;
}
}
If i call the method for retriving the school id in my controller like:
public function indexAction(Request $request)
{
$user = $this->getUser();
$school = $user->getSchool();
echo $school->getId();
}
I get the error message
Attempted to call an undefined method named "getId" of class "Doctrine\ORM\PersistentCollection"
Can someone give me hint , what i'm doing wrong?
Even though $user->getSchool(); is singular in method name, it returns a PersistentCollection - a collection of schools (since the relationship is ManyToMany). If you want to get a specific school's id, you would have to iterate through the schools, like so:
$scools = $user->getSchool()->toArray();
foreach ($scools as $scool) {
// do something with $school
}
In the mapping you defined, User::$school is a ManyToMany, which means the result of getSchool will be a Collection of Schools, not a single School entity.
Two scenarios:
A User can have multiple Schools. Then you probably should rename $school to $schools and getSchool to getSchools. And you can't call $user->getSchools()->getId() since $user->getSchools() is a Collection and does not have a getId method. Check #NoyGabay's answer for a way to access Schools ids.
A User can only have one School. Then you did not define your mapping correctly; you wanted a ManyToOne instead of ManyToMany. Check Doctrine's documentation for association mapping.

Symfony, Sonata Admin datagrid OR filtering

I'm using Symfony 2 with Sonata Admin Bundle.
As far as i can see, Sonata Admin has only AND filtering on list action.
Use this as example:
My entity : Prodotto
/**
* Prodotto
*
* #ORM\Table()
* #ORM\Entity
* #UniqueEntity("prodotto")
*/
class Prodotto extends DefaultEntity
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
//getters - setters Zone
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #var string
*
* #ORM\Column(name="prodotto", type="string", length=255)
*/
private $prodotto;
/**
* #var \mybundle\Entity\Tipologia
* #ORM\ManyToOne(targetEntity="Tipologia")
*/
private $tipologia;
/**
* #var \mybundle\Entity\Brand
* #ORM\JoinColumn(name="brand_id", referencedColumnName="id")
* #ORM\ManyToOne(targetEntity="Brand")
*
*/
private $brand;
/**
* #var \mybundle\Entity\Layout
* #ORM\ManyToOne(targetEntity="Layout")
* #ORM\JoinColumn(name="layout_id", referencedColumnName="id")
*/
private $layout;
/**
* #var \mybundle\Entity\Carta
* #ORM\ManyToOne(targetEntity="Carta")
* #ORM\JoinColumn(name="carta_id", referencedColumnName="id")
*/
private $carta;
//[... many other fields omitted...]
//[... Getters and setters omitted (default get/set, no custom code) ...]
My admin class: ProdottoAdmin (please note that i copied only the configuration useful for this question, the class contains all the required configurations for all the actions)
/**
* #param DatagridMapper $datagridMapper
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('prodotto')
->add('tipologia')
->add('brand')
->add('layout')
->add('misura')
;
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('dataModifica','datetime',array('pattern' => 'dd/MM/yyyy','label'=>'Data'))
->add('tipologia')
->add('brand')
->add('layout')
->add('misura')
->add('Nome Prodotto', 'string', array('template' => 'mybundle:Admin:List/link_column_list_prodotto.html.twig'))
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
My Service Configuration (in services.yml):
services:
mybundle.admin.prodotto:
class: mybundle\Admin\ProdottoAdmin
arguments: [~, mybundle\Entity\Prodotto, mybundle:ProdottoAdmin]
tags:
- {name: sonata.admin, manager_type: orm, group: Prodotti, label: Prodotti}
With this configuration, i actually got a fully functional data grid filter, as you can see from the picture(image added for better understanding of the problem):
But, the default Sonata Admin filtering expose only an AND filter, i can add ONE time all the property of the entity and make an AND filtering with them.
But, now i need to extend that functionality.
I need to add an OR filter, app-wide. I want that the user can filter like "give me all the product that are related with Brand1 OR Brand2 AND are of Tipologia1."
I know i can make precise custom query, like the example above, to obtain a single result, but:
That's will not be app-wide, i have many entities and i can't write all the custom query needed
That's verbose, i will have to write much of the same code in all the data grid filters
That's not wise, because if tomorrow i change an entity, the data grid filter is coupled with the entity and i need to remember to add/modify the query.
So, finally, my question is:
There is a "correct" (or at least, a raccomended) way or pattern / maybe a configurable bundle to implement that OR filtering?

Symfony join entities in one-to-many relationship

I am working on a Symfony project recording sales as relating to stock.
My reasoning:
one sale item is associated to one stock item
one stock item can be associated to multiple sale items
As a result, I setup a one-to-many sale-to-stock relationship as show in the following code snippets:
class Sale
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var float
*
* #ORM\Column(name="cost", type="float")
*/
private $cost;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="datetime")
*/
private $date;
/**
* #ORM\ManyToOne(targetEntity="iCerge\Salesdeck\StockBundle\Entity\Stock", inversedBy="sales")
* #ORM\JoinColumn(name="sid", referencedColumnName="id")
*/
protected $stock;
...
... and ...
class Stock
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var \DateTime
*
* #ORM\Column(name="created", type="datetime")
*/
private $created;
/**
* #var \DateTime
*
* #ORM\Column(name="updated", type="datetime")
*/
private $updated;
/**
* #ORM\OneToMany(targetEntity="iCerge\Salesdeck\SalesBundle\Entity\Sale", mappedBy="stock")
*/
protected $sales;
...
NOW, if my code for implementing a one-to-many relationship is correct, I am trying to load a sale object with it's associated stock data in one query using the following code:
public function fetchSale($sid){
$query = $this->createQueryBuilder('s')
->leftjoin('s.stock', 't')
->where('s.id = :sid')
->setParameter('sid', $sid)
->getQuery();
return $query->getSingleResult();
}
The fetchSale function is from my projects SaleRepository.php class.
The leftjoin part of the query is what I hoped would successfully fetch the related stock information but I just get no output ([stock:protected]) as is shown below:
myprog\SalesProj\SalesBundle\Entity\Sale Object
(
[id:myprog\SalesProj\SalesBundle\Entity\Sale:private] => 50
[cost:myprog\SalesProj\SalesBundle\Entity\Sale:private] => 4.99
[date:myprog\SalesProj\SalesBundle\Entity\Sale:private] => DateTime Object
(
[date] => 2015-04-18 17:12:00
[timezone_type] => 3
[timezone] => UTC
)
[stock:protected] =>
[count:myprog\SalesProj\SalesBundle\Entity\Sale:private] => 5
)
How I can successfully fetch a sales' related stock data in the same query?
Doctrine is lazy-loading by default, so it's possible that $stock hasn't been initialized and the dump is not showing it. Try dumping $sale->getStock(). That tells Doctrine to go fetch it.
You can also force the loading of the Stock by selecting it:
public function fetchSale($sid){
$query = $this->createQueryBuilder('s')
->leftjoin('s.stock', 't')
->where('s.id = :sid')
->setParameter('sid', $sid)
->select('s', 't')
->getQuery();
return $query->getSingleResult();
}
By the way, fetchSale($sid) as it is now is the same as calling:
$entityManager->getRepository('SalesBundle:Sale')->find($sid);
You can use Doctrine's eager loading feature.
1. Always load associated object:
If you always want to fetch the stock object when loading the sale object, you can update your entity definition (see docs for #ManyToOne) by adding a fetch="EAGER" to the #ManyToOne definition:
/**
* #ORM\ManyToOne(targetEntity="iCerge\Salesdeck\StockBundle\Entity\Stock", inversedBy="sales", fetch="EAGER")
* #ORM\JoinColumn(name="sid", referencedColumnName="id")
*/
protected $stock;
Doctrine will then take care of loading all required objects in as few queries as possible.
2. Sometimes load associated object:
If you want to load the associated object only in some queries and not by default, according to the manual you can also tell Doctrine to use eager loading on a specific query. In your case, this may look like:
public function fetchSale($sid){
$query = $this->createQueryBuilder('s')
->where('s.id = :sid')
->setParameter('sid', $sid)
->getQuery();
$query->setFetchMode("FQCN\Sale", "stock", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
return $query->getSingleResult();
}

Symfony2 CRUD Many-to-Many - Sort Listbox with Multiple items [duplicate]

I have a Product class that has many fields on it for ManyToMany, such as ingredients, sizes, species, etc.. A total of about 14 different fields
Not all of the fields are are relevant to each product.
I have mapping set up like this
Class product {
/**
* #var Species[]
* #ORM\ManyToMany(targetEntity="Species")
* #ORM\JoinTable(name="product_species",
* joinColumns={#ORM\JoinColumn(name="productId", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="speciesId", referencedColumnName="id")}
* )
* #ORM\OrderBy({"name" = "asc"})
*/
private $species;
This works great for a manytomany/manyto one.
The problem is in my product_ingredients table I needed to add an additional field, meaning need to switch from ManyToMany to a OneToMany/ManyToOne
So like this
/**
* #var ProductIngredient[]
*
* #ORM\OneToMany(targetEntity="ProductIngredient", mappedBy="product")
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
*/
private $ingredients;
Now my ProductIngredient Entity Looks like this
/**
* #var IngredientType
* #ORM\ManyToOne(targetEntity="IngredientType", fetch="EAGER")
* #ORM\JoinColumn(name="ingredientTypeId", referencedColumnName="id")
*/
private $ingredientType;
/**
* #var Ingredient
*
* #ORM\ManyToOne(targetEntity="Ingredient", inversedBy="products", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="ingredientId", referencedColumnName="id")
* })
*/
private $ingredient;
/**
* #var Product
*
* #ORM\ManyToOne(targetEntity="Product", inversedBy="ingredients")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
* })
*/
private $product;
So in my product class for species I use the #ORM\OrderBy so that species is already ordered.. Is there a way I can somehow also do this for my ingredients field?
Or am I doing my logic wrong and these shouldn't even be fields on the product class and should just be looking up by the repository instead?
I was wanting it to be easy so I could loop through my objects like $product->getIngredients()
instead of doing
$ingredients = $this->getDoctrine()->getRepository('ProductIngredient')->findByProduct($product->getId());
in the Product entity just also aadd the orderBy to the ingredients relation
/**
* ...
* #ORM\OrderBy({"some_attribute" = "ASC", "another_attribute" = "DESC"})
*/
private $ingredients;
Well I came up with a hackish way.. Since I really only care about the sort on output, I have made a basic twig extension
use Doctrine\Common\Collections\Collection;
public function sort(Collection $objects, $name, $property = null)
{
$values = $objects->getValues();
usort($values, function ($a, $b) use ($name, $property) {
$name = 'get' . $name;
if ($property) {
$property = 'get' . $property;
return strcasecmp($a->$name()->$property(), $b->$name()->$property());
} else {
return strcasecmp($a->$name(), $b->$name());
}
});
return $values;
}
I would like to avoid this hack though and still would like to know a real solution
You should use 'query_builder' option in your Form: http://symfony.com/doc/master/reference/forms/types/entity.html#query-builder
The value of the option can be something like this:
function(EntityRepository $er) {
return $er->createQueryBuilder('i')->orderBy('i.name');
}
Don't forget to add the "use" statement for EntityRepository
If you use xml mapping, you could use
<order-by>
<order-by-field name="some_field" direction="ASC" />
</order-by>
inside your <one-to-many> tag.

Resources