get *related* linked models in laravel eloquent instead of raw SQL - php

I'm trying to get 'related' linked models by querying a link table, named company_projects which holds (as you expect) the id's of companies and projects (projects are kind of product-categories).
In this case, the used flow to determine a related project is:
Get companies who are in the same project ('product category') as you
Find the other project id's which are linked to those companies
Get the info of the linked projects fetched by last step
What i'm trying to do is already functional in the following raw query:
SELECT
*
FROM
projects
WHERE
projects.id IN
(
SELECT cp1.project_id
FROM company_projects cp1
WHERE cp1.company_id IN
(
SELECT cp1.company_id
FROM projects p
LEFT JOIN company_projects cp2 ON cp2.project_id = p.id
WHERE p.id = X AND cp2.company_id != Y
)
)
AND projects.id != X
X = ID of current project ('product category')
Y = ID of current 'user' (company)
But my real question is, how to do this elegantly in Laravel Eloquent (currently v4.2). I tried it, but I have no luck so far...
Update:
I should note that I do have experience using Eloquent and Models through multiple projects, but for some reason I just fail with this specific query. So was hoping to see an explained solution. It is a possibility that I'm thinking in the wrong way and that the answer is relatively easy.

You will need to utilize Eloquent relationships in order to achieve this. (Note that I am linking to the 4.2 docs as that is what you are using, but I would highly suggest upgrading Laravel to 5.1)
I am assuming you have a 'Company' and 'Project' model already. Inside each of those models, you need to a define a method that references its relationship to the other model. Based on your description, it sounds like the two have a Many to Many relationship, meaning that a company can have many projects and a project can also belong to many companies. You already have a database table linking the two. In the Eloquent ORM this linking table is called a pivot table. When you define your relationships in the model, you will need to pass the name of that pivot table as your second argument. Here's how it could look for you.
Company model:
class Company extends Model
{
/**
* Get the projects attached to a Comapny. Many To Many relationship.
*/
public function projects()
{
return $this->belongsToMany('Project','company_projects');
}
}
Project model:
class Project extends Model
{
/**
* Get the companies this project belongs to. Many To Many relationship.
*/
public function companies()
{
return $this->belongsToMany('Company','company_projects');
}
}
If your models have these relationships defined, then you can easily reference them in your views and elsewhere. For example, if you wanted to find all of the projects that belong to a company with an ID of 1234, you could do this:
$company = Company::find(1234);
$projects = $company->projects;
Even better, you can utilize something called eager loading, which will reduce your data lookup to a single line (this is mainly useful when passing data to views and you will be looping over related models in the view). So those statements above could be rewritten as:
$company = Company::with('projects')->find(123);
This will return your Company model with all its related products as a property. Note that eager loading can even be nested with a dot notation. This means that you can find all the models that link to your main model, and then all the models for those models, and so on and so forth.
With all of this in mind, let's look at what you specifically want to accomplish.
Let us assume that this method occurs in a Controller that is being passed a project id from the route.
public function showCompaniesAndProjects($id)
{
//Get all companies in the same project as you
//Use eager loading to grab the projects of all THOSE companies
//Result will be a Project Object with all Companies
//(and those projects) as Eloquent Collection
$companies = Project::with('companies.projects')->find($id);
//do something with that data, maybe pass it to a view
return view('companiesView')->with('companies',$companies);
}
After defining your relations in your models, you can accomplish that whole query in a single line.

Related

Laravel recursive whereHas on BelongsToMany relationship

