Refactor Repeated Controller Methods in Laravel - laravel

I'm a bit confused about my code in Laravel. I have a member that can subscribe to one or more lessons. I have three tables: members, lessons, and lesson_member.
I created a member form, and from this form, I can register the member in the lesson (I need to get the lesson id, and I put the record in the pivot table).
Then I have the lesson form; I can create a new lesson and put inside one or more members (in this case I need the member code).
Those two functions are nearly the same, but the parameters are different. In my solution, I created two different controllers with two different functions.
LessonController
<?php
public function addMember(Request $request)
{
$lessonMember = new LessonMember();
$lessonId = $request->session()->get('lessonId', 1);
if ((!($lessonMember::where('lesson_id', '=', $lessonId)
->where('license_member_id', '=', $request->memberId)
->exists()))) {
$lessonMember->lesson_id = $lessonId;
$lessonMember->license_member_id = $request->memberId;
$lessonMember->save();
$member = LicenseMember::find($request->memberId)->member;
return response()->json(['user_saved' => $member, 'llm' => $lessonMember, 'actualMembers' => $actualMembers]);
}
}
MemberController
<?php
public function addLesson(Request $request)
{
$lessonMember = new LessonMember();
$memberId = $request->session()->get('memberId', 1);
if ((!($lessonMember::where('lesson_id', $request->lessonId)
->where('license_member_id', $memberId)
->exists()))) {
$lessonMember->lesson_id = $request->lessonId;
$lessonMember->license_member_id = $memberId;
$lessonMember->save();
$member = LicenseMember::find($memberId)->member;
return response()->json(['user_saved' => $member, 'llm' => $lessonMember]);
}
}
I have the same problem with the removeFromLesson() method and in the updateLessonMember method, but the solution should be similar. It is for sure not DRY, and I think I have to put some code in the model (or somewhere else), but I don't know how to proceed. I want to refactor to have a clean solution. I read about traits, but I don't know if it's the right way to follow.

LessonController and MemberController extends a BaseController.
Inside that base controller, create a function called add_lesson_member(you can call it whatever you want)
Then you just need to call this function using $this->add_lesson_member() inside your LessonController or MemberController.

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 [...];
}

Route model binding with multiple wildcards

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.

Sending different data from different method on same route - laravel 8

I am trying to get order data in order tab and profile details data in profile tab.
Is it possible to achieve ???
If Yes, then please tell me how ?
If No, then please tell me, laravel is the most advance framework of PHP, why we can't send multiple data from multiple methods in same View ?
Controller
public function GetOrders()
{
$gtord = DB::table('orders')->where('email',Session::get('email'))->get();
return view('/my-account')->with('gtord',$gtord);
}
public function ProfileEdit()
{
$data = DB::table('customers')->where('email',Session::get('email'))->first();
return view('/my-account')->with('data',$data);
}
Routes
Route::get('/my-account', 'App\Http\Controllers\CustomerController#ProfileEd');
Route::get('/my-account', 'App\Http\Controllers\CustomerController#GetOrders');
Thank you in advance
You can't have multiple routes with the same 'signature', ie method and url.
If you're just showing/hiding tabs using JS, what you can do is return the view with two variables, eg:
public function AccountView()
{
$data = DB::table('customers')->where('email',Session::get('email'))->first();
$gtord = DB::table('orders')->where('email',Session::get('email'))->get();
return view('/my-account')->with(['data' => $data, 'gtord' => $gtord]);
}
And then just use one route:
Route::get('/my-account', 'App\Http\Controllers\CustomerController#AccountView');
If the two tabs are different urls, or you're using Vue or similar you would have two distinct routes with different signatures.
First, you can't have 2 same routes with the same method. It's quite logical and necessary. Otherwise, the whole routing system would collapse.
On the other hand, you can have a function in the controller, and call the other functions to collect data.
// web.php
Route::get('/my-account', 'App\Http\Controllers\CustomerController#index');
// controller
public function index()
{
$orders = $this->getOrders();
$profile = $this->getProfiles();
return view('yourView', compact(['orders', 'profile']));
}
public function getOrders()
{
//
}
public function getProfiles()
{
//
}
BTW, it's a better practice to move custom function to models, services or traits, and keep only the functions of 7 verbs in the contoller.

How to use multiple models in one controller

I have six models that I need to get in one instance in my controller. How can I do that?
I have my six models:
CommentaireCritique
CommentaireNews
CommentaireDossier
CommentaireEpisode
CommentaireSerie
CommentaireTrailer
They all have the same structure in my database, and I would like to show the latest comms on one single page. I don't know if it's possible to bind them in a single controller. I tried that, but it's not working.
public function index()
{
$comms = CommentaireCritique::all() && CommentaireNews::all()
&& CommentaireDossier::all() && CommentaireEpisode::all()
&& CommentaireSerie::all() && CommentaireTrailer::all()
->get();
return view('admin.commentaires.index', compact('comms'));
}
just after the namespace , before the class declaration
use yourAppNameSpace/modelName
There is no limits to the number of models you can instantiate in your controller as long as you declare them above correctly.I think what you need is way to merge the result of all the models if that is so, then you have to use the merge method, otherwise can you please clarify a little bit your question.
yes, you can retrieve them at one controller,
you're already halfway there, you should separate on different variable
public function index()
{
$comms = CommentaireCritique::all()
$news = CommentaireNews::all()
$dossier = CommentaireDossier::all()
$episodes = CommentaireEpisode::all()
$series = CommentaireSerie::all()
$trailers = CommentaireTrailer::all()
return view('admin.commentaires.index', compact('comms','news','dossier','episodes','series','trailers'));
}
if you want put them in one variable, you can use collection docs
All of the results from all() function returns laravel collections. So use concat() function to concatenate all those into one collection
public function index()
{
$coms = CommentaireCritique::all()
->concat(CommentaireNews::all())
->concat(CommentaireDossier::all())
->concat(CommentaireEpisode::all())
->concat(CommentaireSerie::all())
->concat(CommentaireTrailer::all());
return view('admin.commentaires.index', compact('comms'));
}

How to Override Update Method in Laravel 5.8

I'm trying to create a ticketing system that's linked to timesheets. Whenever someone updates a ticket, they have the option of submitting how much time has been spent on it, in a time_spent form object. Timesheets are polymorphically linked to many objects.
I want to create a trait, CreatesTimesheets, then apply that to relevant models so that:
Each of those models gets a timesheets() function.
It overrides the update() method of each model that it's a trait of, to check whether any time was submitted in time_spent.
It's the second bit that isn't working. My code is as below, and when I update the model (which works fine), this code doesn't fire at all, even testing it with a simple dd().
How do I fix this?
<?php
namespace App\Traits;
use App\Models\HR\Timesheet;
use Auth;
trait CreatesTimesheets
{
public function update(array $attributes = [], array $options = [])
{
dd('test');
if ($request->time_spent)
{
$timesheet = new Timesheet;
$timesheet->time_logged_in_mins = $request->time_spent;
$timesheet->appointment_id = Auth::user()->appointedJobIDToUse();
$this->timesheets()->save($timesheet);
}
parent::update($attributes, $options);
}
public function timesheets()
{
return $this->morphToMany('App\Models\HR\Timesheet', 'timesheetable');
}
}

Resources