Two of my tables (clients and products) have a ManyToMany relation using Laravel's blongToMany and a pivot table.
Now I want to check if a certain client has a certain product.
I could create a model to check in the pivot table but since Laravel does not require this model for the belongsToMany method I was wondering if there is another way to check if a certain relationship exists without having a model for the pivot table.
I think the official way to do this is to do:
$client = Client::find(1);
$exists = $client->products->contains($product_id);
It's somewhat wasteful in that it'll do the SELECT query, get all results into a Collection and then finally do a foreach over the Collection to find a model with the ID you pass in. However, it doesn't require modelling the pivot table.
If you don't like the wastefulness of that, you could do it yourself in SQL/Query Builder, which also wouldn't require modelling the table (nor would it require getting the Client model if you don't already have it for other purposes:
$exists = DB::table('client_product')
->whereClientId($client_id)
->whereProductId($product_id)
->count() > 0;
The question is quite old but this may help others looking for a solution:
$client = Client::find(1);
$exists = $client->products()->where('products.id', $productId)->exists();
No "wastefulness" as in #alexrussell's solution and the query is more efficient, too.
Alex's solution is working one, but it will load a Client model and all related Product models from DB into memory and only after that, it will check if the relationship exists.
A better Eloquent way to do that is to use whereHas() method.
1. You don't need to load client model, you can just use his ID.
2. You also don't need to load all products related to that client into memory, like Alex does.
3. One SQL query to DB.
$doesClientHaveProduct = Product::where('id', $productId)
->whereHas('clients', function($q) use($clientId) {
$q->where('id', $clientId);
})
->count();
Update: I did not take into account the usefulness of checking multiple relations, if that is the case then #deczo has a way better answer to this question. Running only one query to check for all relations is the desired solution.
/**
* Determine if a Client has a specific Product
* #param $clientId
* #param $productId
* #return bool
*/
public function clientHasProduct($clientId, $productId)
{
return ! is_null(
DB::table('client_product')
->where('client_id', $clientId)
->where('product_id', $productId)
->first()
);
}
You could put this in you User/Client model or you could have it in a ClientRepository and use that wherever you need it.
if ($this->clientRepository->clientHasProduct($clientId, $productId)
{
return 'Awesome';
}
But if you already have defined the belongsToMany relationship on a Client Eloquent model, you could do this, inside your Client model, instead:
return ! is_null(
$this->products()
->where('product_id', $productId)
->first()
);
#nielsiano's methods will work, but they will query DB for every user/product pair, which is a waste in my opinion.
If you don't want to load all the related models' data, then this is what I would do for a single user:
// User model
protected $productIds = null;
public function getProductsIdsAttribute()
{
if (is_null($this->productsIds) $this->loadProductsIds();
return $this->productsIds;
}
public function loadProductsIds()
{
$this->productsIds = DB::table($this->products()->getTable())
->where($this->products()->getForeignKey(), $this->getKey())
->lists($this->products()->getOtherKey());
return $this;
}
public function hasProduct($id)
{
return in_array($id, $this->productsIds);
}
Then you can simply do this:
$user = User::first();
$user->hasProduct($someId); // true / false
// or
Auth::user()->hasProduct($someId);
Only 1 query is executed, then you work with the array.
The easiest way would be using contains like #alexrussell suggested.
I think this is a matter of preference, so unless your app is quite big and requires a lot of optimization, you can choose what you find easier to work with.
Hello all) My solution for this problem: i created a own class, extended from Eloquent, and extend all my models from it. In this class i written this simple function:
function have($relation_name, $id) {
return (bool) $this->$relation_name()->where('id','=',$id)->count();
}
For make a check existing relation you must write something like:
if ($user->have('subscribes', 15)) {
// do some things
}
This way generates only a SELECT count(...) query without receiving real data from tables.
To check the existence of a relationship between 2 models, all we need is a single query against the pivot table without any joins.
You can achieve it using the built-in newPivotStatementForId method:
$exists = $client->products()->newPivotStatementForId($product->id)->exists();
use trait:
trait hasPivotTrait
{
public function hasPivot($relation, $model)
{
return (bool) $this->{$relation}()->wherePivot($model->getForeignKey(), $model->{$model->getKeyName()})->count();
}
}
.
if ($user->hasPivot('tags', $tag)){
// do some things...
}
This has time but maybe I can help someone
if($client->products()->find($product->id)){
exists!!
}
It should be noted that you must have the product and customer model, I hope it helps,
Related
I'm currently struggling with retrieving data towards a parent model. I'll drop my database, classes, and things I've tried before.
I have 4 tables: sales_orders, products, work_orders, and product_sales_order (pivot table between sales_orders and products).
SalesOrder.php
class SalesOrder extends Model
{
public function products()
{
return $this->belongsToMany(Product::class)
->using(ProductSalesOrder::class)
->withPivot(['qty', 'price']);
}
}
ProductSalesOrder.php
class ProductSalesOrder extends Pivot
{
public function work_orders()
{
return $this->hasMany(WorkOrder::class);
}
public function getSubTotalAttribute()
{
return $this->qty* $this->price;
}
}
WorkOrder.php
class WorkOrder extends Model
{
public function product_sales_order()
{
return $this->belongsTo(ProductSalesOrder::class);
}
public function sales_order()
{
return $this->hasManyThrough(
ProductSalesOrder::class,
SalesOrder::class
);
}
}
So, what I want to retrieve sales order data from work order since both tables don't have direct relationship and have to go through pivot table and that is product sales order. I've tried hasOneThrough and hasManyThrough but it cast an error unknown column. I understand that error and not possible to use that eloquent function.
Is it possible to retrieve that sales order data using eloquent function from WorkOrder.php ?
You cannot achieve what you want using hasOneThrough as it goes from a table that has no ID related to the intermediate model.
In your example you are doing "the inverse" of hasOneThrough, as you are going from a model that has the ID of the intermediate model in itself, and the intermediate model has the ID of your final model. The documentation shows clearly that hasOneThrough is used exactly for the inverse.
So you still should be able to fix this, and use a normal relation as you have the sales_orders_id in your model SuratPerintahKerja, so you can use a normal relation like belongsTo to get just one SalesOrder and define it like this:
public function salesOrder()
{
return $this->belongsTo(SalesOrder::class, 'sale_orders_id');
}
If you want to get many SalesOrders (if that makes sense for your logic), then you should just run a simple query like:
public function salesOrders()
{
return $this->query()
->where('sale_orders_id', $this->sale_orders_id)
->get();
}
Have in mind that:
I have renamed your method from sales_order to salesOrder (follow camel case as that is the Laravel standard...).
I have renamed your method from sales_order to salesOrders for the second code as it will return more than 1, hence a collection, but the first one just works with one model at a time.
I see you use sale_orders_id, but it should be sales_order_id, have that in mind, because any relation will try to use sales_order_id instead of sale_orders_id, again, stick to the standards... (this is why the first code needs more parameters instead of just the model).
All pivot tables would still need to have id as primary and auto incremental, instead of having the id of each related model as primary... Because in SuratPerintahKerja you want to reference the pivot table ProdukSalesOrder but it has to use both produks_id (should have been produk_id singular) and sale_orders_id (should have been sales_order_id). So if you were able to use something like produk_sales_order_id, you could be able to have better references for relations.
You can see that I am using $this->query(), I am just doing this to only return a new query and not use anything it has as filters on itself. I you still want to use current filters (like where and stuff), remove ->query() and directly use the first where. If you also want to add ->where('produks_id', $this->produks_id) that is valid and doesn't matter the order. But if you do so, I am not sure if you would get just one result, so ->get() makes no sense, it should be ->first() and also the method's name should be salesOrder.
Sorry for this 6 tip/step, but super personal recommendation, always write code in English and do not write both languages at the same time like produks and sales orders, stick to one language, preferrably English as everyone will understand it out of the box. I had to translate some things so I can understand what is the purpose of each table.
If you have any questions or some of my code does not work, please tell me in the comments of this answer so I can help you work it out.
Edit:
After you have followed my steps and changed everything to English and modified the database, this is my new code:
First, edit ProductSalesOrder and add this method:
public function sales_order()
{
return $this->belongsTo(SalesOrder::class);
}
This will allow us to use relations of relations.
Then, have WorkOrder as my code:
public function sales_order()
{
return $this->query()->with('product_sales_order.sales_order')->first();
}
first should get you a ProductSalesOrder, but then you can access ->sales_order and that will be a model.
Remember that if any of this does not work, change all the names to camelCase instead of kebab_case.
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))
I'm a trying to get the total distance of an activity which has multiple steps (with distance) ? For now, I'm doing like this :
Controller :
$total_distance = 0;
foreach (\Auth::user()->activities as $key => $activity) {
$total_distance += $activity->getTotalDistance();
}
Activity Model :
public function steps()
{
return $this->hasMany('App\Step');
}
public function getTotalDistance()
{
return $this->steps->sum('distance');
}
Is there a proper solution to do that ?
Thanks for your help
The way you do it requires quite a lot of DB queries to be run:
1 to fetch user activities
1 per activity to fetch its steps
You can get the number you need with just one query using Eloquent's aggregate function sum():
$total_distance = Step::join('activities', 'activity_id', '=', 'activities.id')->where('activities.user_id', Auth::id())->sum('distance');
You can read about other aggregate methods that Eloquent offers here: http://laravel.com/docs/5.1/queries#aggregates
Just like #Jeemusu said, your approach seems fine. But if you want to do it in some other way you can try to do it in several ways.
Look at Laravel's Has Many Through relationship.
The "has many through" relation provides a convenient short-cut for accessing distant relations via an intermediate relation. For example, a Country model might have many Post through a User model.
So using has many through you can access steps directly something like
$user->steps->sum('distance');
You need to make relationship that user has many steps through activities.
Something like:
class User extends Eloquent {
public function steps() {
$this->hasManyThrough('Steps','Activity','user_id','activity_id');
}
}
The other solution is to put foreign key in steps table so you can directly access it from user model like:
$user->steps->sum('distance');
class User extends Eloquent {
public function steps() {
$this->HasMany('Steps','user_id');
}
}
Hope this can help you.
I am working with Laravel 5 and I am having issue getting ->wherePivot() to work on a Many-to-Many relationship. When I dd() the SQL it looks like Eloquent is looking for records in the pivot table with a `pose_state`.`pose_id` is null`.
I am hoping it is a simple error and not a bug. Any ideas are appreciated.
Database Structure
pose
id
name
type
state
id
name
machine_name
pose_state
pose_id
state_id
status
Models
Pose
<?php namespace App;
use DB;
use App\State;
use Illuminate\Database\Eloquent\Model;
class Pose extends Model {
public function states()
{
return $this->belongsToMany('App\State')
->withPivot('status_id')
->withTimestamps();
}
public function scopeWithPendingReviews()
{
return $this->states()
->wherePivot('status_id',10);
}
}
State
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class State extends Model {
public function poses()
{
return $this->belongsToMany('Pose')
->withPivot('status_id')
->withTimestamps();
}
}
PosesController function
public function listPosesForReview(){
$poses = Pose::withPendingReviews()->get();
dd($poses->toArray() );
}
SQL
select
`states`.*, `pose_state`.`pose_id` as `pivot_pose_id`,
`pose_state`.`state_id` as `pivot_state_id`,
`pose_state`.`status_id` as `pivot_status_id`,
`pose_state`.`created_at` as `pivot_created_at`,
`pose_state`.`updated_at` as `pivot_updated_at`
from
`states` inner join `pose_state` on `states`.`id` = `pose_state`.`state_id`
where
`pose_state`.`pose_id` is null and `pose_state`.`status_id` = ?
EDIT
When I updated my code to removing the scope it worked. Thanks #Deefour for putting me on the right path! Maybe scope has something else to that I am missing.
public function pendingReviews()
{
return $this->states()
->wherePivot('status_id','=', 10);
}
YET ANOTHER EDIT
I finally got this to work. The solution above was giving me duplicate entries. No idea why this works, but it does, so I will stick with it.
public function scopeWithStatusCode($query, $tag)
{
$query->with(['states' => function($q) use ($tag)
{
$q->wherePivot('status_id','=', $tag);
}])
->whereHas('states',function($q) use ($tag)
{
$q->where('status_id', $tag);
});
}
I think your implementation of scopeWithPendingReviews() is an abuse of the intended use of scopes.
A scope should be thought of as a reusable set of conditions to append to an existing query, even if that query is simply
SomeModel::newQuery()
The idea is that a pre-existing query would be further refined (read: 'scoped') by the conditions within the scope method, not to generate a new query, and definitely not to generate a new query based on an associated model.
By default, the first and only argument passed to a scope method is the query builder instance itself.
Your scope implementation on your Pose model was really a query against the states table as soon as you did this
$this->states()
This is why your SQL appears as it does. It's also a clear indicator you're misusing scopes. A scope might instead look like this
public function scopeWithPendingReviews($query) {
$query->join('pose_state', 'poses.id', '=', 'pose_state.pose.id')
->where('status_id', 10);
}
Unlike your new pendingReviews() method which is returning a query based on the State model, this scope will refine a query on the Pose model.
Now you can use your scope as you originally intended.
$poses = Pose::withPendingReviews();
which could be translated into the more verbose
$poses = Pose::newQuery()->withPendingReviews();
Notice also the scope above doesn't return a value. It's accepting the existing query builder object and adding onto it.
The other answer to this question is filled with misinformation.
You cannot use wherePivot() as is claims.
Your use of withTimestamps() is not at all related to your problem
You don't have to do any "custom work" to get timestamps working. Adding the withTimestamps() call as you did is all that is needed. Just make sure you have a created_at and updated_at column in your join table.
I think that your implementation of scopes is fine, the problem I see is just a typo. Your schema shows that the field is called status but your where condition is referring to a status_id
Try:
->wherePivot('status', 10);
Also, the withTimestamps() method is causing issues. You don't have timestamps in your schema for the pivot (as I can see) so you shouldn't be putting these in the your relation definitions as it's trying to fetch the timestamps relating to when the relation was created/updated. You can do this if you set up your pivot table schema to have the timestamp fields, but I think you'll have to do some custom work to get the timestamps to save properly.
This worked for me (Laravel 5.3):
$task = App\Models\PricingTask::find(1);
$task->products()->wherePivot('taggable_type', 'product')->get();
You can also have this problem (return no results) if the column you are using in wherePivot hasn't been added to withPivot.
I'm using Laravel and having a small problem with Eloquent ORM.. I can get this working simply with SQL query using a JOIN but I can't seem to get it working with Eloquent!
This is what I want, I have two tabels. one is 'Restaurants' and other is 'Restaurant_Facilities'.
The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called restaurant_facilities with id, restaurant_id, wifi, parking, etc
Now what I want to do is.. load all restaurants which have wifi = 1 or wifi = 0..
How can i do that with Eloquent ? I have tried eager loading, pivot tables, with(), collections() and nothing seems to work!
The same problem I have for a Many-To-Many relation for cuisines!
I have the same restaurant table and a cuisine table and a restaurant_cuisine_connection table..
but how do I load all restaurants inside a specific cuisine using it's ID ?
This works.
Cuisine::find(6)->restaurants()->get();
but I wanna load this from Restaurant:: model not from cuisines.. because I have many conditions chained together.. its for a search and filtering / browse page.
Any ideas or ways ? I've been struggling with this for 3 days and still no answer.
Example Models :
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities() {
return $this->hasOne('Facilities');
}
}
class Facilities extends Eloquent {
protected $table = 'restaurants_facilities';
public function restaurant() {
return $this->belongsTo('Restaurant');
}
}
PS :
This seems to be working.. but this is not Eloquent way right ?
Restaurant::leftJoin(
'cuisine_restaurant',
'cuisine_restaurant.restaurant_id',
'=', 'restaurants.id'
)
->where('cuisine_id', 16)
->get();
Also what is the best method to find a count of restaurants which have specific column value without another query ? like.. i have to find the total of restaurants which have parking = 1 and wifi = 1 ?
Please help on this.
Thank you.
I don't see anything wrong with doing the left join here, if you have to load from the Restaurant model. I might abstract it away to a method on my Restaurant model, like so:
class Restaurant extends Eloquent {
protected $table = 'restaurants'; // will be default in latest L4 beta
public function facility()
{
return $this->hasOne('Facility');
}
// Or, better, make public, and inject instance to controller.
public static function withWifi()
{
return static::leftJoin(
'restaurant_facilities',
'restaurants.id', '=', 'restaurant_facilities.restaurant_id'
)->where('wifi', '=', 1);
}
}
And then, from your routes:
Route::get('/', function()
{
return Restaurant::withWifi()->get();
});
On the go - haven't tested that code, but I think it should work. You could instead use eager loading with a constraint, but that will only specify whether the facility object is null or not. It would still return all restaurants, unless you specify a where clause.
(P.S. I'd stick with the singular form of Facility. Notice how hasOne('Facilities') doesn't read correctly?)
I stumbled across this post while trying to improve my REST API methodology when building a new sharing paradigm. You want to use Eager Loading Constraints. Let's say you have an api route where your loading a shared item and it's collection of subitems such as this:
/api/shared/{share_id}/subitem/{subitem_id}
When hitting this route with a GET request, you want to load that specific subitem. Granted you could just load that model by that id, but what if we need to validate if the user has access to that shared item in the first place? One answer recommended loading the inversed relationship, but this could lead to a confusing and muddled controller very quickly. Using constraints on the eager load is a more 'eloquent' approach. So we'd load it like this:
$shared = Shared::where('id', $share_id)
->with([ 'subitems' => function($query) use ($subitem_id) {
$query->where('subitem_id', $subitem_id)
}]);
So where only want the subitem that has that id. Now we can check if it was found or not by doing something like this:
if ($shared->subitems->isEmpty())
Since subitems is a collection (array of subitems) we return the subitem[0] with this:
return $shared->subitems[0];
Use whereHas to filter by any relationship. It won't join the relation but it will filter the current model by a related property. Also look into local scopes to help with situations like this https://laravel.com/docs/5.3/eloquent#local-scopes
Your example would be:
Restaurant::whereHas('facilities', function($query) {
return $query->where('wifi', true);
})->get();
Restaurant::whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
})->get();
To achieve the same thing with local scopes:
class Restaurant extends Eloquent
{
// Relations here
public function scopeHasWifi($query)
{
return $query->whereHas('facilities', function($query) {
return $query->where('wifi', true);
});
}
public function scopeHasCuisine($query, $cuisineId)
{
return $query->whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
});
}
}
For local scopes you DO NOT want to define them as static methods on your model as this creates a new instance of the query builder and would prevent you from chaining the methods. Using a local scope will injects and returns the current instance of the query builder so you can chain as many scopes as you want like:
Restaurant::hasWifi()->hasCuisine(6)->get();
Local Scopes are defined with the prefix scope in the method name and called without scope in the method name as in the example abover.
Another solution starring whereHas() function:
$with_wifi = function ($query) {
$query->where('wifi', 1);
};
Facilities::whereHas('restaurant', $with_wifi)
Nice and tidy.
Do you absolutely have to load it from the Restaurant model? In order to solve the problem, I usually approach it inversely.
Facilities::with('restaurant')->where('wifi' ,'=', 0)->get();
This will get all the restaurant facilities that match your conditions, and eager load the restaurant.
You can chain more conditions and count the total like this..
Facilities::with('restaurant')
->where('wifi' ,'=', 1)
->where('parking','=', 1)
->count();
This will work with cuisine as well
Cuisine::with('restaurant')->where('id','=',1)->get();
This grabs the cuisine object with the id of 1 eager loaded with all the restaurants that have this cuisine