Query Builder inside entity - Doctrine - php

I'm looking to a better way to write this function.
It's inside a Doctrine Entity Model
public function getCompanySubscriptions()
{
foreach ($this->subscriptions as $key => $value) {
if ($value->getPlan()->getType() == 'E' && $value->getActive()) {
return $value;
}
}
return null;
}
$this->subscriptions is a many-to-one collection and can have different "types" (but only one of them with type "E").
Problem is: If the Company has too many $subscriptions this function will be too slow to return that only one of type "E" which I need to check when building the view with TWIG. The solution would be to use a QueryBuilder, but I haven't found a way to use it directly from the entity model.

You cannot use a QueryBuilder inside your entity, but instead you can use doctrine Criteria for filtering collections (on SQL level). Check the documentation chapter 8.8. Filtering Collections for more details on Criteria.
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.
For example to only get active subscriptions:
$subscriptions = $this->getCompanySubscriptions();
$criteria = Criteria::create()
->where(Criteria::expr()->eq("active", true));
$subscriptions = $subscriptions ->matching($criteria);
Like that you can solve your performance issues, since the collection is loaded from the database using the conditions from the Criteria directly.
The problem in your case might be that you need to join on Plan, but joining is not possible in a Criteria. So if joining is really necessary then you should consider using a custom query where you do the join with conditions in your company EntityRepository (for example with a QueryBuilder).
Note.
The foreach in your question can be rewritten using the filter method from the ArrayCollection class. The filter takes a predicate and all elements satisfying the predicate will be returned.
Look also here in the doctrine 2 class documentation for more info.
Your predicate would look something like:
$predicate = function($subscription){
$subscription->getPlan()->getType() == 'E' && $subscription->getActive();
}
and then:
return $this->subscriptions->filter($predicate);

Related

Laravel 5.4 - how to find specific record in relation

I have some eloquent models named Client and Company. And my Client may belong to multiple Company:
public function companies()
{
return $this->belongsToMany(Company::class);
}
I'd like to check if provided Client belongs to given Company. This is what I ended up with:
$client->companies
->filter(
function ($value, $key) use ($company) {
return $company->getKey() === $value->getKey();
}
)
->count() > 0;
Is there any shorter way?
Using models and the relationship collection:
$client->companies->contains($company);
Using relationship query to check existence:
$client->companies()->where('company_id', $company->id)->exists();
// even shorter, and don't need to know about the key yourself
$client->companies()->whereKey($company)->exists();
Going form the other direction:
$company->clients->contains($client); // if setup
$company->clients()->where(....)->exists();
You can always use your relationship method to query the relation:
$client->companies()->where('company_id', $company->id)->exists();
This uses the query builder to actually query the relation at the database level, unlike when you treat companies as a property which gets a collection of all of the related rows from the database.
Try
$myArray = $client->companies->comp_id;
where comp_id can be any column name. This should return an array. Then check if the array is empty by doing:
if (empty($myArray))

Laravel exclude current id from query eloquent results

