users table have the following columns:
created_at (timestamp)
last_order (timestamp)
when new users is created then last_order is null and created_at is current timestamp.
When user place an order then last order column is updated with timestamp of order placing time.
Now I want to retrieve all users who didn't place any order last 7 days. I define my user model as follows:
class User extends Model
{
/**
* #var string The database table used by the model.
*/
public $table = 'users';
/**
* #var array Validation rules
*/
public $rules = [
];
public $hasMany = [
'voucher' => ['ItScholarBd\Api\Models\Voucher'],
'order' => ['ItScholarBd\Api\Models\OrderLog']
];
public function scopeCustomer($query)
{
return $query->where('role_id', '=', 5);
}
public function scopeNew($query,$days)
{
return $query->where('created_at', '>=', Carbon::now()->subDays($days));
}
public function scopeIdle($query,$days)
{
$dayOffset = Carbon::now()->subDays($days);
return $query->where(function($q,$dayOffset){
$q->whereNull('last_order')
->where('created_at','<',$dayOffset);
})->orWhere(function($q,$days){
$q->whereNotNull('last_order')
->whereRaw("DATEDIFF(created_at,last_order)>$days");
});
}
}
Here scopeNew() is working perfectly but scopeIdle() is throwing the following error:
Type error: Too few arguments to function App\User::App\{closure}(),
1 passed in .\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php on line 222 and exactly 2 expected
You don't get to make up parameters for functions you don't control. The closure will be passed a single argument, and you need to import any other variables with use:
public function scopeIdle($query, $days)
{
$dayOffset = Carbon::now()->subDays($days);
return $query
->where(function ($q) use ($dayOffset) {
$q->whereNull('last_order')
->where('created_at', '<', $dayOffset);
})
->orWhere(function ($q) use ($days) {
$q->whereNotNull('last_order')
->whereRaw("DATEDIFF(created_at, last_order) > ?", [$days]);
});
}
Note the use of parameters in the whereRaw statement, which can save you from SQL injection. You will also find your code easier to work on if you consistently indent and space it.
Try using a closure:
return $query->where(function($q) use ($dayOffset){
Related
I have a Laravel 5.4 app I created a while back. I need to add a functionality:
I need to fetch a list of users with a sum of pending orders amount.
name |email |pending_amount
user1_name |user1_email |1249
user2_name |user1_email |23424
I have these models
User.php
...
public function orders()
{
return $this->hasMany(Order::class, 'writer_id', 'id');
}
...
Order.php
class Order extends Model
{
//
protected $fillable = [
'source_id',
'client_id',
'user_id',
'status_id', // unpaid, pending or completed
'title',
'description',
'amount',
];
This is what I have.
public function index()
{
//
$writers = User::whereHas("roles", function ($q) {
$q->where("name", "customer")
->orWhere("name", "customer");
})
->with("orders")
->get();
return response()->json([
'customers' => $customers
]);
The query above is providing all the orders that a user has, however I only need the total amount of pending orders from the orders table.
The end result should be something like this
Update
This is what I have on the fetch query
<?
$users = User::whereHas("roles", function ($q) {
$q->where("name", "user")
->orWhere("name", "users");
})
->whereHas("orders", function($q) use($completed, $approved) {
$q->where('status_id', $completed)
->orWhere('status_id', $approved)
->sum('amount');
})
->get();
But am getting this error
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'users.id' in 'where clause' (SQL: select sum(`amount`) as aggregate from `orders` where `users`.`id` = `orders`.`user_id` and `status_id` = 9 or `status_id` = 6)
Possible solution is the following
$users = whereHas("roles", function ($q) {
$q->where("name", "user")
->orWhere("name", "users");
})
->whereHas("orders", function($q) use($completed, $approved) {
$q->where('status_id', $completed)
->orWhere('status_id', $approved);
})
})->with('orders')->get();
And then make a calculated field in your user model class. like this
private $pendingAmount
/**
* #return mixed
*/
public function getPendingAmount()
{
return $this->pendingAmount;
}
/**
* #param mixed $pendingAmount
*/
public function setPendingAmount($pendingAmount)
{
$this->pendingAmount = $pendingAmount;
}
public function calcPendingAmount(){
$this->pendingAmount = $this->orders()->sum('amount');
}
Next, in the loop, calculate the sum for each user.
foreach($users as &$user){
$user->calcPendingAmount();
}
This field can be accessed using the get method
There is no function "withSum" in laravel 5.4,like the latest versions of Laravel. therefore, either an additional loop or not use eloquent, but use DB facade and query builder.
Database: Query Builder
So I have a class Order extends Model.
I created an Accessor called requiresApproval that returns true or false.
public function getRequiresApprovalAttribute(): bool
{
if ($some_physical_column_from_db === 'does not matter') {
return true;
}
return false;
}
When I have my Order model and I call $order->requiresApproval I get my boolean value. Everything works great.
However I need this accessor to appear on the list of attributes because I want to use it in my repository class in where condition within query.
So based on the official documentation, I added:
protected $appends = [
'requires_approval',
];
but when I dd() my Order, this attribute is not on the list of attributes (while $appends property indicates the accessor is there).
Long story short:
When in my repository I call:
public function getOrdersEligibleToBeSendToOptions(): Collection
{
$columns = [
'*',
];
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->where('requiresApproval', '=', true) // this fails
// ->where('requires_approval', '=', true) // this fails as well
->get($columns);
}
I get:
What am I doing wrong? How can I use my accessor within repository class?
OK, this works, but the reason I don't like this solution is the fact that just half of the conditions are on the DB layer, the rest is by filtering what's already fetched.
If the query is going to return (let's say) thousand of records and filter returns just a few of them I personally see this as a huge waste of DB resource.
public function getOrdersEligibleToBeSendToOptions(): Collection
{
$columns = [
'*',
];
$results = $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->get($columns);
return $results->filter(function ($value, $key) {
return $value->requiresApproval === false;
});
}
Eloquent queries work on the database fields, but you can use your accessor after fetching a colletion from the database like this.
Here is some good article about this:
https://medium.com/#bvipul/laravel-accessors-and-mutators-learn-how-to-use-them-29a1e843ce85
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->get($columns)
->filter(function ($row) {
return $row->requires_approval === true;
});
The model virtual attributes cannot be used within queries. Perhaps a better approach would be to create a scope to enforce this constraint on a query:
class Order extends Model
{
public function scopeRequiresApproval($query)
{
return $query->where('some_column', '>', 100);
}
}
Then
return $this->model
->where('status_uuid', '<>', OrderStatusesInterface::STATUS_COMPLETED)
->requiresApproval()
->get($columns);
It's hard to explain what I need, but I will try.
$this->order->where("client_id", 5)
->with([
"roadOrderStatus" => function ($query) {
$query->select("id", "identifier");
},
"currency" => function ($query) {
$query->join("translations", "currencies", '=', 'translations.id')
->join('translation_entries', 'translations.id', '=', 'translation_entries.translation_id')
->join('languages', 'translation_entries.language_id', '=', 'languages.id')
->whereIn('languages.code', ["eng"]);
},
])->get();
I have two relationships on order (roadOrderStatus and currency). if in callbacks, I don't write anything, response is great. it works. but currency needs translation, because (currencies table has column called name and it's integer, because it gets translated). so if you look at my code, when fetching currency relationship , i also need to join to translations to get translated currency, but if I write joins in currency relationship callback like above, it returns - currency - null
I tried -
1) ->whereIn('languages.code', ["eng"])->get();
2) return $query->join("translations", .e.t.c)
My order model--
<?php
namespace App\Http\Models\Orders;
use App\Http\Traits\ScopeTrait;
use Illuminate\Database\Eloquent\Model;
class RoadOrder extends Model
{
use ScopeTrait;
public function orderFinancialProperty(){
return $this->morphOne("App\Http\Models\Orders\OrderFinancialProperty", "orderFinancialPropertable");
}
public function roadOrderStatus(){
return $this->belongsTo("App\Http\Models\Orders\RoadOrderStatus");
}
public function roadTransportType(){
return $this->belongsTo("App\Http\Models\Transport_Type\RoadTransportType");
}
public function currency(){
return $this->belongsTo('App\Http\Models\Statics\Currency');
}
public function insurancer(){
return $this->belongsTo("App\Http\Models\Insurance\Insurancer");
}
}
I'm using Laravel 5.4
Working code
$cityWithEvents = City::with(['events' => function ($q) {
$q->whereDate('start_time', Carbon::today('America/Montreal'))->orwhereBetween('start_time', [Carbon::today('America/Montreal'), Carbon::tomorrow('America/Montreal')->addHours(4)]);
}])->where('active', 1)->get()->keyBy('id');
Not working code
$cityWithEvents = City::with('todayEventsWithAfterHoursIncluded')
->where('active', 1)
->get()
->keyBy('id');
City model
public function events() {
return $this->hasManyThrough('App\Event', 'App\Venue');
}
public function todayEventsWithAfterHoursIncluded () {
return $this->events()
->whereDate('start_time', Carbon::today('America/Montreal'))
->orwhereBetween('start_time', [
Carbon::today('America/Montreal'),
Carbon::tomorrow('America/Montreal')->addHours(4)
]);
}
Questions
When trying to create a scope method the query gives me different result. I can't see why and what should I change
I've only used scopes a few times, but never within a ->with() clause. On your City model, create a new scope:
public function scopeTodayEventsWithAfterHoursIncluded($query){
return $query->with(["events" => function($subQuery){
$subQuery->whereDate('start_time', Carbon::today('America/Montreal'))->orWhereBetween('start_time', [Carbon::today('America/Montreal'), Carbon::tomorrow('America/Montreal')->addHours(4)]);
});
}
Then, on your City query, add it as a scope function:
$cityWithEvents = City->where('active', 1)
->todayEventsWithAfterHoursIncluded()
->get();
I think the way you are using it requires that your Event model has the scope on it, as you're technically calling with("events") on your base query and your scoped one.
Let me know if this changes you results.
If you do the query, you should do it like this:
$cityWithEvents = City::withTodayEventsWithAfterHoursIncluded()
->where('active', 1)
->get()
->keyBy('id');
You scope in you model should look like this:
public function scopeWithTodayEventsWithAfterHoursIncluded ($query)
{
return $query
->with(['events' => function ($q) {$q
->whereDate('start_time', Carbon::today('America/Montreal'))
->orwhereBetween('start_time', [
Carbon::today('America/Montreal'),
Carbon::tomorrow('America/Montreal')->addHours(4)
]);
}]);
}
Now it should be equal.
I'm developing a web API with Laravel 5.0 but I'm not sure about a specific query I'm trying to build.
My classes are as follows:
class Event extends Model {
protected $table = 'events';
public $timestamps = false;
public function participants()
{
return $this->hasMany('App\Participant', 'IDEvent', 'ID');
}
public function owner()
{
return $this->hasOne('App\User', 'ID', 'IDOwner');
}
}
and
class Participant extends Model {
protected $table = 'participants';
public $timestamps = false;
public function user()
{
return $this->belongTo('App\User', 'IDUser', 'ID');
}
public function event()
{
return $this->belongTo('App\Event', 'IDEvent', 'ID');
}
}
Now, I want to get all the events with a specific participant.
I tried with:
Event::with('participants')->where('IDUser', 1)->get();
but the where condition is applied on the Event and not on its Participants. The following gives me an exception:
Participant::where('IDUser', 1)->event()->get();
I know that I can write this:
$list = Participant::where('IDUser', 1)->get();
for($item in $list) {
$event = $item->event;
// ... other code ...
}
but it doesn't seem very efficient to send so many queries to the server.
What is the best way to perform a where through a model relationship using Laravel 5 and Eloquent?
The correct syntax to do this on your relations is:
Event::whereHas('participants', function ($query) {
return $query->where('IDUser', '=', 1);
})->get();
This will return Events where Participants have a user ID of 1. If the Participant doesn't have a user ID of 1, the Event will NOT be returned.
Read more at https://laravel.com/docs/5.8/eloquent-relationships#eager-loading
#Cermbo's answer is not related to this question. In that answer, Laravel will give you all Events if each Event has 'participants' with IdUser of 1.
But if you want to get all Events with all 'participants' provided that all 'participants' have a IdUser of 1, then you should do something like this :
Event::with(["participants" => function($q){
$q->where('participants.IdUser', '=', 1);
}])
N.B:
In where use your table name, not Model name.
for laravel 8.57+
Event::whereRelation('participants', 'IDUser', '=', 1)->get();
With multiple joins, use something like this code:
$userId = 44;
Event::with(["owner", "participants" => function($q) use($userId ){
$q->where('participants.IdUser', '=', 1);
//$q->where('some other field', $userId );
}])
Use this code:
return Deal::with(["redeem" => function($q){
$q->where('user_id', '=', 1);
}])->get();
for laravel 8 use this instead
Event::whereHas('participants', function ($query) {
$query->where('user_id', '=', 1);
})->get();
this will return events that only with partcipats with user id 1 with that event relastionship,
I created a custom query scope in BaseModel (my all models extends this class):
/**
* Add a relationship exists condition (BelongsTo).
*
* #param Builder $query
* #param string|Model $relation Relation string name or you can try pass directly model and method will try guess relationship
* #param mixed $modelOrKey
* #return Builder|static
*/
public function scopeWhereHasRelated(Builder $query, $relation, $modelOrKey = null)
{
if ($relation instanceof Model && $modelOrKey === null) {
$modelOrKey = $relation;
$relation = Str::camel(class_basename($relation));
}
return $query->whereHas($relation, static function (Builder $query) use ($modelOrKey) {
return $query->whereKey($modelOrKey instanceof Model ? $modelOrKey->getKey() : $modelOrKey);
});
}
You can use it in many contexts for example:
Event::whereHasRelated('participants', 1)->isNotEmpty(); // where has participant with id = 1
Furthermore, you can try to omit relationship name and pass just model:
$participant = Participant::find(1);
Event::whereHasRelated($participant)->first(); // guess relationship based on class name and get id from model instance
[OOT]
A bit OOT, but this question is the most closest topic with my question.
Here is an example if you want to show Event where ALL participant meet certain requirement. Let's say, event where ALL the participant has fully paid. So, it WILL NOT return events which having one or more participants that haven't fully paid .
Simply use the whereDoesntHave of the others 2 statuses.
Let's say the statuses are haven't paid at all [eq:1], paid some of it [eq:2], and fully paid [eq:3]
Event::whereDoesntHave('participants', function ($query) {
return $query->whereRaw('payment = 1 or payment = 2');
})->get();
Tested on Laravel 5.8 - 7.x