I have a model called User which has a BelongsToMany relationship on it called lineManagers(). This relationship returns a collection of User models. The nature of this setup allows for a parent-child style relationship to operate on multiple levels.
The BelongsToMany uses a table called line_manager_user which has a simple schema of mapping a user id to a line manager user id:
| user_id | line_manager_id |
User A -> lineManagers()-> User B
User C -> lineManagers() -> User D
Depending upon certain permissions, I want to be able to query this relationship to multiple levels for users who have a line manager with a specific users.id, potentially using a whereHas() but I'm aware that this could be quite a detriment to performance.
I had tried the below query but to no avail (the last section is the relevant part):
$query = User::query()
->with('lineManagers')
->orderBy('first_name')
->orderBy('last_name')
->havingEmploymentStatus(UserEmploymentStatus::EMPLOYED)
->whereHas('lineManagers.lineManagers.lineManagers', function (Builder $query) {
$query->where('id', $this->getAuthenticatedUser()->id);
});
I don't specifically want 3 levels, ideally the query would retrieve lineManagers until an empty collection is hit. This does need to be something that I done at query level rather than collection level unfortunately
If you made it a function that called itself until it drilled down to the empty collection you could do it. That wouldn't matter how many levels there are then
Check out https://www.sitepoint.com/laravel-blade-recursive-partials/
I wouldn't recommend doing it in the blade file, rather work their "Plain old PHP" code into a function

Should some relationship tables have their own models?

I'm writing an API with an MVC framework in PHP, and I'm using the Eloquent ORM.
My app has some models which are eventually linked through relationship tables, but intended to be created in a separate, decentralized manner.
Should these relationship tables have their own models, or should the models that are related have methods to create links?
With Eloquent, in regards to intermediate or pivot tables with many to many relationships, you shouldn't need to create an additional model.
You should always set up the relationships for related Models with the belongsToMany() method, documented here: https://laravel.com/docs/5.3/eloquent-relationships#many-to-many
class User extends Model
{
/**
* The roles that belong to the user.
*/
public function roles()
{
return $this->belongsToMany('App\Role');
}
}
They have various methods of then using this relationship to adding or updating items to the pivot table including attach, detach, or sync documented here: https://laravel.com/docs/5.3/eloquent-relationships#updating-many-to-many-relationships
$user = App\User::find(1);
$user->roles()->attach($roleId);
You can also add data to extra fields:
$user->roles()->attach($roleId, ['expires' => $expires]);
Extra fields on the pivot table are important when you have data, like timestamps, that relate to the relationship and not either of the related models.
An example I could think of would be if you wanted to maintain a history of store managers. You wouldn't just want a store_id and manager_id, you'd also want a started_at and ended_at timestamp. This would allow you to view who is managing a store right now, but also who managed a store in the past.
With Eloquent, these types of extra fields don't require their own model, they can be accessed through the various methods documented above.

laravel Eloquent join and Object-relationship mapping

