Add distinct values to array collection - symfony

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))

Related

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.

Update a field after Linking / Unlinking Many-Many records in SilverStripe

I have created a Customer DataObject by extending Member. Customer has a many_many data relation with a Package DataObject.
I would like increment/decrement a Credits field in the Customer DataObject when a Package is linked / unlinked through the CMS based on the Limit field in the Package table.
Customer
class Customer extends Member {
private static $db = array(
'Gender' => 'Varchar(2)',
'DateOfBirth' => 'Date',
'Featured' => 'Boolean',
'Credits' => 'Int'
);
private static $many_many = array(
'Packages' => 'Package'
);
public function getCMSFields() {
$fields = new FieldList();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = new GridField(
'Packages',
'Package',
$this->Packages(),
$config
);
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
Package
class Package extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)',
'Limit' => 'Int'
);
private static $belongs_many_many = array(
'Customers' => 'Customer'
);
}
When you create or delete many to many relationship just one record is modified in your database - the one in table which joins elements of both sides of the relationship. Therefore neither object the relationship is based on is updated. This is why methods like: onBeforeWrite, onAfterWrite, onBeforeDelete and onAfterDelete will not be called at all and you cannot use them to detect such change.
However, Silverstripe provides class ManyManyList which is responsible for all operations connected to many to many relationships. There are two methods which are of your interest: add and remove. You can override them and put inside action to do what you need. These methods are obviously called on each link or unlink operation no matter object types are, so you should make some filtering on classes you are particularly interested in.
The proper way to override the ManyManyList class is to use Injector mechanism, so as not to modify anything inside the framework or cms folder. The example below uses relationship between Members and Groups in Silverstripe but you can easily adopt it to your need (Customer -> Member; Package -> Group).
app.yml
Injector:
ManyManyList:
class: ManyManyListExtended
ManyManyListExtended.php
/**
* When adding or removing elements on a many to many relationship
* neither side of the relationship is updated (written or deleted).
* SilverStripe does not provide any built-in actions to get information
* that such event occurs. This is why this class is created.
*
* When it is uses together with SilverStripe Injector mechanism it can provide
* additional actions to run on many-to-many relations (see: class ManyManyList).
*/
class ManyManyListExtended extends ManyManyList {
/**
* Overwritten method for adding new element to many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param mixed $item
* #param null $extraFields
* #throws Exception
*/
public function add($item, $extraFields = null) {
parent::add($item, $extraFields);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) added to Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Overwritten method for removing item from many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param DataObject $item
*/
public function remove($item) {
parent::remove($item);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) removed from Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Check if relationship is of Group-Member type.
*
* #return bool
*/
private function isGroupMembershipChange() {
return $this->getJoinTable() === 'Group_Members';
}
/**
* Get the actual ID for many-to-many relationship part - local or foreign key value.
*
* This works both ways: make action on a Member being element of a Group OR
* make action on a Group being part of a Member.
*
* #param DataObject|int $item
* #param string $keyName
* #return bool|null
*/
private function getMembershipID($item, $keyName) {
if ($this->getLocalKey() === $keyName)
return is_object($item) ? $item->ID : $item;
if ($this->getForeignKey() === $keyName)
return $this->getForeignID();
return false;
}
}
The solution provided by 3dgoo should also work fine but IMO that code does much more "hacking" and that's why it is much less maintainable. It demands more modifications (in both classes) and needs to be multiplied if you would like to do any additional link/unlink managing, like adding custom admin module or some forms.
The problem is when adding or removing items on a many to many relationship neither side of the relationship is written. Therefore onAfterWrite and onBeforeWrite is not called on either object.
I've come across this problem before. The solution I used isn't great but it was the only thing that worked for me.
What we can do is set an ID list of Packages to a session variable when getCMSFields is called. Then when an item is added or removed on the grid field we refresh the CMS panel to call getCMSFields again. We then retrieve the previous list and compare it to the current list. If the lists are different we can do something.
Customer
class Customer extends Member {
// ...
public function getCMSFields() {
// Some JavaScript to reload the panel each time a package is added or removed
Requirements::javascript('/mysite/javascript/cms-customer.js');
// This is the code block that saves the package id list and checks if any changes have been made
if ($this->ID) {
if (Session::get($this->ID . 'CustomerPackages')) {
$initialCustomerPackages = json_decode(Session::get($this->ID . 'CustomerPackages'), true);
$currentCustomerPackages = $this->Packages()->getIDList();
// Check if the package list has changed
if($initialCustomerPackages != $currentCustomerPackages) {
// In here is where you put your code to do what you need
}
}
Session::set($this->ID . 'CustomerPackages', json_encode($this->Packages()->getIDList()));
}
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = GridField::create(
'Packages',
'Package',
$this->Packages(),
$config
);
// This class needs to be added so our javascript gets called
$packageField->addExtraClass('refresh-on-reload');
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
The if ($this->ID) { ... } code block is where all our session code happens. Also note we add a class to our grid field so our JavaScript refresh works $packageField->addExtraClass('refresh-on-reload');
As mentioned before, we need to add some JavaScript to reload the panel each time a package is added or removed from the list.
cms-customer.js
(function($) {
$.entwine('ss', function($){
$('.ss-gridfield.refresh-on-reload').entwine({
reload: function(e) {
this._super(e);
$('.cms-content').addClass('loading');
$('.cms-container').loadPanel(location.href, null, null, true);
}
});
});
})(jQuery);
Inside the if($initialCustomerPackages != $currentCustomerPackages) { ... } code block there are a number of things you can do.
You could use $this->Packages() to fetch all the current packages associated to this customer.
You could call array_diff and array_merge to get just the packages that have been added and removed:
$changedPackageIDs = array_merge(array_diff($initialCustomerPackages, $currentCustomerPackages), array_diff($currentCustomerPackages, $initialCustomerPackages));
$changedPackages = Package::get()->byIDs($changedPackageIDs);
The above code will add this functionality to the Customer side of the relationship. If you also want to manage the many to many relationship on the Package side of the relationship you will need to add similar code to the Package getCMSFields function.
Hopefully someone can come up with a nicer solution. If not, I hope this works for you.
note: Didn't actually check does the model work but by visually checking this should help you:
On the link you provided you are using
$customer = Customer::get()->Filter...
That returns a DataList of objects, not a singular object unless you specify what is the object you want from the DataList.
If you are filtering the Customers then you want to get a SPECIFIC customer from the DataList, e.g. the first one in this case.
$customer = Customer::get()->filter(array('ID' => $this->CustomerID))->first();
But You should be able to get the singular DataObject with:
$customer = $this->Customer();
As you are defining the Customer as "has_one". If the relation was a Has many, using () would get you a DataList of objects.
Protip:
You don't need to write our own debug files in SilverStripe. It has own functions for it. For example Debug::log("yay"); what writes the output to a file and Debug::dump("yay") that dumps it directly out.
Tip is that you can check what is the object that you accessing right. Debug::dump(get_class($customer)); would output only the class of the object.

How to check if item is related to certain other item in many to many relationship?

I have document entity with one to many relation to documentstags entity.
How may I check whether certain document is related to documentstags with certain "tag" column value?
Documents entity:
/**
* #ORM\OneToMany(targetEntity="Documentstags", mappedBy="documentid")
*/
protected $tags;
Documentstags entity:
/**
* #ORM\Column(type="string", length=220)
*/
protected $tag;
How may I check whether document A is related to Documentstags item which tag value e.g. B?
Currently I have implemented the following code as function of Documents entity but it does not seems to be effecient:
$tags = $this->getTags();
$is_zatwierdzony = false;
foreach($tags as $tag)
{
if($tag.tag == $this->avaliabletags['zatwierdzony']['name']) $is_zatwierdzony = true
}
U can use Criteria class:
for example:
...
use Doctrine\Common\Collections\Criteria;
...
public function getMatchingTags()
{
$criteria = Criteria::create()
->where(Criteria::expr()->eq("tag", $this->avaliabletags['zatwierdzony']['name']));
$tags = $this->getTags()->matching($criteria);
#do something with matching tags
}
Doctrine2 documentation: Filtering Collections

Symfony2: How to create custom query methods on related entities

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

Remove all related children in Symfony entity

Suppose we have a field with ManyToMany relation as
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Users")
* #ORM\JoinTable(name="users_roles",
* joinColumns={#ORM\JoinColumn(name="User_Id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="Role_Id", referencedColumnName="id")})
*/
protected $userRole;
To remove one related element from table we can have this function in our entity:
/**
* Remove userRole
* #param \Acme\MyBundle\Entity\Users $user
*/
public function remvoveUserRole(\Acme\MyBundle\Entity\Users $user)
{
$this->userRole->removeElement($user);
}
The Question:
The ArrayCollection type has the function removeElement which is used to remove one element of the relationship. There is another function clear which in api says Clears the collection, removing all elements, therefore can I have a function like below in my entity to clear all the related elements so that by flushing it removes all?
/**
* Remove all user Roles
*/
public function remvoveAllUserRole()
{
$this->userRole->clear();
}
will it work for just ManyToMany related tables or it might work for ManyToOne, too?
Ţîgan Ion is right - removeElement/clear only removes those elements from memory.
However, I think you could achieve something as close depending on how did you configure cascade and orphanRemoval in your relationship.
$em = ...; // your EntityManager
$roles = $user->getRoles();
$roles->clear();
$user = $em->merge($user); // this is crucial
$em->flush();
In order for this to work, you need to configure User relationship to
cascade={"merge"} - this will make $em->merge() call propagate to roles.
orphanRemoval = true - since this is #ManyToMany, this will make EntityManager remove free-dangling roles.
Can't test this now, but as far as I can see it could work. I will try this out tomorrow and update the answer in need be.
Hope this helps...
Note: This logic works for ManyToMany relationship, but not for ManyToOne
I tested the way to delete all related roles for specific use (ManyToMany) and it worked. What you need is to define a function in your UserEntity as
/**
* Remove all user Roles
*/
public function remvoveAllUserRole()
{
$this->userRole->clear();
}
Then in your controller or anywhere else(if you need) you can call the function as below
$specificUser = $em->getRepository('MyBundle:Users')->findOneBy(array('username' => 'test user'));
if (!empty($specificUser)) {
$specificUser->removeAllUserRole();
$em->flush();
}
Then it will delete all related roles for the test user and we don't need to use the for loop and remove them one by one
if I'm not mistaken, this will not work, you have to delete the "role
s" from the arrayCollection directly from the database
$roles = $user->getRoles()
foreach $role from $roles
$em->remove($role);
$em->flush();
now you should get an empty collection
p.s: the best way is to test your ideas

Resources