Symfony2: How to create custom query methods on related entities - symfony

I have a User entity which has an ArrayCollection of Positions. Each Position has for sure a user_id property.
Now i want to get all positions from a user (to get all i would do $user->getPositions()) that are matching a specific query, for example have a date property that matches the current date. Therefor i want to do something like $user->getCurrentPositions() and it should return a subset of the positions related to that user.
How is that possible?
EDIT:
What i really wanna do is something like this in my controller:
$em = $this->getDoctrine()->getManager();
$users = $em->getRepository('fabianbartschWhereMyNomadsAtBundle:User')->findAll();
foreach ($users as $user) {
$positions = $user->getCurrentPositions();
foreach ($positions as $position) {
echo $position->getLatitude().'<br>';
}
}
I wanna iterate over all users and from each user i want to have the relevant positions. But that isnt possible from the repository i guess, as i get the following message: Attempted to call method "getCurrentPositions" on class ...

If you are using Doctrine you can use the built-in Criteria API which is meant for this purpose exactly.
Collections have a filtering API that allows you to slice parts of data from a collection. If the collection has not been loaded from the database yet, the filtering API can work on the SQL level to make optimized access to large collections.

Ok i found out, its for sure possible with Repositories:
Entity\User.php
/**
* #ORM\Entity(repositoryClass="fabianbartsch\WhereMyNomadsAtBundle\Entity\UserRepository")
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
Entity\UserRepository.php
/**
* UserRepository
*/
class UserRepository extends EntityRepository
{
public function getCurrentPositions()
{
$query = $this->getEntityManager()
->createQuery(
"SELECT p
FROM xxx:Position p
WHERE p.start <= '2014-08-17' AND p.end >= '2014-08-17'"
);
try {
return $query->getResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
}
In the user object only related position entries are affected by the query, so is no need to join user entity with the position entity. Pretty simple, should just try out instead posting on stackoverflow, sry guys :P

Related

Single position to restrict access to a Doctrine entity

I've just started working with Doctrine and built a simple blog project. One of my requirements is that a blog post should not be visible to anybody (for simpleness, skip an editor's interface) until the publish date is reached.
As far as I see, it's obvious to do so using a custom repository. Let's extend the find method the following way:
public function find($id, $lockMode = null, $lockVersion = null)
{
/** #var Post $post */
$post = parent::find($id, $lockMode, $lockVersion);
if($post->getCreatedAt() > new \DateTime()) {
return null;
}
return $post;
}
This restricts the access for a page showing a single Post entity. For an overview page, the same can be done using a custom method:
public function findForOverview()
{
$query = $this->createQueryBuilder('p')
->where('p.createdAt < CURRENT_TIMESTAMP()')
->orderBy('p.createdAt', 'DESC')
->getQuery();
return $query->getResult();
}
So, even for this simple requirement, I've already written two custom methods. If I continue to work on my project, other restriction limitations might occur and additional ways to load that entity might arise. And as far as I see, for each case I have to implement the logic for all access guards.
Is there no simpler way to do that? I'm thinking of something like an annotation or an "entity load listener" that makes it simple to write one single entry point for all such checks - making it impossible to forget such checks...
Such restrictions are usually implemented by using mechanism of SQL filters in Doctrine. Implementation of this filter works on lower level then DQL and allows you to apply modifications for SQL query being constructed. In your case it may look like this:
namespace App\ORM\Filter;
use App\Entity\Post;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class PostVisibilityFilter extends SQLFilter
{
/**
* Gets the SQL query part to add to a query.
*
* #param ClassMetadata $targetEntity
* #param string $targetTableAlias
* #return string The constraint SQL if there is available, empty string otherwise
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if ($targetEntity->name !== Post::class) {
return '';
}
return sprintf('%s.%s >= now()', $targetTableAlias, $targetEntity->getColumnName('createdAt'));
}
}

Add distinct values to array collection

Within my application I am sending out notifications to different users, which are assigned to agencies, which are assigned to documents. So whenever a document gets created and an agency is assigned to that document, all the users belonging to that agency should get a notification. The problem: it may happen, that an user is assigned to multiple agencies so whenever the notifications get sent out and all his agencies are assigned to the document, he would get notified multiple times. I'd like to avoid this but I can't figure out how to add only distinct objects to my array collection since it doesn't seem like there's something like the array_unique function.
So far it looks like that:
foreach($document->getAgencies() as $agency) {
if(count($agency->getUseragencies()) > 0){
$users = $agency->getUseragencies();
foreach ($users as $user){
... notifications are generated
$manager->addNotification($user, $notif);
}
}
}
Any help would be appreciated!
oh and background info: Agency is an own entity, as well as User is and they are in a many to many relationship!
edit
mapping infos:
in entity Agency:
/**
* #ORM\ManyToMany(targetEntity="UserBundle\Entity\User", mappedBy="agencies")
**/
private $useragencies;
in entity User
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Agency", inversedBy="useragencies", cascade={"persist"})
* #ORM\JoinTable(name="user_user_agencies",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="iata8", referencedColumnName="iata8")})
* #var \AppBundle\Entity\Agency
**/
private $agencies;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Notification", mappedBy="user", orphanRemoval=true, cascade={"persist"})
**/
private $notifications;
ArrayCollection has a method contains that you could use - link.
I assume your Agency entity has also bidirectional mapping with Document entity i guess as ManyToMany. To get the users whom you want to send the notification which belongs to multiple agencies and an agency can have many documents you can use below DQL to get distinct users by providing the document id if you have any other property to identify the document you can adjust WHERE clause accordingly
SELECT DISTINCT u
FROM YourBundle:User u
JOIN u.agencies a
JOIN a.documents d
WHERE d.id = :documentid
Using query builder it would be something like
$user = $em->getRepository('YourBundle:User');
$user = $user->createQueryBuilder('u')
->select('u')
->join('u.agencies','a')
->join('a.documents','d')
->where('d.id = :documentid')
->setParameter('documentid', $documentid)
->distinct()
->getQuery();
$users= $user->getResult();
While you cannot use in_array() on an ArrayCollection, you'll have to build your own array of unique users.
$users = array();
foreach($document->getAgencies() as $agency) {
if(count($agency->getUseragencies()) > 0) {
foreach ($agency->getUseragencies()as $user) {
// ... notifications are generated
if(!in_array($user->getId(), $users)) {
$users[] = $user->getId();
}
}
foreach($users as $userId) {
$manager->addNotification($userId, $notif);
}
}
}
or a simpler, lower-cost version:
$sent = array();
foreach($document->getAgencies() as $agency) {
if(count($agency->getUseragencies()) > 0) {
foreach ($agency->getUseragencies()as $user) {
// ... notifications are generated
if(!in_array($user->getId(), $sent)) { // check if user has already been sent the notification
$manager->addNotification($user, $notif); // send the notification
$sent[] = $user->getId(); // add user to the 'sent' list
}
}
}
}
Alternatively, you could save yourself a lot of trouble by writing a custom DQL Query (possibly in your UserRepository class) to fetch the list of user from database directly. This would remove a lot of complexity in the code by removing the need for a loop altogether.
You need to add a condition to handle values already in the array. Try adding something like the condition below.
foreach($user as $user){
if(!in_array($value, $list, true))

Doctrine get all entities from collection

I don't know if what I am trying is really possible. So I thought to ask it to you guys.
What I am trying to do:
get a set of companies
get all the users associated with the given companies
In code:
$companyIds = array(1,2,3);
$companies = $this->em->getRepository('AppBundle:Company')->findById($companyIds);
dump($companies->getUsers()); // this will not work, but I like it to work
Where they are associated as follows:
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Company", inversedBy="users")
* #ORM\JoinColumn(name="company_id", referencedColumnName="id", nullable=false)
*/
private $company;
}
class Company
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\User", mappedBy="company")
*/
private $users;
}
Repository returns you an ArrayCollection of entities, not a single entity, therefore you need to access each of them separately.
This should work:
foreach($companies as $company) {
$company->getUsers();
}
The problem with the above is that by default it will fetch (lazy load) users from database for each company in separate query (on demand when calling getUsers), which would be very inefficient on larger scale.
There are couple possible solution depending on your needs.
You could configure doctrine to always fetch users with companies, which is called eager fetch.
Having fetched users, you can merge ArrayCollections (and remove duplicates if needed), to achieve single collection containing all users.
Other way could be to fetch companies with users by creating sufficient DQL query in a custom method of you company's repository. If you need only users and don't need companies, then it could be a query that only fetches users without companies.
Try something like this in your User-Repository:
public function getAllUsersFromCompanies($ids)
{
$qb = $this->createQueryBuilder('u');
$qb->leftJoin('u.company', 'c')
->where('c.id IN :ids')
->setParameter('ids', $ids)
->getQuery();
return $query->getResult();
}
We are joining the user table with the company table here, which gives us the company for each user. Then, we filter out every user, that has the wrong company.
You can e.g. fetch all Comapany entities with users with one query:
$companies = $em->createQueryBuilder()
->select('c, u')
->from('AppBundle:Company', 'c')
// or left join based on you needs
->innerJoin('c.users', 'u')
->getQuery()
->getResult();
This will not result in queries when fetching company users.

Symfony2 remove and save many to many relations

I need your help today. I'm working on a small application using Symfony 2.1 but I have a base problem, I have to tables with a many to many relation which creates a third table:
class Usuario implements UserInterface {
/**
* #ORM\ManyToMany(targetEntity="Alood\BackBundle\Entity\Alergeno", inversedBy="usuarios")
* #ORM\JoinTable(name="UsuariosProductos",
* joinColumns={#ORM\JoinColumn(name="usuario_user", referencedColumnName="user")},
* inverseJoinColumns={#ORM\JoinColumn(name="alergeno_id", referencedColumnName="id")}
* )
**/
protected $alergenos;
}
public function __construct(){
$this->alergenos = new ArrayCollection();
}
public function getAlergenos() { return $this->alergenos; }
and:
/**
* #ORM\ManyToMany(targetEntity="Alood\BackBundle\Entity\Usuario", mappedBy="alergenos")
**/
protected $usuarios;
Then I need to remove the non selected Alergenos, this is my controller:
$alergenosUser = $em->getRepository("BackBundle:Usuario")->find($usuario);
$resultSym = array_diff($alergenosUsuarioIds, $alergen);
foreach($resultSym as $result) {
$alergenosUser->getAlergenos()->remove($result);
}
$em->persist($alergenosUser);
$em->flush();
Could you please help me to figure out what I'm doing wrong? Thanks you so much!
In order to remove an item from a collection use the following:
$collection->removeElement($item);
The remove($key) function will remove by key while removeElement($item) removes the item from the collection if found. Have a look at the ArrayCollection code here.
Be aware that doctrine will only check the owning side of a relation for changes.
It is not clear what the $alergenosUsuarioIds and $alergen variables represent but you might be mistaken about the usage of the remove() method of ArrayCollection.
You need to give it an index, not the id of the entity you want to remove. You can also use the removeElement() method and pass it the entity.
For instance you can do something like this :
$elements = $alergenosUser->getAlergenos();
foreach ($elements as $element) {
if ($element->getId() == $id_from_array_diff_or_whatever) {
$elements->removeElement($element);
}
}
or
$elements = $alergenosUser->getAlergenos();
foreach ($elements as $key => $element) {
if ($element->getId() == $id_from_array_diff_or_whatever) {
$elements->remove($key);
// or
unset($elements[$key]);
}
}
You can also use the matching() but I'm not sure it's available with the version shipped with symfony2 2.1.
So your problem can be solved doing the relation yourself.
ManyToMany doesn't really exist because as you say a third table is created. You want to delete elements only in this third table.
So you have to build the relation yourself to delete directly an element in the third table.
So first create the third entity.
Do two relation ManyToOne from the third entity to the two others entities.
Then you just have to simply remove an element of the third entity you just created.

Doctrine2 subquery in template - equivalent of symfony1 lazy query getter

I have two tables:
comment - id, application_id, comment, user_id, created_at, deleted_at
comment_likes - comment_id, user_id
I can retrieve the comments for an application using the standard DQLSELECT u FROM Comment WHERE :application = application
When lopping through the comments, I want to see if the logged in user has already liked a comment.
In symfony1, I would have used a simple lazy query $comment->hasUserLiked()
At the moment, in symfony2, I have to do a query of all the user likes for an application comments and a query of all the application comments.
When looping through application comments I do a sub-loop in each comment to check whether that a user likes record exists in the user likes comments collection. This is not clean.
Hope this makes sense.
Is there a better way?
EDIT: I could use a sub-controller to render whether a user likes the comment or not....but that seems rather over the top just for a couple of lines of html. Although, cleaner than the current implementation.
You need to set up a bidirectional one-to-many relationship between the Comment and Comment\Like entities. This way, a Comment entity would know about all the likes it has. Then you could implement a method in it like $comment->hasBeenLikedBy($user) which would loop through all the likes it has and see if any of them was done by the user you passed.
The Comment entity:
<?php
namespace Model;
class Comment
{
/**
* #OneToMany(targetEntity="Model\Comment\Like", mappedBy="comment")
*/
private $likes;
public function hasBeenLikedBy(User $user)
{
foreach ($this->likes as $like) {
if ($like->getUser() == $user) {
return true;
}
return false;
}
}
}
The Comment\Like entity:
<?php
namespace Model\Comment;
class Like
{
/**
* #ManyToOne(targetEntity="Model\Comment")
*/
private $comment;
/**
* #ManyToOne(targetEntity="Model\User")
*/
private $user
public function getUser()
{
return $this->user;
}
}
This code is not complete and may contain mistakes, but I hope it's enough to show you the overall approach.
You can write your own hasUserLiked() function to query the database when called
You could join the comments and likes table and get the likes in the same call.
You can use the following query if your doctrine schema is setup correctly:
SELECT c FROM Comment c
LEFT JOIN c.CommentLikes cl
WHERE c.application = :application

Resources