Ok so i'm kind of newish to eloquent and laravel (not frameworks tho) but i hit a wall here.
I need to perform some queries with conditions on different tables, so the eager load (::with()) is useless as it creates multiples queries.
Fine, let use the join. But in that case, it seems that Laravel/Eloquent just drops the concept of Object-relationship and just return a flat row.
By exemple:
if i set something like
$allInvoicesQuery = Invoice::join('contacts', 'contacts.id', '=', 'invoices.contact_id')->get();
and then looping such as
foreach ($allInvoicesQuery as $oneInvoice) {
... working with fields
}
There is no more concept of $oneInvoice->invoiceFieldName and $oneInvoice->contact->contactFieldName
I have to get the contacts fields directly by $oneInvoice->contactFieldName
On top of that the same named columns will be overwrited (such as id or created_at).
So my questions are:
Am i right assuming there is no solution to this and i must define manually the field in a select to avoid the same name overwritting like
Invoice::select('invoices.created_at as invoice.create, contacts.created_at as contact_create)
In case of multiple joins, it makes the all query building process long and complex. But mainly, it just ruins all the Model relationship work that a framework should brings no?
Is there any more Model relationship oriented solution to work with laravel or within the Eloquent ORM?
Instead of performing this join, you can use Eloquent's relationships in order to achieve this.
In your Invoice model it would be:
public function contact(){
return $this->belongsTo('\App\Contact');
}
And then of course inside of your Contact model:
public function invoices(){
return $this->hasMany('\App\Invoice');
}
If you want to make sure all queries always have these active, then you'd want the following in your models:
protected $with = ['Invoice']
protected $with = ['Contact'];
Finally, with our relationships well defined, we can do the following:
$invoices = Invoice::all();
And then you can do:
foreach($invoices as $invoice)[
$invoice->contact->name;
$invoice->contact->phone;
//etc
}
Which is what I believe you are looking for.
Furthermore, you can find all this and much more in The Eloquent ORM Guide on Laravel's site.
Maybe a bit old, but I've been in the same situation before.
At least in Laravel 5.2 (and up, presumably), the Eloquent relationships that you have defined should still exist. The objects that are returned should be Invoice objects in your case, you could check by dd($allInvoiceQuery); and see what the objects are in the collection. If they are Invoice objects (and you haven't done ->toArray() or something), you can treat them as such.
To force only having the properties in those objects that are related to the Invoice object you can select them with a wildcard: $allInvoicesQuery = Invoice::select('invoices.*')->join('contacts', 'contacts.id', '=', 'invoices.contact_id')->get();, assuming your corresponding table is called invoices.
Hope this helps.

Best way to apply Eloquent's ORM with Laravel?

I have two tables:
treatments (
id,
name
)
companies (
id,
name
)
And I need to build a relation to a "price" table. I thougth in something like follows:
prices (
treatment_id,
company_id,
price
)
But i don know how to apply the ORM to a php aplication. I'm using Laravel with Eloguent's ORM. I think that the real question would be if this is a good way to design the db. Perhaps I should make it diferent?
Any advices?
Thanks,
Ban.
If a Company can have multiple Treatments and a treatment can be bought from multiple companies at different prices, then you have a Many-to-many relationship, with prices being the pivot table (which if you would adhere to convention would be named company_treament, but that's not a must). So you'll need to have two models for Treatments and Companies, which would look like this:
class Company extends \Eloquent {
public function treatments()
{
return $this->belongsToMany('Treatment', 'prices')->withPivot('price');
}
and
class Treatment extends \Eloquent {
public function companies()
{
return $this->belongsToMany('Company', 'prices')->withPivot('price');
}
}
The treatments() and companies() methods from the models are responsible for fetching the related items. Usually the hasMany method only requires the related model as the first parameter, but in your case the pivot table name is non-standard and is set to prices by passing it as the second parameter. Also normally for the pivot table only the relation columns would be fetched (treatment_id and company_id) so you need to specify the the extra column using withPivot. So if you want to get the treatments for a company with the id 1 list you whould to something like this:
$treatments = Company::find(1)->treatments;
The opposite is also true:
$companies = Treatment::find(1)->companies;
If you need to access the price for any of those relations you can do it like this:
foreach ($treatments as $treatment)
{
$price = $treatment->pivot->price;
}
You can read more about how to implement relationships using Eloquent in the Laravel Docs.
EDIT
To insert a relation entry in the pivot table you can use attach and to remove one use detach (for more info read the Docs).
$treatment->companies()->attach($companyId, array('price' => $price));
$treatment->companies()->detach($companyId);
To update a pivot table entry use updateExistingPivot:
$treatment->companies()->updateExistingPivot($companyId, array('price' => $price));

Adding a custom query to the model

For simplicity's sake, I have two tables: projects and tasks. A project can have many tasks, and a task belongs to a project.
I've set up the database, created the associations and used cake bake to generate the models, controllers and views and all is perfect.
When I look at a the index view for projects, I see a table listing all the projects as expected. What I want to do is really simple: I want a column in that table that shows a count of all the tasks assigned to that project.
The SQL query for this is trivial (returning a list of project names and a count of the tasks):
SELECT projects.name,
(SELECT COUNT(*) FROM tasks WHERE project_id = projects.id) as taskCount
FROM projects
So how do I achieve this in CakePHP?
The index method in the projects controller looks like this at the moment:
function index() {
$this->Project->recursive = 0;
$this->set('projects', $this->paginate());
}
You can use the virtualFields or counterCache, but it is another way.

Categories