I am fairly new to laravel and I built a little "similar posts" section. So every post has a tag and I query all the id's from the current tag. And then I find all the posts with thoses id's. Now my problem is that the current post is always included. Is there an easy way to exclude the current id when querying?
I can't seem to find anything in the helper function on the laravel docs site
this is my function:
public function show($id)
{
$project = Project::findOrFail($id);
foreach ($project->tags as $tag){
$theTag = $tag->name;
}
$tag_ids = DB::table('tags')
->where('name', "=", $theTag)
->value('id');
$similarProjects = Tag::find($tag_ids)->projects;
return view('projects.show', ['project' => $project, 'similarProjects' => $similarProjects]);
}
An easy way to solve your issue would be to use the Relationship method directly instead of referring to it by property, which you can add additional filters just like any eloquent transaction.
In other words, you would need to replace this:
Tag::find($tag_ids)->projects
With this:
Tag::find($tag_ids)->projects()->where('id', '!=', $id)->get()
Where $id is the current project's id. The reason behind this is that by using the method projects(), you are referring your model's defined Relationship directly (most probably a BelongsToMany, judging by your code) which can be used as a Query Builder (just as any model instance extending laravel's own Eloquent\Model).
You can find more information about laravel relationships and how the Query Builder works here:
https://laravel.com/docs/5.1/eloquent-relationships
https://laravel.com/docs/5.1/queries
However, the way you are handling it might cause some issues along the way.
From your code i can assume that the relationship between Project and Tag is a many to many relationship, which can cause duplicate results for projects sharing more than 1 tag (just as stated by user Ohgodwhy).
In this type of cases is better to use laravel's whereHas() method, which lets you filter your results based on a condition from your model's relation directly (you can find more info on how it works on the link i provided for eloquent-relationships). You would have to do the following:
// Array containing the current post tags
$tagIds = [...];
// Fetch all Projects that have tags corresponding to the defined array
Project::whereHas('tags', function($query) use ($tagIds) {
$query->whereIn('id', $tagIds);
})->where('id', !=, $postId)->get();
That way you can exclude your current Project while avoiding any duplicates in your result.
I don't think that Tag::find($tag_ids)->projects is a good way to go about this. The reason being is that multiple tags may belong to a project and you will end up getting back tons of project queries that are duplicates, resulting in poor performance.
Instead, you should be finding all projects that are not the existing project. That's easy.
$related_projects = Project::whereNotIn('id', [$project->id])->with('tags')->get();
Also you could improve your code by using Dependency Injection and Route Model Binding to ensure that the Model is provided to you automagically, instead of querying for it yourself.
public function show(Project $project)
Then change your route to something like this (replacing your controller name with whatever your controller is:
Route::get('/projects/{project}', 'ProjectController#show');
Now your $project will always be available within the show function and you only need to include tags (which was performed in the "with" statement above)

Laravel - Dynamic relationship using hasManyThough() and unique merge

I can think of several ad-hoc ways to do this, but I'm really looking for a 'best practices' type of solution.
I have 3 tables involved
- users (user_id)
- usages ('user_id', 'provider_id', 'nurse_id', 'patient_id')
- usage_alerts ('usage_id')
Im trying to eager load alerts using hasManyThrough() based on a user's role.
The user_id field is agnostic, and can apply to any role, so merging and filtering needs to take place.
Using $this->hasManyThrough('UsageAlert', 'Usage')->get() will return a collection, making the ->merge() method available. However, when eager loading, on return, i get an error since it's a collection object.
Call to undefined method Illuminate\Database\Eloquent\Collection::addEagerConstraints()
For example, this is my current relation (returns the error above)
public function alerts()
{
$alerts = $this->hasManyThrough('UsageAlert', 'Usage')->get();
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('provider')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'provider_id'));
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('patient')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'patient_id'));
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('nurse')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'nurse_id'));
return $alerts;
}
Any suggestions? Pperhaps too much complexity for a relationship?
Best practice manipulates the relationship, though official documentation on how lacks. For your scenario, you can union the additional queries into the primary "agnostic" relationship:
$relation = $this->hasManyThrough('UsageAlert', 'Usage');
foreach (['provider','patient','nurse'] as $group) {
if (Sentry::getUser()->inGroup(Sentry::findGroupByName($group))) {
$relation->getBaseQuery()->union(
$this->
hasManyThrough('UsageAlert', 'Usage', $group . '_id')->
getBaseQuery()->
select('UsageAlert.*') // limits union to common needed columns
);
}
}
return $relation;
This approach returns a Relation, rather than a Collection, as would be expected by API users.

Advanced Filtering of Associated Entity Collection in Symfony

