Suppose we have a set of entities:
$entities = $em->getRepository('MyBundle:Entity')->findBy(array('cat' => 1));
What is the best way to pick up a single entity out of this set by ID? Of course I can search for it with a loop or with array_filter, e.g.:
$entity = null;
foreach ($entities as $_entity) {
if ($_entity->getId() == $id) {
$entity = $_entity;
break;
}
}
But maybe there is a build in Symfony/Doctrine method for that?
Hi you can use the filter method from the ArrayCollection class, but it's not so different from looping entities like you're doing
$idToSearch = $n;
$newCollection = $entities->filter(
function($entity) use ($idToSearch) {
return $entity->getId() == $idToSearch;
}
);
You need to iterate through them with foreach, although that scales very poorly. Depending on the amount of results you expect, it could be faster to simply run two queries.
Related
I want to loop over a collection of items and attach a relationship based on if a particular condition is satisfied. Here is my code
public function bulkAssign()
{
$trainers = MasterTrainer::all();
for ($i=0; $i < count($trainers); $i++) {
$this->assignToManager($trainers[$i]);
}
// return redirect()->back()->with('success', 'Project Managers Assigned Successfully');
}
private function assignToManager($trainer)
{
$manager = ProjectManager::where('state', $trainer->state)->first();
return $trainer->update([
'project_manager_id' => $manager->id
]);
}
What I get is it attaches only the first manager to all the elements in the collection. What am i doing wrong?
can you inline the func for now? do some sort of echo/debugging?
but also I see several issues:
yes do use foreach because that is a bit better and you avoid having to use $i (making code a little more easy to read)
you are not attaching a relationship, you are setting a project_manager_id (i say this because initially i automatically thought you were going to dynamically add a relationship to model)
without knowing your db schema.. could you not do some sort of trick to avoid having to do this nth times?
$manager = ProjectManager::where('state', $trainer->state)->first();
you could either do:
$states = $trainers->pluck('states');
$managers = // do a query to get one trainer per state using group by
foreach ($trainers... ) {
$manager = $managers->where('state', $trainers->state)->first() // this is collection not eloquent
$trainer->update([
'project_manager_id' => $manager->id
]);
other would be to create a scope where you do a sub query to get manager id when u query for trainers
I have multiple collections merging into one, then sorting that one by datetime to ultimately create a timeline between the collections.
Heres the catch, the datetime columns to sort are different names.
Is there anything I can do to make this cleaner - possibly attach the foreach loop with the ->merge? Looks ugly with the foreach loop. note: code below works but I feel it's a lazy way out and might be slow with more items in the collection.
// Create timeline, sortby creation datetimes.
$TimelineItems = collect();
$TimelineItems = $Appointments->merge($lead->SalesResult);
foreach ($TimelineItems as $key => $TimelineItem) {
if(!empty($TimelineItem->appointment_created)) {
$TimelineItems[$key]->created_at = $TimelineItem->appointment_created;
}
if(!empty($TimelineItem->salesresult_created_timestamp)) {
$TimelineItems[$key]->created_at = $TimelineItem->salesresult_created_timestamp;
}
}
$TimelineItems = $TimelineItems->sortByDesc('created_at');
dd($TimelineItems);
The best solution would probably be to standardize your model objects to use standard date stamp fields - then you wouldn't need to transform them.
Failing that, you could use each() or transform():
// Create timeline, sortby creation datetimes.
$TimelineItems = collect();
$AppointmentTemps = collect($Appointments);
$SalesResultTemps = $lead->SalesResult;
$TimelineItems = $AppointmentTemps
->merge($SalesResultTemps)
->transform( function ($item) {
if(!empty($item->appointment_created)) {
$item->created_at = $item->appointment_created;
}
if(!empty($item->salesresult_created_timestamp)) {
$item->created_at = $item->salesresult_created_timestamp;
}
return $item;
})
->sortByDesc('created_at');
dd($TimelineItems);
The transform method iterates over the collection and calls the given callback with each item in the collection. The items in the collection will be replaced by the values returned by the callback:
See the docs for the transform() collection method for reference.
Assume I have Product entities and Review entities attached to products. Is it possible to attach a fields to a Product entity based on some result returned by an SQL query? Like attaching a ReviewsCount field equal to COUNT(Reviews.ID) as ReviewsCount.
I know it is possible to do that in a function like
public function getReviewsCount() {
return count($this->Reviews);
}
But I want doing this with SQL to minimize number of database queries and increase performance, as normally I may not need to load hundreds of reviews, but still need to know there number. I think running SQL's COUNT would be much faster than going through 100 Products and calculating 100 Reviews for each. Moreover, that is just example, on practice I need more complex functions, that I think MySQL would process faster. Correct me if I'm wrong.
You can map a single column result to an entity field - look at native queries and ResultSetMapping to achieve this. As a simple example:
use Doctrine\ORM\Query\ResultSetMapping;
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$rsm = new ResultSetMapping;
$rsm->addEntityResult('AppBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
$results = $query->getResult();
Then in your Product entity you would have a $reviewsCount field and the count would be mapped to that. Note that this will only work if you have a column defined in the Doctrine metadata, like so:
/**
* #ORM\Column(type="integer")
*/
private $reviewsCount;
public function getReviewsCount()
{
return $this->reviewsCount;
}
This is what is suggested by the Aggregate Fields Doctrine documentation. The problem is here is that you are essentially making Doctrine think you have another column in your database called reviews_count, which is what you don't want. So, this will still work without physically adding that column, but if you ever run a doctrine:schema:update it's going to add that column in for you. Unfortunately Doctrine does not really allow virtual properties, so another solution would be to write your own custom hydrator, or perhaps subscribe to the loadClassMetadata event and manually add the mapping yourself after your particular entity (or entities) load.
Note that if you do something like COUNT(r.id) AS reviewsCount then you can no longer use COUNT(id) in your addFieldResult() function, and must instead use the alias reviewsCount for that second parameter.
You can also use the ResultSetMappingBuilder as a start into using the result set mapping.
My actual suggestion is to do this manually instead of going through all of that extra stuff. Essentially create a normal query that returns both your entity and scalar results into an array, then set the scalar result to a corresponding, unmapped field on your entity, and return the entity.
After detailed investigation I've found there are several ways to do something close to what I wanted including listed in other answers, but all of them have some minuses. Finally I've decided to use CustomHydrators. It seems that properties not managed with ORM cannot be mapped with ResultSetMapping as fields, but can be got as scalars and attached to an entity manually (as PHP allows to attach object properties on the fly). However, result that you get from doctrine remains in the cache. That means properties set in that way may be reset if you make some other query that would contain these entities too.
Another way to do that was adding these field directly to doctrine's metadata cache. I tried doing that in a CustomHydrator:
protected function getClassMetadata($className)
{
if ( ! isset($this->_metadataCache[$className])) {
$this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
if ($className === "SomeBundle\Entity\Product") {
$this->insertField($className, "ReviewsCount");
}
}
return $this->_metadataCache[$className];
}
protected function insertField($className, $fieldName) {
$this->_metadataCache[$className]->fieldMappings[$fieldName] = ["fieldName" => $fieldName, "type" => "text", "scale" => 0, "length" => null, "unique" => false, "nullable" => true, "precision" => 0];
$this->_metadataCache[$className]->reflFields[$fieldName] = new \ReflectionProperty($className, $fieldName);
return $this->_metadataCache[$className];
}
However, that method also had problems with entities' properties reset. So, my final solution was just to use stdClass to get the same structure, but not managed by doctrine:
namespace SomeBundle;
use PDO;
use Doctrine\ORM\Query\ResultSetMapping;
class CustomHydrator extends \Doctrine\ORM\Internal\Hydration\ObjectHydrator {
public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) {
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach($resultSetMapping->entityMappings as $root => $something) {
$rootIDField = $this->getIDFieldName($root, $resultSetMapping);
foreach($data as $row) {
$key = $this->findEntityByID($result, $row[$rootIDField]);
if ($key === null) {
$result[] = new \stdClass();
end($result);
$key = key($result);
}
foreach ($row as $column => $field)
if (isset($resultSetMapping->columnOwnerMap[$column]))
$this->attach($result[$key], $field, $this->getPath($root, $resultSetMapping, $column));
}
}
return $result;
}
private function getIDFieldName($entityAlias, ResultSetMapping $rsm) {
foreach ($rsm->fieldMappings as $key => $field)
if ($field === 'ID' && $rsm->columnOwnerMap[$key] === $entityAlias) return $key;
return null;
}
private function findEntityByID($array, $ID) {
foreach($array as $index => $entity)
if (isset($entity->ID) && $entity->ID === $ID) return $index;
return null;
}
private function getPath($root, ResultSetMapping $rsm, $column) {
$path = [$rsm->fieldMappings[$column]];
if ($rsm->columnOwnerMap[$column] !== $root)
array_splice($path, 0, 0, $this->getParent($root, $rsm, $rsm->columnOwnerMap[$column]));
return $path;
}
private function getParent($root, ResultSetMapping $rsm, $entityAlias) {
$path = [];
if (isset($rsm->parentAliasMap[$entityAlias])) {
$path[] = $rsm->relationMap[$entityAlias];
array_splice($path, 0, 0, $this->getParent($root, $rsm, array_search($rsm->parentAliasMap[$entityAlias], $rsm->relationMap)));
}
return $path;
}
private function attach($object, $field, $place) {
if (count($place) > 1) {
$prop = $place[0];
array_splice($place, 0, 1);
if (!isset($object->{$prop})) $object->{$prop} = new \stdClass();
$this->attach($object->{$prop}, $field, $place);
} else {
$prop = $place[0];
$object->{$prop} = $field;
}
}
}
With that class you can get any structure and attach any entities however you like:
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$em = $this->getDoctrine()->getManager();
$rsm = new ResultSetMapping();
$rsm->addEntityResult('SomeBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $em->createNativeQuery($sql, $rsm);
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'SomeBundle\CustomHydrator');
$results = $query->getResult('CustomHydrator');
Hope that may help someone :)
Yes, it is possible, you need to use QueryBuilder to achieve that:
$result = $em->getRepository('AppBundle:Product')
->createQueryBuilder('p')
->select('p, count(r.id) as countResult')
->leftJoin('p.Review', 'r')
->groupBy('r.id')
->getQuery()
->getArrayResult();
and now you can do something like:
foreach ($result as $row) {
echo $row['countResult'];
echo $row['anyOtherProductField'];
}
If you're on Doctrine 2.1+, consider using EXTRA_LAZY associations:
They allow you to implement a method like yours in your entity, doing a straight count on the association instead of retrieving all the entities in it:
/**
* #ORM\OneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY")
*/
private $Reviews;
public function getReviewsCount() {
return $this->Reviews->count();
}
The previous answers didn't help me, but I found a solution doing the following:
My use case was different so the code is a mock. But the key is to use addScalarResult and then cleanup the result while setting the aggregate on the entity.
use Doctrine\ORM\Query\ResultSetMappingBuilder;
// ...
$sql = "
SELECT p.*, COUNT(r.id) AS reviewCount
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
";
$em = $this->getEntityManager();
$rsm = new ResultSetMappingBuilder($em, ResultSetMappingBuilder::COLUMN_RENAMING_CUSTOM);
$rsm->addRootEntityFromClassMetadata('App\Entity\Product', 'p');
$rsm->addScalarResult('reviewCount', 'reviewCount');
$query = $em->createNativeQuery($sql, $rsm);
$result = $query->getResult();
// APPEND the aggregated field to the Entities
$aggregatedResult = [];
foreach ($result as $resultItem) {
$product = $resultItem[0];
$product->setReviewCount( $resultItem["reviewCount"] );
array_push($aggregatedResult, $product);
}
return $aggregatedResult;
I would like to understand the best way to order a Doctrine Collection based on associated Entity. In this case, it is not possible to use the #orderBy annotation.
I have found 5 solutions on the Internet.
1) Adding a method to the AbstractEntity (according to Ian Belter https://stackoverflow.com/a/22183527/1148260)
/**
* This method will change the order of elements within a Collection based on the given method.
* It preserves array keys to avoid any direct access issues but will order the elements
* within the array so that iteration will be done in the requested order.
*
* #param string $property
* #param array $calledMethods
*
* #return $this
* #throws \InvalidArgumentException
*/
public function orderCollection($property, $calledMethods = array())
{
/** #var Collection $collection */
$collection = $this->$property;
// If we have a PersistentCollection, make sure it is initialized, then unwrap it so we
// can edit the underlying ArrayCollection without firing the changed method on the
// PersistentCollection. We're only going in and changing the order of the underlying ArrayCollection.
if ($collection instanceOf PersistentCollection) {
/** #var PersistentCollection $collection */
if (false === $collection->isInitialized()) {
$collection->initialize();
}
$collection = $collection->unwrap();
}
if (!$collection instanceOf ArrayCollection) {
throw new InvalidArgumentException('First argument of orderCollection must reference a PersistentCollection|ArrayCollection within $this.');
}
$uaSortFunction = function($first, $second) use ($calledMethods) {
// Loop through $calledMethods until we find a orderable difference
foreach ($calledMethods as $callMethod => $order) {
// If no order was set, swap k => v values and set ASC as default.
if (false == in_array($order, array('ASC', 'DESC')) ) {
$callMethod = $order;
$order = 'ASC';
}
if (true == is_string($first->$callMethod())) {
// String Compare
$result = strcasecmp($first->$callMethod(), $second->$callMethod());
} else {
// Numeric Compare
$difference = ($first->$callMethod() - $second->$callMethod());
// This will convert non-zero $results to 1 or -1 or zero values to 0
// i.e. -22/22 = -1; 0.4/0.4 = 1;
$result = (0 != $difference) ? $difference / abs($difference): 0;
}
// 'Reverse' result if DESC given
if ('DESC' == $order) {
$result *= -1;
}
// If we have a result, return it, else continue looping
if (0 !== (int) $result) {
return (int) $result;
}
}
// No result, return 0
return 0;
};
// Get the values for the ArrayCollection and sort it using the function
$values = $collection->getValues();
uasort($values, $uaSortFunction);
// Clear the current collection values and reintroduce in new order.
$collection->clear();
foreach ($values as $key => $item) {
$collection->set($key, $item);
}
return $this;
}
2) Creating a Twig extension, if you need the sorting just in a template (according to Kris https://stackoverflow.com/a/12505347/1148260)
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;
}
3) Transforming the collection into an array and then sorting it (according to Benjamin Eberlei https://groups.google.com/d/msg/doctrine-user/zCKG98dPiDY/oOSZBMabebwJ)
public function getSortedByFoo()
{
$arr = $this->arrayCollection->toArray();
usort($arr, function($a, $b) {
if ($a->getFoo() > $b->getFoo()) {
return -1;
}
//...
});
return $arr;
}
4) Using ArrayIterator to sort the collection (according to nifr https://stackoverflow.com/a/16707694/1148260)
$iterator = $collection->getIterator();
$iterator->uasort(function ($a, $b) {
return ($a->getPropery() < $b->getProperty()) ? -1 : 1;
});
$collection = new ArrayCollection(iterator_to_array($iterator));
5) Creating a service to gather the ordered collection and then replace the unordered one (I have not an example but I think it is pretty clear). I think this is the ugliest solution.
Which is the best solution according to you experience? Do you have other suggestions to order a collection in a more effective/elegant way?
Thank you very much.
Premise
You proposed 5 valid/decent solutions, but I think that all could be reduced down to two cases, with some minor variants.
We know that sorting is always O(NlogN), so all solution have theoretically the same performance. But since this is Doctrine, the number of SQL queries and the Hydration methods (i.e. converting data from array to object instance) are the bottlenecks.
So you need to choose the "best method", depending on when you need the entities to be loaded and what you'll do with them.
These are my "best solutions", and in a general case I prefer my solution A)
A) DQL in a loader/repository service
Similar to
None of your case (somehow with 5, see the final notes note). Alberto Fernández pointed you in the right direction in a comment.
Best when
DQL is (potentially) the fastest method, since delegate sorting to DBMS which is highly optimized for this. DQL also gives total controls on which entities to fetch in a single query and the hydrations mode.
Drawbacks
It is not possible (AFAIK) to modify query generated by Doctrine Proxy classes by configuration, so your application need to use a Repository and call the proper method every time you load your entities (or override the default one).
Example
class MainEntityRepository extends EntityRepository
{
public function findSorted(array $conditions)
{
$qb = $this->createQueryBuilder('e')
->innerJoin('e.association', 'a')
->orderBy('a.value')
;
// if you always/frequently read 'a' entities uncomment this to load EAGER-ly
// $qb->select('e', 'a');
// If you just need data for display (e.g. in Twig only)
// return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
return $qb->getQuery()->getResult();
}
}
B) Eager loading, and sorting in PHP
Similar to case
Case 2), 3) and 4) are just the same thing done in different place. My version is a general case which apply whenever the entities are fetched. If you have to choose one of these, then I think that solution 3) is the most convenient, since don't mess with the entity and is always available, but use EAGER loading (read on).
Best when
If the the associated entities are always read, but it is not possible (or convenient) to add a service, then all entities should loaded EAGER-ly. Sorting then can be done by PHP, whenever it makes sense for the application: in an event listener, in a controller, in a twig template... If the entities should be always loaded, then an event listener is the best option.
Drawbacks
Less flexible than DQL, and sorting in PHP may be a slow operation when the collection is big. Also, the entities need to be hydrated as Object which is slow, and is overkill if the collection is not used for other purpose. Beware of lazy-loading, since this will trigger one query for every entity.
Example
MainEntity.orm.xml:
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping>
<entity name="MainEntity">
<id name="id" type="integer" />
<one-to-many field="collection" target-entity="LinkedEntity" fetch="EAGER" />
<entity-listeners>
<entity-listener class="MainEntityListener"/>
</entity-listeners>
</entity>
</doctrine-mapping>
MainEntity.php:
class MainEntityListener
{
private $id;
private $collection;
public function __construct()
{
$this->collection = new ArrayCollection();
}
// this works only with Doctrine 2.5+, in previous version association where not loaded on event
public function postLoad(array $conditions)
{
/*
* From your example 1)
* Remember that $this->collection is an ArryCollection when constructor is called,
* but a PersistentCollection when are loaded from DB. Don't recreate the instance!
*/
// Get the values for the ArrayCollection and sort it using the function
$values = $this->collection->getValues();
// sort as you like
asort($values);
// Clear the current collection values and reintroduce in new order.
$collection->clear();
foreach ($values as $key => $item) {
$collection->set($key, $item);
}
}
}
Final Notes
I won't use case 1) as is, since is very complicated and introduce inheritance which reduce encapsulation. Also, I think that it has the same complexity and performance of my example.
Case 5) is not necessarily bad. If "the service" is the application repository, and it use DQL to sort, then is my first best case. If is a custom service only to sort a collection, then I think is definitely not a good solution.
All the codes I wrote here is not ready for "copy-paste", since my objective was to show my point of view. Hope it would be a good starting point.
Disclaimer
These are "my" best solutions, as I do it in my works. Hope will help you and others.
So i have a string, representing several objects (tags in this case)
i.e.: "php,mysql,doctrine2"
Let's say my database already has "php" and "doctrine2".
Now i want the best way to add the missing elemets (in this case mysql).
Should i create an object for every element and just use persist/sync or something, or is there a better way?
I need all the objects at the end anyway to add them to a new object (with a simple many-to-many relation) anyway.
I'd be happy about any suggestions.
1) Pull out all your tag names with a single query into an array
2) Use array_filter along with a closure to detect tags not present in the dataset
3) Create an insert for the new tags
$currentTags = getCurrentTagsArray();
$newTags = explode(',', 'php,mysql,doctrine2');
$newTagsToSave = array_filter($currentTags, function($item) use ($newTags){
if (in_array($item, $newTags))
{
return false;
}
return true;
});
Or...
You can use Doctrine 2's ArrayCollection wrapper (\Doctrine\Common\Collections\ArrayCollection()) it has pretty much the same implementation above as a filter method (you still need to pass the closure).
$myCollection->filter($closure);
I had a similar problem where I had to synchronize an entity collection with an external source. However, my problem required not only additions, but also updates and deletes. I used code to diff the ArrayCollection with another array, and call CRUD methods add based on the differences. As far as I can tell from the docs, doctrine doesn't natively handle this. Average performance should be O(n) but takes some memory.
/**
* #param array $source - the array we are starting with
* #param array $new - the array we want to end with
* #param $fnHash - function used to determine object equality, not based on object id
* #param $fnUpdate - function to perform update of existing object, takes current object and new object as params
* #param $fnAdd - function to perform insert
* #param $fnDelete - function to perform delete
*/
public static function syncArrays(array $source, array $new,
$fnHash, $fnUpdate, $fnAdd, $fnDelete)
{
// make modifiable array copies mapped by hashes of the elements
$sourceKeys = array_map($fnHash, $source);
$hasKeys =count($sourceKeys) > 0;
$newarray = ($hasKeys) ? array_combine(array_map($fnHash, $new), $new) : $new;
if ($hasKeys) { // true => may have updates or deletes
$sourcearray = array_combine($sourceKeys, $source);
// updates
foreach ($sourceKeys as $hashkey) {
if (isset($sourcearray[$hashkey]) && isset($newarray[$hashkey])) {
$fnUpdate($sourcearray[$hashkey], $newarray[$hashkey]);
unset($sourcearray[$hashkey]);
unset($newarray[$hashkey]);
}
}
// deletes
foreach ($sourcearray as $entity) {
$fnDelete($entity);
}
}
//adds
foreach ($newarray as $entity) {
$fnAdd($entity);
}
}
The way I call it to update my doctrine association $parentEntity->getPayments() is:
ArrayHelper::syncArrays($parentEntity->getPayments()->toArray(), $newPayments,
function($entity) {return $a->getName();}, // hash function
function($current, $new) {
$current->setTotal($new->getTotal()); // update function
},
function($a) use ($parent, $manager) {
$parent->addVendorPaymentObject($a); // add function
$manager->persist($a);
},
function($a) use ($manager) { // delete function
$manager->remove($a);
}
);