Laravel Updating Polymorphic Relations - laravel

Let's say I have 3 tables, Events, Matches and Training. Events is a polymorphic table, it can be either Match or Training.
------------------ ----------- --------------
| Events | | Matches | | Trainings |
------------------ ---------- --------------
| id | | id | | id |
| name | | home | | type |
| eventable_id | | away | --------------
| eventable_type | -----------
------------------
Now, I create an event like this:
$match = Match::create(['home' => Team 1', 'away' => 'Team 2']);
$match->events()->create(['name' => 'Event name']);
Therefore I have this record in my Events table:
--------------------------------------
| 1 | 'Event name' | 1 | 'App\Match' |
--------------------------------------
And in Matches:
---------------------------
| 1 | 'Team 1' | 'Team 2' |
---------------------------
My Event Model:
public function eventable() {
return $this->morphTo();
}
My Match/Training Model:
public function events() {
return $this->morphMany('App\Event', 'eventable');
}
However, what if I want to update that Event and I want to change it from Match To Training but keep the event.
Is there a more Eloquent way or do I have to do it like this ?
$event->eventable->delete();
$training = Training::create(['type' => 'Stamina Training']);
$event->update([
'name' => $request->name,
'eventable_id' => $training->id,
'eventable_type' => 'App\Training'
]);

Alright, I also faced the same issue and the solution is to use the associate function on the relation. Instead of deleting the "eventable" on the "event", you can simply associate another model instance to the morph relationship. like below.
$training = Training::create(['type' => 'Stamina Training']);
$event->eventable()->associate($training);
$event->update([
'name' => $request->name
]);

I solved this issue by following the same convention on this related question. It's not a Laravel way but it could do the trick.

Related

Laravel query builder: group relations and count total amount

I'm using Laravel 9 and want to create chart for user-role dependency and I need count amount of users with specific role
This is my tables (for example)
// users
| id | name |
| --- | --- |
| 1 | John Doe |
| 2 | Not John Doe |
// roles
| id | slug |
| --- | ----- |
| 1 | admin |
| 2 | ga |
// role_users
| user_id | role_id |
| ------- | ------- |
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
I want to count users with admin role
User::whereHas('roles', fn ($query) => $query->whereSlug(Role::ADMIN()))->count()
It works, I get correct number but I need to do same query for another roles so this is where things become ugly. I want to avoid doing this
$a = User::whereHas('roles', fn ($query) => $query->whereSlug(Role::ADMIN()))->count(); // 2
$b = User::whereHas('roles', fn ($query) => $query->whereSlug(Role::MODERATOR()))->count(); // 10
$c = User::whereHas('roles', fn ($query) => $query->whereSlug(Role::GA()))->count(); // 12
$d = User::whereHas('roles', fn ($query) => $query->whereSlug(Role::CA()))->count(); // 85
At the end I need to get an array of numbers like [2, 10, 12, 85]. How can I do it using Laravel queries?
Was able to achieve similar with
$users_count = DB::table('role_users')
->select(DB::raw('count(*) as users_count, role_id'))
->groupBy('role_id')
->get()
->mapWithKeys(fn($item, $key) => [$item->role_id => $item->users_count])
->toArray();
Now I can access count array with array_values($users_count)

Laravel 5.8 Backpack addColumn custom type

We are taking over a site and before rebuilding it we need to fix a few issues using
Laravel 5.8
Backpack crud 3.6
The dashboard shows all users (id, name, email) from the MySql users table, which is straight forward using
$this->crud->setFromDb();
Another column is created to display the status (active/inactive) of the user in relation to subscriptions stored in a different table as the users table.
This column uses the addColumn method with a custom column type like so:
$columnStat = [
'label' => "Status",
'type' => 'user_status',
'name' => 'id', // the db column for the foreign key
'key' => 'user_status', // this key helps to differentiate between column using same name
'entity' => 'subscriptions', // the method that defines the relationship in your Model
'attribute' => 'user_id', // foreign key attribute that is shown to user
'model' => "App\Models\UserSubscriptions" // foreign key model
];
$this->crud->addColumn($columnStat);
The custom type is defined in a blade file backpack/crud/columns/user_status.blade.php:
#php
$isActive = false;
$user_id = $entry->{$column['name']};
if(isset($user_id) && is_int($user_id)) {
$subscription = \App\Models\UserSubscription::where('end_date', '>' , Carbon\Carbon::today())
->where('payment_status', '=' , 1)
->where('user_id', '=' , $user_id)
->get()->last();
if($subscription){
$isActive = true;
}
}
#endphp
{{-- regular object attribute --}}
#if($isActive)
<td>Active</td>
#else
<td>Inactive</td>
#endif
The MySql table for user_subcriptions is the following:
mysql> desc user_subscriptions;
+-------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+----------------+
| id | int unsigned | NO | PRI | NULL | auto_increment |
| subscription | text | YES | | NULL | |
| start_date | date | YES | | NULL | |
| end_date | date | YES | | NULL | |
| user_id | int unsigned | NO | MUL | NULL | |
| payment_status | tinyint(1) | NO | | 0 | |
| deleted_at | timestamp | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-------------------+--------------+------+-----+---------+----------------+
This is all working fine (active and inactive are displayed correctly) except that the column can't be sorted in ascending or descending order.
The Backpackforlaravel docs show an example on how to improve the column to get a custom order:
(search internet for: backpackforlaravel.com and custom-order-logic-for-columns)
$this->crud->addColumn([
// Select
'label' => 'Category',
'type' => 'select',
'name' => 'category_id', // the db column for the foreign key
'entity' => 'category', // the method that defines the relationship in your Model
'attribute' => 'name', // foreign key attribute that is shown to user
'orderable' => true,
'orderLogic' => function ($query, $column, $columnDirection) {
return $query->leftJoin('categories', 'categories.id', '=', 'articles.select')
->orderBy('categories.name', $columnDirection)->select('articles.*');
}
]);
I just don't understand how to implement this here in my case.
My questions:
How can I modify the code to make the status column sortable in ascending or descending order?
is there a way for me to output/log the query of the column so that I can understand how to modify it?
Any help would be highly appreciated.
To view the query behind the column and log it, view the detailed answer given here: How to see AJAX query behind list view in Backpack?. To add to this previous answer, if you have Laravel Debugbar (https://github.com/barryvdh/laravel-debugbar) installed you can display the Backpack column query there using this: \Debugbar::info(asSql($this->crud->query));

Laravel search by date plus time from releated table

I have table order related to warehouse with related to warehouse_delivery.
With following structure:
+--------------------------------------+ +-----------+
| Order | | Warehouse |
+----+--------------+------+-----------+ +----+------+
| id | warehouse_id | cost | send_date | | id | name |
+----+--------------+------+-----------+ +----+------+
+--------------------------------------------------------------------------+
| WarehouseDelivery |
+----+--------------+------------------------+-----------------------------+
| id | warehouse_id | domestic_delivery_time | international_delivery_time |
+----+--------------+------------------------+-----------------------------+
Delivery time is stored as days.
And now I would like to search by send_date to find Orders where send_date + domestic_delivery_time is today.
I don't have a idea how can I grab that domestic_delivery_time added to send_date.
Order::where('warehouse_id', $warehouseId)
->with(['product', 'warehouse.warehouseDelivery'])
->get();
How to write query?
Thank you.
I'm not sure there's a database-agnostic solution for this.
You'd need to use DB::raw. Here's a solution using a scope that covers MySQL, SQL Server and PostgreSQL:
// On Owner.php
public function scopeDueToday($query) {
$today = \Carbon\Carbon::now()->format('Y-m-d');
$driver = $query->getConnection()->getDriverName();
return $query->whereHas('warehouse.warehouseDelivery', function ($deliveries) use ($today, $driver) {
$raw = [
'mysql' => "CAST(start_date + INTERVAL domestic_delivery_time days AS date)",
'pgsql' => "(start_date + (domestic_delivery_time || ' day')::interval)::date",
'sqlsrv' => "CONVERT(date, DATEADD(day, domestic_delivery_time, start_date))",
];
return $deliveries->where(\DB::raw($raw[$driver]), '=', $today);
});
}
// On your controller
Order::dueToday()->where('warehouse_id', $warehouseId)
->with(['product', 'warehouse.warehouseDelivery'])->get();

Performance of whereHas laravel

I have read many articles warning about whereHas(it use subquery in where exists) performance when the data is big(Here: 18415, 5328, ...). In my case below will I need to replace it? And how?.
Table products
+----+---------------------+
| id | created_at |
+----+---------------------+
| 1 | 2020-10-10 10:10:10 |
| 2 | 2020-10-10 10:10:10 |
+----+---------------------+
Table product_translations(indexes: map_id, language_code, slug)
+----+--------+---------------+-----------+-----------+
| id | map_id | language_code | name | slug |
+----+--------+---------------+-----------+-----------+
| 1 | 1 | en | name en 1 | name-en-1 |
| 2 | 1 | es | name es 1 | name-es-1 |
| 3 | 2 | en | name en 2 | name-en-2 |
| 4 | 2 | es | name es 2 | name-es-2 |
+----+--------+---------------+-----------+-----------+
Product.php
function translation(){
return $this->hasOne('App\Models\ProductTranslation', 'map_id', 'id');
}
ProductController.php
function view($slug){
$currentItem = Product::with(['translation' => function($q) use($slug){
$q->where(['slug' => $slug, 'language_code' => \App::getLocale()]);
}])->whereHas('translation', function($q) use ($slug){
$q->where(['slug' => $slug, 'language_code' => \App::getLocale()]);
})
if ($currentItem == null) {
abort(404);
}
return view('product.view', compact('currentItem'));
}
I'm using laravel 8x.
You can replace whereHas with whereIn.
$currentItem = Product::with(['translation' => function($q) use($slug){
$q->where(['slug' => $slug, 'language_code' => \App::getLocale()]);
}])->whereIn(
'id',
ProductTranslation::select('map_id')
->where(['slug' => $slug, 'language_code' => \App::getLocale()])
);
Select the foreign key from a subselect and uses it with where in.

Validation rule to check against multiple rows

When updating location names, I need validation to check against multiple rows in the table, not just a single id.
My table looks like this:
+----+--------------+-----------+----------+
| id | name | lat | lng |
+----+--------------+-----------+----------+
| 1 | Location One | 53.348333 | 0.348333 |
| 2 | Location One | 57.348222 | 0.348222 |
| 3 | Location One | 57.348111 | 0.545454 |
| 4 | Location Two | 55.348554 | 0.555444 |
| 5 | Location Two | 56.348667 | 0.348333 |
| 6 | Location Two | 56.348778 | 0.111111 |
+----+--------------+-----------+----------+
Creating new locations works as expected. But I'm not sure how to set the validation rule to exclude the current location I'm trying to update - it needs to check against the 'name' column.
I was hoping something like this may work - but it doesn't.
public function rules()
{
return [
'name' => 'required|max:255|unique:evac_routes,name,'.$this->name,
...
];
}
In my controller I'm using Route::where('name', $route->name)->update, which works, but I can't convert this logic to the validation rule:
public function update($id, UpdateRouteRequest $request)
{
$route = Route::findOrFail($id);
$updateRows = Route::where('name', $route->name)->update([
'name' => $request->name,
...
]);
return redirect('routes');
}
you need to create custome validation and pass array of ids that want to be checked such as:
public function multipelvalidation()
{
Validator::extend('multipel', function ($attribute, $value) {
//check validation here and return true or false
});
}
//... and use your rule in validation like below:
'name' => 'required|multipel|max:255|unique:evac_routes,name,'.$this->name,

Resources