Route model binding with multiple wildcards - laravel

How to explicitly say to route model binding to fetch only related categories? I have my web.php file as follows:
Route::get('/catalog/{category}', [CategoryController::class, 'index'])->name('category.index');
Route::get('/catalog/{category}/{subcategory}', [SubcategoryController::class, 'index'])->name('subcategory.index');
Route::get('/catalog/{category}/{subcategory}/{subsubcategory}', [SubsubcategoryController::class, 'index'])->name('subsubcategory.index');
Subsubcategory controller:
public function index(Category $category, Subcategory $subcategory, Subsubcategory $subsubcategory)
{
$subsubcategory->load('product')->loadCount('product');
$products = Product::where('subsubcategory_id', $subsubcategory->id)->orderByRaw('product_order = 0, product_order')->get();
return view('subsubcategory.index', compact('subsubcategory', 'products'));
}
And model in question:
public function subcategory()
{
return $this->belongsTo(Subcategory::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function getRouteKeyName()
{
return 'slug';
}
It works partially ok. It loads all the slugs, but the problem is, let's say I have Samsung Subsubcategory with it's parent categories like:
catalog/mobile-phones/android/samsung
Whenever I modify url from catalog/mobile-phones/android/samsung to catalog/mobile-phones/ios/samsung it works, where in fact it should not. How to handle this second scenario?
PS: it also applies if I open subcategory and change category slug. But, obviously, if upper level category does not exists, it's going to throw 404.

You may want to explore the docs a bit in regard to explicit route model binding and customizing the resolution logic to get some ideas.
https://laravel.com/docs/8.x/routing#customizing-the-resolution-logic
The following is untested and I'm making some guesses about your table structures, but I think this should give you a basic concept of how you can alter route model binding to fit your needs. The same concept could also be applied to the {subcategory} binding, but with one less relationship check.
App/Providers/RouteServiceProvider.php
public function boot()
{
// ...default code...
// add custom resolution for binding 'subsubcategory'
Route::bind('subsubcategory', function($slug, $route) {
// check to see if category exists
if ($category = Category::where('slug',$route->parameter('category'))->first()) {
// check to see if subcategory exists under category
if ($subcategory = $category->subcategories()->where('slug',$route->parameter('subcategory'))->first()) {
// check to see if subsubcategory exists under subcategory
if ($subsubcategory = $subcategory->subsubcategories()->where('slug',$slug)->first()) {
// success, proper relationship exists
return $subsubcategory;
}
}
}
// fail (404) if we get here
throw new ModelNotFoundException();
});
}
I will note, however, that this makes a number of separate database calls. There may be more efficient ways to achieve the same goal through other methods if optimization is a concern.

Related

Define fields based on resource's model attributes in Laravel Nova

I have a (relatively) basic need in Nova that I can't seem to figure out and I slowly start to feel that I'm approaching things the wrong way.
So, I've got a User, Company, Device and Transfer models and respectively resources, everything pretty default regarding the resource setup.
The schema is the following:
users: id, company_id
companies: id, type_id, name where type_id is pointing to one of three pre-populated types (manufacturer, dealer, client)
devices: id, imei
transfers: id, from_company_id, to_company_id, accepted_at
and Transfer is in a Many-to-Many with Device.
The idea behind the transfers being that Manufacturers transfer to Dealers, Dealers transfer to Clients, so it's really only a one-way thing.
Now the problem occurs at the following crucial point in the logic:
In my Transfer resource pages, I want to show different fields depending on the type of the company the currently authenticated user belongs to. Basically, if the company is:
Manufacturer, then display a DEALER column populated with the transfers' toCompany relation;
Dealer, then display a CONTRAGENT column populated with the transfers' fromCompany or toCompany relations (depending on which mathces the current auth() company)
Client, then display a DEALER column populated with the transfers' fromCompany
All of the described logic works fine with the following code (App\Nova\Transfer.php as is) UNTIL I wanted to finally display the transfer's devices on the details page:
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Http\Requests\NovaRequest;
class Transfer extends Resource
{
public static $model = \App\Models\Transfer::class;
public static $title = 'id';
public static $search = [
'id',
];
public static $with = [
'fromCompany',
'toCompany'
];
public function fields(Request $request)
{
$company = auth()->company();
if($company->hasType('manufacturer'))
{
$contragentTitle = 'Dealer';
$contragent = 'toCompany';
}
else if($company->hasType('dealer'))
{
//\Debugbar::info($this); //showing empty resource when populating the devices
$contragentTitle = 'Contragent';
$contragent = $this->fromCompany->is($company) ? 'toCompany' : 'fromCompany'; //exception here, since the resource is empty and fromCompany is null
}
else
{
$contragentTitle = 'Dealer';
$contragent = 'fromCompany';
}
$contragentCompanyField = BelongsTo::make("$contragentTitle company", $contragent, Company::class);
if($company->hasType('dealer'))
{
$contragentCompanyField->displayUsing(function ($contragentCompany) use ($contragent){
return $contragentCompany->title() . " (".($contragent == 'toCompany' ? 'Outgoing' : "Incoming").')';
});
}
return [
ID::make(__('ID'), 'id')->sortable(),
$contragentCompanyField,
BelongsToMany::make('Devices') //problematic field, when removed, everything is fine...
];
}
public static function indexQuery(NovaRequest $request, $query)
{
if(auth()->check())
{
return $query->where(function($subQuery){
return $subQuery->where('from_company_id', auth()->company()->id)->orWhere('to_company_id', auth()->company()->id);
});
}
}
public function cards(Request $request)
{
return [];
}
public function filters(Request $request)
{
return [];
}
public function lenses(Request $request)
{
return [];
}
//action is working fine (additional canRun added to avoid policy conflicts)
public function actions(Request $request)
{
return [
(new Actions\AcceptTransfer())->showOnTableRow()->canSee(function ($request) {
if ($request instanceof \Laravel\Nova\Http\Requests\ActionRequest) {
return true;
}
return $this->resource->exists
&& $this->resource->toCompany->is(auth()->company())
&& $this->resource->accepted_at === null;
})->canRun(function ($request) {
return true;
})
];
}
}
Now the strange thing that is happening is that the fields() method gets called multiple times on multiple ajax requests behind the scenes with Nova and when populating the devices relationship table, it gets called without a resource, although a call is never actually needed (as far as I can grasp the mechanics behind Nova) or at least when fetching relationships, you must still have the model information (at least the ID) somewhere to fetch by... So basically, if I'm a user of a dealer company, I can't see the devices that are being transferred (currently throwing a calling is() on null exception).
Now, this happens to be a big problem, since it hinders most of the stuff I need for my transfers, but also generally I don't like my approach so far, so... What would be the right way to achieve this multi-layer resource? Ideally I'd like to define three different transfer resource classes and somehow tell nova which one to use based on the user's company's type (since branching will most probably just grow more complex and therefore uglier as of the current aproach), but I can't figure out the way to do so.
I've also considered moving this entire logic to a separate Nova tool, but I really don't know much about them yet and whether that would be the right option... The only thing stopping me is that I still won't be able to elegantly solve the multi-layer problem and will have to write much of the otherwise useful Nova CRUD logic and views myself...
Any explanations (regarding the multiple calls of fields() and why resource is empty) or general structural recommendations to solve this case would be greatly appreciated! Many thanks in advance!
EDIT:
I was able to circumvent the error by taking advantage of viaResourceId, so instaed of $this I ended up using:
$transfer = $this->id ? $this->resource : \App\Models\Transfer::find($request->viaResourceId);
but the messy code and the unneeded calls still remain an open question. Thanks again in advance!
Here is an example of how I handled this:
public function fields(NovaRequest $request)
{
/** #var \App\Models\User $user */
$user = $this->id ? $this->resource : \App\Models\User::find($request->viaResourceId);
if ($user && $user->whatEver()) {
// display special fields in preview/detail view
return [...];
}
// display for index and if no model is found
return [...];
}

How to used Tucker-Eric/EloquentFilter Laravel

good day, I am using Tucker-Eric/EloquentFilter Laravel.
I want to filter it by relationship using Models
I want to automate it, instead of using the following:
public function users($users)
{
// dd($users);
return $this->r('users', $users);
}
public function user($user)
{
// dd($user);
return $this->r('user', $user);
}
public function owner($owner)
{
// dd($owner);
return $this->r('owner', $owner);
}
I want to make it one function that based on the relationship
so that I want to add another relationship on the model I don't need anymore to add another function.
Thanks!
We specifically stayed away from the type of implicit functionality you're looking for and opted for explicit filter methods to avoid security issues if/when new relations/properties were added to a model they wouldn't implicitly be available to filter against.
With that, what you're looking for isn't recommended because of the security concerns above but it can still exist if you implement it.
It sounds like the setup method would be the best place to implement it since it would be called first every time ->filter() is called.
public function setup()
{
foreach($this->input() as $key => $val) {
if($this->getModel()->$key() instanceof \Illuminate\Database\Eloquent\Relations\Relation) {
// Your logic here
}
}
}

A readable way to pull in a hasMany collection into a result object in the controller

Inside a resource controller I have the following show function.
public function show(Invite $invite)
{
return response($invite->jsonSerialize(), Response::HTTP_CREATED);
}
The invite model has many guests and the guest model belongs to an invite. Standard resource routes. When I query the url, I get a response like:
{
id":17,
"user_id":2,
"event_id":1,
"name":"Fred Neumann +1",
"called":0,
"emailed":0,
"invited":1,
"max_guests":2,
"created_at":"2019-05-18 21:31:07",
"updated_at":"2019-05-18 21:31:07",
"deleted_at":null
}
Now I would also like to return the guests along with the invite info. I can achieve this by modifying the show function as such:
public function show(Invite $invite)
{
// Don't remove this line:
$invite->guests = $invite->guests;
return response($invite->jsonSerialize(), Response::HTTP_CREATED);
}
This works fine but it's not obvious that it actually does anything. I could easily see myself removing it later by accident and breaking the API, hence the comment. Is there a more readable alternative?
Load the guests relationship with lazy eager loading:
public function show(Invite $invite)
{
return response($invite->load('guests')->jsonSerialize(), Response::HTTP_CREATED);
}

Laravel Backpack - getting current record from crud controller

In my crud controller I am trying to get the name of the person who is currently being edited.
so
http://192.168.10.10/admin/people/93/edit
In the people crud controller
public function setup() {
dd(\App\Models\People::get()->first()->name)
}
This returns the first person not the person currently being edited.
How do I return the current person (with an id of 93 in this example)
Ok, So since you use backpack look into CrudController to see how the method looks:
public function edit($id)
{
$this->crud->hasAccessOrFail('update');
$this->data['entry'] = $this->crud->getEntry($id);
$this->data['crud'] = $this->crud;
$this->data['fields'] = $this->crud->getUpdateFields($id);
$this->data['id'] = $id;
return view('crud::edit', $this->data);
}
So now you can overwrite the edit function and change whatever you want. You can even create a custom edit page if you so wish.
Setup on the other hand is usually used to add things like
$this->crud->addClause(...);
Or you can even get the entire constructor and put it in the setup method because setup call looks like this:
public function __construct()
{
// call the setup function inside this closure to also have the request there
// this way, developers can use things stored in session (auth variables, etc)
$this->middleware(function ($request, $next) {
$this->setup();
return $next($request);
});
}
So you could do something like \Auth::user()->id;
Also it's normal to work like this. If you only use pure laravel you will only have access to the current id in the routes that you set accordingly.
Rahman said about find($id) method. If you want to abort 404 exception just use method findOrFail($id). In my opinion it's better way, because find($id)->name can throw
"Trying to get property of non-object error ..."
findOrFail($id) first fetch user with specified ID. If doesn't exists just throw 404, not 500.
The best answer is:
public function edit($id)
{
return \App\Models\People::findOrFail($id);
}
Good luck.
you need person against id, try below
public function setup($id) {
dd(\App\Models\People::find($id)->name);
}

Clean way of checking if resource exists and if user can edit it

I have two very common steps that I have to repeat in almost every CRUD method in my Controllers. I have my Users split into 2 groups ( Users, Administrators ). Now Users can edit, update and delete only their own entries while admins can do all the CRUD operations.
The second piece of code I find my self writing every time is checking if the resource exist which is repetitive and somewhat annoying.
Here is what I attempted:
<?php
class BaseController extends Controller
{
// Received Eloquent model each model has user_id field
public function authorize($resource)
{
// Check if currently logged in users id matches user_id
// value of the resource
if($resource->user_id !== CurrentUser::getUser()->id)
{
// Users id does not match with resource user_id check if user is admin
if(!CurrentUser::getGroup() === 'Admin')
{
// The id's do not match and user is not admin redirect him back to root
Session::flash('error', 'You cannot edit this resource');
return Redirect::to('/');
}
}
}
}
class CarController extends BaseController
{
public function edit($id)
{
// Attempt to find the resource
$car = Car::find($id);
// Check if found
if(!$car)
{
// Resource was not found
Session::flash('error', 'Resource was not found');
return Redirect::to('/cars');
}
// First check if user is allowed to edit the resource
// this however does not work because returned Redirect is simply ignored I would
// have to return boolean and then check it but...
$this->authorize($car);
// ... rest of the code
}
}
This would not be a problem if I had 3-4 methods but I have some 6-10 methods and as you can see this part takes some 20 lines of code add that 6-10 times not to mention it's repetitive to the point where it get's annoying.
I have tried to solve the problem using a filter but the problem is that I can pass the id to the filter but not get it to work in a way that I would pass the model as well.
There has to be a cleaner way to implement all this. I'm somewhat happy with authorize function/process but it would be awesome not having to call is every time possibly having some filter and each controller would define global variable/array of methods that require authorization.
As for checking if record was found I was hoping maybe a filter could be done to catch all RecordNotFound exceptions and redirect back to controllers index route with a message.
You can use findOrFail() and catch the exception in your BaseController and you also have two options:
try
{
$post = $this->post->findOrFail($id);
return View::make('posts.show', compact('post'));
}
catch(ModelNotFoundException $e)
{
return Redirect::route('posts.index');
}
Or
$post = $this->post->findOrFail($id);
return View::make('posts.show', compact('post'));
And a exception handler returning back to your form with the input:
App::error(function(ModelNotFoundException $exception)
{
return Redirect::back()->withErrors()->withInput();
});
Note that those are just examples, not took from your code.

Resources