I have a system that might have a lot of controllers in the future. What I wanted to achieve (and achieved) is to make some form of dynamic routes, in order not to flood routes/web.php file with tons of route groups (there might be more than 300 routes).
So I did this:
routes/web.php:
Route::get('/report/{report_name}', [ReportController::class, 'index'])->name('report');
Route::post('/report/{report}/{method_name}', [ReportController::class, 'getRpcCall'])->name('report'); // This one used for RPC call (like Vue.js/AJAX calls)
ReportController.php:
public function index(string $reportName = null)
{
if (!class_exists(self::REPORTS_NAMESPACE . $reportName . 'Controller')) {
throw new ReportClassNotFoundException($reportName);
}
$report = app(self::REPORTS_NAMESPACE . $reportName . 'Controller');
return $report->index();
}
public function getRpcCall(Request $request, string $report = null, string $methodName = null): mixed
{
if (!class_exists(self::REPORTS_RPC_NAMESPACE . $report . 'RpcController')) {
throw new ReportClassNotFoundException($report);
}
$report = app(self::REPORTS_RPC_NAMESPACE . $report . 'RpcController');
$method = $methodName;
if (!method_exists($report, $method)) {
throw new MethodNotFoundException($report, $methodName);
}
return $report->$method($request);
}
But since I am a big fan of using FormRequests (in order not to bloat Controller with stuff that shouldn't be there, according to the SRP), I created a controller for reporting, that is being called dynamically, using above methods, like this: {{ route('rpc.report', ['FuelReporting', 'updateFuelCardData']) }}. Here it is:
class SomeReportingRpcController extends Controller
{
public function updateFuelCardData(SomeModelUpdateRequest $request): Response
{
// Some stuff going on here
}
}
As you can see, I use SomeModelUpdateRequest as a form request (created via php artisan make:request SomeModelUpdateRequest) for validation purposes. But the problem is that Laravel throws an error that
Argument #1 ($request) must be of type App\Http\Requests\SomeModelUpdateRequest, Illuminate\Http\Request given, called in ReportController.php
As I thought, that even if SomeModelUpdateRequest extends FormRequest (which extends Illuminate\Http\Request class), it should be fine, since method param type hinting is not conflicting with parent and child classes, but I was wrong.
Any ideas on how to keep the "dynamic route" logic and be able to use Form Requests?
I already have a working solution to ditch the Form request and use $request->validate() in some service class, but it is not so clean and I won't be able to use $request->validated() after that, that is kind of crucial for me.
Kind regards,
Related
Working on a todo app tutorial, I don't quite understand what the argument (Request $request) means as well as other (Task $task) and so on.
I've done some research and I get answers about dependency injection and how this syntax came from Perl.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\Task;
class TasksController extends Controller
{
public function index()
{
$user = Auth::user();
return view('welcome',compact('user'));
}
public function add()
{
return view('add');
}
public function create(Request $request)
{
$task = new Task();
$task->description = $request->description;
$task->user_id = Auth::id();
$task->save();
return redirect('/');
}
public function edit(Task $task)
{
if (Auth::check() && Auth::user()->id == $task->user_id)
{
return view('edit', compact('task'));
}
else {
return redirect('/');
}
}
public function update(Request $request, Task $task)
{
if(isset($_POST['delete'])) {
$task->delete();
return redirect('/');
}
else
{
$task->description = $request->description;
$task->save();
return redirect('/');
}
}
}
First of all you need to understand what are the classes and how objects of a class created. Secondly, in object oriented programing everything we treat as an object. One single task is an object of Task class. So to edit a task we need to pass a task object.
On the other hand Request is a helper class for http, which allows you to get all the data carried out from previous http request. For example if your are submitting a form, all the form data as well as some extra data will be passed to the Laravel framework and we can access those data via Request class. You are right these all about dependency injection.
Further more, in object oriented programming world, the concept of dependency injection and objects are same. When you explore other object oriented programmings, you will find out your answer. Task is our class and we want to edit a task, so task must be an object of the class "Task" in OOP world of entity and data.
TO be clear, just remember what we used to do with procedural non object oriented programming. Simple, we used to do something like this
public function edit($task_id){
//edit record (task) based on task id in mysql DB
}
But in OOP (object oriented Programming), we play with class and object not array and fields because we have ORM (object Relational Mappings).
In very basic sense:
if you want to write a method to add two int in c programming, you do like
sum(int a, int b){
return a+b;
}
in this method you are passing two arguments to the method. but in your edit method above, you need to pass task object because your are working with data type task (simple word).
It is called type declaration in PHP also known as type hint in PHP 5. According to PHP Manual
Type declarations allow functions to require that parameters are of a
certain type at call time. If the given value is of the incorrect
type, then an error is generated: in PHP 5, this will be a recoverable
fatal error, while PHP 7 will throw a Type Error exception.
I am using Jenssegers Optimus package to obfuscate my URLS.
Currently, I am calling it in every single controller that deals with get requests. Problem is, I need to constantly encode and decode my IDs in almost all methods in my controllers.
E.g.:
use Jenssegers\Optimus\Optimus;
class ResponseController extends Controller
{
protected $optimus;
public function __construct(Optimus $opt)
{
$this->optimus = $opt;
}
public function index()
{
$labels = Label->get();
foreach ($labels as $key => $label){
$label->id = $this->optimus->encode($label->id);
$labels[$key] = $label;
}
return view('responses/index', compact('labels'));
}
public function show($id)
{
$id = $this->optimus->decode($id);
$label = Label::get($id);
}
}
I thought of creating Accessors & Mutators to always encrypt the IDs of the models I need to obfuscate in the URL. So I'd put them in a trait to reuse the code. I tried:
use Jenssegers\Optimus\Optimus;
trait EncodeId{
public function getIdAttribute($value, Optimus $optimus)
{
return $optimus->encode($value);
}
}
Then I added this trait to my model. However, Laravel would throw an error complaining about Optimus $optimus in the method definition. It said $optimus was expected to be a type of Jenssegers\Optimus\Optimus even though I am declaring it. That works for controllers just fine, but it doesn't work for models apparently. Or I shouldn't try to use a trait in this case.
Here's the actual error:
FatalThrowableError in EncodeId.php line 10:
Type error: Argument 2 passed to App\Label::getIdAttribute() must be an instance of Jenssegers\Optimus\Optimus, none given, called in /home/../vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 2734
It would be really nice if I could use Optimus obfuscation on the model level rather than calling its encode and decode functions multiple times in my controllers.
There's another package called FakeID that is meant to do that. I tried to implement it in my project but it didn't work. I am pretty sure I could handle it myself since it seems a simple task.
Get mutators (AKA accessors) are called along with a single argument. That's why you are getting expected to be a type of Jenssegers\Optimus\Optimus error as Jenssegers\Optimus\Optimus is not injected by framework when calling acessors or mutators (like controllers does).
Just read this snippet from source code (line 2632):
public function getAttributeValue($key)
{
$value = $this->getAttributeFromArray($key);
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
//...
}
And now the call to $this->mutateAttribute($key, $value); (line 2736)
protected function mutateAttribute($key, $value)
{
return $this->{'get'.Str::studly($key).'Attribute'}($value);
}
Did you understand now?
Acessor/get mutator are called along with just one argument: $value.
Solution
You could to try something like this:
public function getIdAttribute($value)
{
return app(Optimus::class)->encode($value);
}
Invoking Optimus instance from container (app()) would do the trick.
In Laravel, I have a class that I would like to make available to the service controller, make some changes to in the controller action, and then render out with a ViewComposer.
I have done this several times before without issue, but for some reason this time my usual approach is not working - clearly I'm doing something different, and I'm beginning to suspect I've fundamentally misunderstood an aspect of what I am doing.
I have a ServiceProvider with this register() method:
public function register()
{
$this->app->singleton(HelperTest::class, function ($app) {
$pb = new HelperTest();
$pb->test = "jokes on you batman";
return $pb;
});
}
Then in my controller I'm doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
$this->helper->test = "hahah";
}
And then I have a viewcomposer doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
}
public function compose(View $view)
{
$view->with('output', $this->helper->test);
}
When I call {{ $output }} in the blade view, I expect to see hahah, but instead I get jokes on you batman.
My debugging has shown that all three of these methods are definitely being called. It looks to me like the ViewComposer is for some reason instantiating its own, fresh instance of the class. What am I doing wrong?
Thanks!
Execute php artisan optimize on your console, this will generate an optimized class loader for your application, then check if you can find your class HelperTest registered in services.php inside boostrap/cache. Until HelperTest is not registered there, Laravel IoC can't resolve your class.
Thanks for watching my first question.
I have something confused.
How could I write the operations of database into database and don't write the function in every Controller?
I have considered middleware and find that must change my route register style.
my Route is this:
Route:resource('province','\\Modules\\Info\\Controllers\\P_ProvinceController');
Dose it has some awesome methods replace this?
public function Store(Request $request)
{
$params = $request->input('data');
$params['CreateID'] = Auth::user()->id;
$params['CreateName'] = Auth::user()->name;
$params['CreateTime'] = Carbon::now();
$province = P_ProvinceModel::Create($params);
$params['Pro_Is_Del'] = 1;
$log_info['table'] = $province->getTable();
$log_info['type'] = "create";
$log_info['user'] = Auth::user()->name;
$log_info['datetime'] = Carbon::now();
LogModel::create($log_info);
if($province){
return response()->json(array(
'status' => 200,
'msg' => '新增成功',
'data' => $province
));
}else
return response()->json(array(
'status' => 500,
'msg' => '保存失败',
));
}
Thanks.
Here is how I solved across model functionality
First Create a Trait that does what you want on save.
<?php
namespace App\Models\Observers;
trait CreatedByObserver
{
public static function bootCreatedByObserver(){
/** Simply means that whenever this model is creating a model do: */
static::creating(function($model){
if(auth()->check()){
$responsiblePerson = auth()->user()->first_name . " " . auth()->user()->last_name;
} else {
$responsiblePerson = "system";
}
/** You can set any model variables within */
$model->created_by = $responsiblePerson;
});
}
}
In there do all you need to do when a record is saved/created/updated/deleted
Then In all Models you want this behaviour used add the trait.
Check them out here : https://laravel.com/docs/5.2/eloquent#events
As far i understand your question, you are asking for way to make your controller an abstract type, i.e. controller just need to handle the route and view not any other things(like database,application logic etc) which is the philosophy of the laravel Framework.
To make your controller abstract (meaning of abstract as explained aboave),
first you need to understand, "What your application logic are and what your database logic are ?"
when you understand these two things then, you can easily separate your aapplication logic and databasse logic from your controller.
For example :
For keeping your Application logic you can make service folder in your root of your project also you can make folder name 'Dao' (Database access object) in the same path of service . You need to keep these folder in autoload from your composer. Just make class for service and your Dao.
And now your application follow will be,
First Route, will hit controller then controller will need to call some method in service and then service will call the respective DAO . method.
Example :
Controller/YourController.php
Class YourController extends Controller {
public function Store(Request $request,yourservice,$yourService)
{
$this->myservice = $yourservice;
$this->myservice->store('your inputs request');
return $something ;
}
}
service/yourService.php
Class yourService {
public function store($yourinputs,yourDao $mydao){
$this->mydao = $mydao;
//you can use your application logic here
return $this->mydao->create($yourinputs);
}
And now the turn is for DAO :
dao/yourdao.php
use model // use your model here .
class yourDao {
public function create($yourdata,yourmodel $model){
$this->model = $model;
return $this->model->create($yourdata);
}
}
Now, you can see the controller just save the data in database, but don't know how it is saving the data and what are the application logic.
This explanation is just a simple way of doing project to make a controller abstract. There are other various ways of doing this. For example you can see Repository Design Pattern , which also used by laravel core .
Hope this explanation will not bore anyone . :) Happy coding .
I'm trying to load a blade view with layout, but I get this error:
"Attempt to assign property of non-object"
The structure is the following:
Route:
Route::pattern('controller', '\w+');
Route::get('{controller}', function($controller) {
$controllerClass = $controller.'Controller';
App::make($controllerClass)->index();
});
Controller:
class PricesController extends BaseController {
protected $layout = 'layouts.master';
public function index()
{
$this->layout->content = View::make('prices.index');
}
}
The debug says the issue is at line $this->layout->content = View::make('prices.index');
The views are fine... I have layouts folder with master.blade.php and I also have prices folder with index.blade.php.
The content section is exists as well with #stop and the #yield is there in the layout.
In the BaseController there is the setupLayout method:
protected function setupLayout()
{
if ( ! is_null($this->layout))
{
$this->layout = View::make($this->layout);
}
}
What is the problem? Why I get that exception?
Thank you!
I know I helped you in the #laravel irc channel but there are 3 things here for any others with this problem.
This is not a good use of route files. Controller implicit routing is hard to maintain if your app gets larger. Consider using Route::resource instead if you're just trying to save a few lines of code. But I'll give you the benefit of the doubt.
You'll want to use the nest method for your layout, i.e. $this->layout->nest('content', 'prices.index');
The setupLayout() function is not being called because you are calling index() directly on the object. This is not how Laravel normally processes controllers.
I'm not going to walk through the entire routing process but if you look at vendors/laravel/framework/src/Illuminate/Routing/ControllerDisplatcher.php on line 89 you will see:
protected function call($instance, $route, $method)
{
$parameters = $route->parametersWithoutNulls();
return $instance->callAction($method, $parameters);
}
Let's look at vendors/laravel/framework/src/Illuminate/Routing/Controller.php on line 227 and you will see:
public function callAction($method, $parameters)
{
$this->setupLayout();
$response = call_user_func_array(array($this, $method), $parameters);
... irrelevant stuff omitted ...
}
These reason I show these things is show the magic Laravel is doing behind the scenes.
Basically you are skipping that magic and just calling PricesController->index() directly instead of going through the router. This is why setupLayout is never being called and you will get an exception because there is no $this->layout object yet created.