If I have an associated entity which is a collection, what options do you have when fetching?
e.g. Lets say I have a $view entity with this definition inside it:
/**
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="entity")
* #ORM\OrderBy({"timeMod" = "DESC"})
*/
protected $versions;
public function getVersions() {
return $this->versions;
}
And I want to get the all the versions associated with the entity like this:
$view->getVersions();
This will return a collection. Great. But is it possible to take that collection and filter it by criteria, e.g. newer than a certain date? Or order it by some (other) criteria?
Or at this point are you just expected to do a query on the repository:
$versions = $em->getRepository("GutensiteCmsBundle:View\ViewVersion")->findBy(array(
array(
'viewId', $view->getId(),
'timeMod', time()-3600
)
// order by
array('timeMod', 'DESC')
));
There is a surprisingly unknown feature in recent versions of Doctrine, which makes these sort of queries much easier.
It doesn't seem to have a name, but you can read about it in the Doctrine docs at 9.8 Filtering Collections.
Collections have a filtering API that allows 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.
In your case you could write a method like this on your View entity.
use Doctrine\Common\Collections\Criteria;
class View {
// ...
public function getVersionsNewerThan(\DateTime $time) {
$newerCriteria = Criteria::create()
->where(Criteria::expr()->gt("timeMod", $time));
return $this->getVersions()->matching($newerCriteria);
}
}
This will do one of two things:
If the collection is hydrated, it will use PHP to filter the existing collection.
If the collection is not hydrated, it will fetch a partial collection from the database using SQL constraints.
Which is really great, because hooking up repository methods to your views is usually messy and prone to break.
I also like #igor-pantovic's answer, although I've seen the method cause some funny bugs.
I would personally avoid using order by on annotation directly. Yes, you are supposed to do a query, just as you would if you were using raw SQL without Doctrine at all.
However, I wouldn't do it at that point but even before. In your specific case I would create an ViewRepository class:
class ViewRepository extends EntityRepository
{
public function findWithVersionsNewerThan($id, \DateTime $time)
{
return $this->createQueryBuilder('view')
->addSelect('version')
->join('view.versions', 'version')
->where('view.id = :id')
->andWhere('version.timeMod > :time')
->setParameter('time', $time)
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
}
Now you can do:
$yourDateTime = // Calculate it here ... ;
$view = $em->getRepository("GutensiteCmsBundle:View\ViewVersion")->findWithVersionsNewerThan($yourDateTime);
$versions = $view->getVersions(); // Will only contain versions newer than datetime provided
I'm writing code from the top of my head here directly so sorry if some syntax or method naming error sneaked in.

Difference Between Defaultscope() And Beforefind()?

I have a code for defaultScope:
public function defaultScope()
{
$currentdb = explode('=', Yii::app()->db->connectionString);
return array(
'condition'=> "tenant=:tenant",
'params' => array(":tenant"=>$currentdb[2]));
}
And this code for Beforefind:
public function beforeFind() {
$currentdb = explode('=', Yii::app()->db->connectionString);
$criteria = new CDbCriteria;
$criteria->condition = "tenant=:tenant";
$criteria->params = array(":tenant"=>$currentdb[2]);
$this->dbCriteria->mergeWith($criteria);
parent::beforeFind();
}
I am getting same result in both the functions. Which function is better and why?
I think that both can accomplish what you want, but for me the best usage is using scopes. In the yii guide we can find the following definition for scopes:
A named scope represents a named query criteria that can be combined
with other named scopes and applied to an active record query.
It's is what you want to do: apply some query criteria before executing the query. And since you want those criteria to be added on every query then defaultScope is the way to go!
I disagree. I'm having a database with records for multiple users and I'm trying to filter on those records that should be visible for the current user only. I got stuck today on trying to fixing that with defaultScope and I found out that beforeFind is the way to go in this case. The problem can be nailed down to the fact that beforeFind doesn't seem to be used on the relations while defaultScope is. This means you get stuck when you apply criteria in your defaultScope of an object with relations that are eagerly loaded with alike criteria because of the order in which the criteria are applied in the joins.
Let me try to explain this with Yii's blog guide: when we only want the posts of the current author, we could write the following defaultScope:
$c = new CDbCriteria();
$c->with('author');
$c->addInCondition('author.author_id', array(1,2,3));
return $c;
When using $post->author, we will find out that author.author_id is applied before author is defined as a join. This is not the best example, but you will find yourself having these problems when having more than two joins in your relations.
Therefore, I would suggest using beforeFind instead of defaultScope.

Categories