Laravel Nova n+1 problem when running sql inside fields() - laravel

For some reason, I need dynamically add columns in the fields method. This is not only dynamic columns but also contains computed fields.
This is very simplified version of what I'm trying to do inside fields():
$additional_fields = [];
Product::visible()->each(function($attr) use (&$additional_fields, $request) {
$additional_fields[] = Text::make($attr->name, function() use ($attr, $request) {
$first_subscription = $this->subscriptions()->whereHas('product', function($q) {
return $q->where('visible', 1);
});
...
}
}
This, of course, causing the N+1 problem as statements for Product and Subscription are executed on every row.
I need to move this piece of code somewhere else and run it only once. I can't figure out how to do this yet.

Your can use indexQuery method in your resource to load relationships
/**
* #param NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$query = $query->with('relation');
return $query;
}

Related

Laravel: how model gets injected from route parameter

I've seen the following route:
Route::prefix('/users/{user}')->group(function () {
Route::get('groups/{group}', 'UserGroupController#show');
}
And in UserGroupController:
use App\Group;
public function show(Request $request, User $user, Group $group)
{
dd($group);
}
My question is how does the $group model object gets constructed here from a raw route parameter string?
My guess is laravel's service container does the following magic
(maybe sth like
Injecting the Group model,
then do sth like Group::where('id', $group)->first()
but unsure about this.
You guess right. There is a binding in the core service provider which retrieves model. The bound model is the same if you would call:
$temp = new Group
$model = Group::where($temp->getRouteKeyName(), request()->route('group'))->firstOrFail();
UPD. Actualy just found where it happens:
/**
* Retrieve the model for a bound value.
*
* #param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation $query
* #param mixed $value
* #param string|null $field
* #return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function resolveRouteBindingQuery($query, $value, $field = null)
{
return $query->where($field ?? $this->getRouteKeyName(), $value);
}

Practical way to only return selected columns in Laravel in method

I am doubting (life of a junior developer) what would be the most practical way to only return the value from a specific column in Laravel of a database record.
Example controller method:
public function show(ProductsCategory $category)
{
return $category;
}
This outputs all the columns, like this
{"id":104,"category_name":"Soft drinks","created_at":"2021-06-09T17:16:54.000000Z","updated_at":"2021-06-09T17:16:54.000000Z"}
However what I am after is just getting the category_name column retuned, like this
{"category_name":"Soft drinks"}
I can accomplish this by doing
public function show($id)
{
$category = ProductsCategory::select('category_name')->findOrFail($id);
echo json_encode($category)
exit;
}
However doubting if this would be the most practical way to go? Is there an more elegant/straight forward way? Or am I grossly overthinking this?
I think its developer choice . One way to select column like you mentioned .Another way is like below
$category = ProductsCategory::findOrFail($id,['category_name']);
By default findOrFail($id, $columns = ['*']) return all columns so they mentioned *
Also instead of json_encode as json,you can directly return $category
Also if you want to pass custom headers or status code then you can return like below
return response()->json($category)
Here is json method params
/**
* Create a new JSON response instance.
*
* #param mixed $data
* #param int $status
* #param array $headers
* #param int $options
* #return \Illuminate\Http\JsonResponse
*/
public function json($data = [], $status = 200, array $headers = [], $options = 0);
If you want to get full control over returned resource you should use API Resources.
To create resource file run:
php artisan make:resource ProductsCategoryResource
In resource file you can define fields to return, i.e. if you need only id and name you do this:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductCategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}
Then, set response in controller:
public function show($id)
{
$category = ProductsCategory::select('id', 'name')->findOrFail($id);
return new ProductCategoryResource($category);
}
You can also return resource collection:
public function index()
{
$categories = ProductsCategory::select('id', 'name')->get()l
return ProductCategoryResource::collection($category);
}
I think this is really elegant and organized way.

How to get an array of strings instead of object from with() function?

This is the Laravel ProductController and Products has a many-to-many relationship with Tags.
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$products = Product::with('tags')->latest()->get();
return response()->json($products);
}
On the json response, if I map the products, product.tag is returning an array of objects.
[{"name": "shirt"}, {"name": "red"}]
Is there a way to get only the name property at the with('tags') at the controller, like:
["shirt", "red"]
Also I have been trying something like this:
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$products = Product::with(['tags' => function ($query) {
$result = $query->select(['name']);
return $result->get();
}])->latest()->get();
return response()->json($products);
}
Its possible to filter the data inside tags function?
You should probably use a Resource class to return just the items required in your API. This includes being able to process child relationships. See https://laravel.com/docs/6.x/eloquent-resources#generating-resources
Or, you can do it the way you tried with select but more like;
$products = Product::with(['tags' => function ($query) {
return $query->select(['name']);
}])->latest()->get();

Display specific eloquent query in nova resource index view

I want to display the following eloquent in index view for nova resource
Post::where('frontpage', true)->get()
And perform Post model CRUD operations, How can I do that?
You can simply override indexQuery of your Nova resource, Reference
/**
* Build an "index" query for the given resource.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
return $query->where('frontpage', true);
}
Nova utilizes the concept of lenses to do this.
Create a new lens from the command line:
php artisan nova:lens IsFrontpage
Modify the query() method in app/Nova/Lenses/IsFrontpage.php:
public static function query(LensRequest $request, $query)
{
return $request->withOrdering($request->withFilters(
$query->where('frontpage', '=', true)
));
}
Attach the lens to a resource:
public function lenses(Request $request)
{
return [
new IsFrontpage()
];
}
Access the lens in the Nova admin panel: /nova/resources/posts/lens/is-frontpage
Take a closer look at the Nova documentation to also customize the URL slug (see uriKey()) and the columns (see fields()).

Laravel 5.1 using morph relation with global scope

I have global scope on my Product model and method withChildren to get data over scope. All was ok, until i tried use it with morph relation.
Code
Scope code
public function apply(Builder $builder, Model $model)
{
return $builder->whereNull('parent_id');
}
/**
* Remove the scope from the given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function remove(Builder $builder, Model $model)
{
$query = $builder->getQuery();
foreach ((array) $query->wheres as $key => $where)
{
if($where['column'] === 'parent_id')
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
}
}
}
withChildren method
public function scopeWithChildren()
{
return with(new static)->newQueryWithoutScope(new ParentScope);
}
Scope injected in model through boot method, like so
protected static function boot()
{
parent::boot();
//exclude children products from all results by default
Product::addGlobalScope(new ParentScope);
}
Problem
Relation returns null before i can implement my withChildren method. Invoice and Product have simple plymorphic relation.
$products = $invoice->products; //products is null, because of global scope
Tried
$invoice->products()->withChildren()->get() //500 error without any description
$invoice->with('products', function($q) {$e->withChildren();})->get(); //explode() expects parameter 2 to be string, object given

Resources