I have routes with user binding like
Route::get('users/{user}/posts', [PostController::class, 'index']);
Route::get('users/{user}/comments', [CommentController::class, 'index']);
So I can use /users/1/posts, /users/5/posts etc, and in controller it's automatically available thanks to model binding
public function index(User $user)
{
dd($user);
}
But for current logged user I want to make possible to also use /me/ instead ID, like /users/me/posts
Is there a way to make it without defining separate controller methods where I would have to find user manually, and without duplicating all routes? So is it possible to "extend" default Laravel model binding globally?
I believe using a fixed route parameter like this is the most sustainable solution, especially if this is a shared code base. It will involve some repeated code, but makes it immediately clear what routes are available. And you can predefine the grouping callback to avoid repeating the route definitions.
$routeGroup = function ($r) {
$r->get('posts', [PostController::class, 'index']);
$r->get('comments', [CommentController::class, 'index']);
};
Route::prefix('users/{user}')->group($routeGroup);
Route::prefix('users/me')->group($routeGroup);
And then make the parameter optional in your controller method.
public function index(User $user = null)
{
$user = $user ?? Auth::user();
dd($user);
}
Another possibility is overriding the resolveRouteBinding method on your model. If you go with this method (or Patricus' solution) I'd suggest leaving comments in the route file explaining what you've done.
class User extends Model {
public function resolveRouteBinding($value, $field = null): ?self
{
return $value === 'me'
? Auth::user()
: parent::resolveRouteBinding($value, $field);
}
}
Sure, you just need to use explicit route model binding instead of the default implicit binding. No need to change your routes or your controllers.
In your RouteServiceProvider::boot() method, you can add the following binding for the user parameter:
// use App\Models\User;
// use Illuminate\Support\Facades\Auth;
// use Illuminate\Support\Facades\Route;
public function boot()
{
Route::bind('user', function ($value) {
if ($value == 'me') {
return Auth::user();
}
return User::findOrFail($value);
});
}
Now all your routes with the {user} parameter defined will use that function to bind the User model in the route.
You may want to update the function to be able to handle case sensitivity, or handle when the route is accessed as a guest, but that's up to your implementation details.
Related
I'm building a Laravel-app and I have a route where I need to include a third-party script/iframe. I want to protect that route with a simple access code without setting up the laravel-authentication.
Is that possible? If so, how can I achieve that?
All solutions I give below suggest you are trying to access your route with code=X URI/GET parameter.
Simple Route
You can simply check for the given code to be correct in each route's method, and redirect somewhere if that's not the case.
web.php
Route::get('yourRouteUri', 'YourController#yourAction');
YourController.php
use Request;
class YourController extends Controller {
public function yourAction(Request $request) {
if ($request->code != '1234') {
return route('route-to-redirect-to')->redirect();
}
return view('your.view');
}
}
Route with middleware
Or you can use middlewares for avoiding to repeat the condition-block in each route if you have many of them concerned by your checking.
app/Http/Middleware/CheckAccessCode.php
namespace App\Http\Middleware;
use Request;
use Closure;
class CheckAccessCode
{
public function handle(Request $request, Closure $next)
{
if ($request->code != '1234') {
return route('route-to-redirect-to')->redirect();
}
return $next($request);
}
}
app/Http/Kernel.php
// Within App\Http\Kernel Class...
protected $routeMiddleware = [
// Other middlewares...
'withAccessCode' => \App\Http\Middleware\CheckAccessCode::class,
];
web.php
Route::get('yourRouteUri', 'YourController#yourAction')->middleware('withAccessCode');
You can create your own middleware.
Register the middleware in the $routesMiddleware of your app/Http/Kernel.php file.
Then use it like this:
Route::get('script/iframe', 'YourController#index')->middleware('your_middleware');
-- EDIT
You can access the route like this:
yoururl.com/script/iframe?code=200
Then in the middleware handle method:
if ($request->code !== 200) {
// you don't have access redirect to somewhere else
}
// you have access, so serve the requested page.
return $next($request);
I'm doing an existence check within a middleware, by checking a route-parameter.
If the check succeeds, I'm attaching it's model to the request to make it available throughout the rest of the request-cycle, application.
// App\Http\Middleware\CheckForExistence.php:
...
public function handle($request, Closure $next)
{
// some checks...
// success
$request->attributes->add([
'company' => $someModel
]);
}
I now have a controller which 'needs' this information in a couple of methods. So my thought was to add it to the construct of the controller and add it as a protected var in the whole controller:
// App\Http\Controllers\MyController.php
<?php
use Illuminate\Http\Request;
class MyController extends Controller
{
protected $company;
public function __construct(Request $request)
{
$this->company = $request->attributes->get('company');
}
public function index()
{
dd($this->company); // returns null
}
}
This controllers index() returns null instead of the give model.
If I change the index() method to:
public function index(Request $request)
{
return $request->attributes->get('company');
}
This returns the model; as expected.
Why is this happening? It looks like the middleware is not run when the controller is constructed.... Is there a way to circumvent it?
Or am I missing the obvious here.....
I could off course repeat myself in each method; but that is not very DRY ;)
You can't access the session or authenticated user in your controller's constructor because the middleware has not run yet, So you can do it like this :
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->company = $request->attributes->get('company');
return $next($request);
});
}
For reasons currently unclear to me, the controller object is constructed before the request changes are reflected in the request object. In short the request is not considered properly constructed when a controller is constructed. This post seems to imply that.
There's two ways to work around this (if for a second we ignore what you're trying to do).
Use request dependency injection
public function index(Request $request)
{
$compary = $request->attributes->get('company');
}
This is not really WET because you're just swapping $this->company with $request->attributes->get('company') it's just a refactor. You should be injecting the request in the controller action anyway and if you don't want to do that you can use the request() helper.
Use a callback middleware in the constructor (Maraboc's answer explains how)
Now if you want a more case specific solution though you can use case specific dependency injection:
If you need to bind a model to a specific route parameter you can use route model binding and add the following in your RouteServiceProvider (or any provider).
Route::bind("companyAsARouteVarName", function () {
// this is why more details in the question are invaluable. I don't know if this is the right way for you.
//checks
// success
return $someModel;
});
Then you will register your route as:
Route::get("/something/{companyAsARouteVarName}", "SomeController#index");
and your controller will be:
public function index(Company $companyAsARouteVarName) {
//Magic
}
Controller constructor will be initialized before middleware execution.
You can get data from Injected $request object in controller functions.
I'm providing a HasTranslation-Trait for any of my eloquent models. All models using this trait will receive a one-to-many-relation like this (where you can see my basic Model to ModelLanguages relations):
public function languages()
{
return $this->hasMany(get_class($this).'Lang', 'master_id', 'id');
}
What I want to do is:
Always eager load a "hasOne"-relationship with the translation of current user's language. So whenever the user is logged in, my models should have something like $model->userLanguage being eager loaded and is of type ModelLang.
This looks like this and works great:
public function userLanguage()
{
$user = \Auth::user();
if (!$user)
{
throw new \Exception(__CLASS__.': userLanguage not available because there is no session');
}
return $this->hasOne(get_class($this).'Lang', 'master_id', 'id')->where('language_id', $user->language_id);
}
I'm struggling with the possibility to automatically (eager) load this relations for all models by just including this trait.
What I've tried so far
Use a constructor within the trait: Can work, but no good idea, because this can collide with other trait's contructor. Basically I'd confirm the statement: Do not use the constructor in any trait.
Use your boot-Trait method (bootHasTranslation) but there I do not have the concrete object to call load or with method on. I did not find any hook into the instantiated eloquent model where I want to add my relation to eager load
Any ideas? Is there something obvious I've overlooked here?
You can create a middleware for the logged in user language.
public function handle($request, Closure $next)
{
if (!Auth::check()) {
return redirect('/')->with('Success','You have successfully logged out');
}
$user = \Auth::user();
$language = $user->languages()->where('language_id', $user->language_id);
$request->attributes->add([
'user_language' => $language
]);
$next($request);
}
You can get this data after middleware like
$request->attributes->get('user_language');
I'm little confused with the database queries in the laravel, where do we have to write our queries: in controllers, models or routes ?
I've been through many tutorials and I see so much difference. Just creating confusion.
Kindly explain somebody
It depends on different factors, but in short you can write them both in Model, Controllers or Repositories
If you're writing a controller action and you need a query that you'll use only once, it's perfectly fine to write the query directly in the controller (or even in the route's closure ).
For example, if you want to get all users of type admin:
$admins = User::where('type', 'admin')->get();
Now, suppose you need to get the admins in more than one controller method; instead of rewriting the same query, you can create a Repository class to wrap the access to the users' Model and write the query inside the Repository:
class UserRepository
{
public function getAllAdmins()
{
return User::where('type', 'admin')->get();
}
}
Now in your controllers you can inject the Repository and use the same method of the Repository to get the admin users: this will keep your code DRY as you don't have to repeat the same query among the controllers' actions
Controller
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
//controller action
public function index()
{
$admins = $this->userRepo->getAllAdmins();
}
Finally, let's suppose you need a query to count the number of the admin users. You could write this query in the UserRepository:
public function getAdminNum()
{
return User::where('type', 'admin')->count();
}
And it would be ok, but we can note that the User::where('type', 'admin') fragment of the query is shared with the query in getAllAdmins So we can improve this by using query scopes :
User Model
public function scopeAdmins($query)
{
return $query->where('type', 'admin');
}
by this, in the UserRepository methods we can rewrite our previous queries as:
public function getAllAdmins()
{
return User::admins()->get();
}
public function getAdminNum()
{
return User::admins()->count();
}
And i've just showed you a case in which a query would be writed inside a Model
You do not write any query in Model. Model is just for mapping the class that you are going to use for a table like User Model will be mapped to users (plural of model name).
You do not write queries in the routes closures like this
Route::get('/', ['as' => 'home', function(){
$totalProblems = Problem::count();
$solvedProblems = Problem::where('solved', 1)->get()->count();
$unsolvedProblems = Problem::where('solved', 0)->get()->count();
return view('Pages.index', ['totalProblems' => $totalProblems, 'solvedProblems' => $solvedProblems, 'unsolvedProblems' => $unsolvedProblems]);
}]);
This is considered as bad practice, its just for testing purposes.
You always write your queries in the controller method associated with your route like this
Route::get('test', 'HomeController#test');
and in your HomeController
<?php namespace App\Http\Controllers;
use App\Problem;
class HomeController extends Controller {
public function test(){
$user = User::all();
return view('Pages.test')->withUser($user); //or
return view('Pages.test')->with('user' , $user);
}
}
I have Laravel's built in auth working. Users can register and login.
What I'd like to do is set the $id parameter for the UserController's show() method with the value from Auth::user()->id;
My thoughts behind this is, to not have to use id's in the routes.
I'm pretty new to OOP, and php in general, so I'm not sure how to tackle this.
Thanks in advance for any tips or help!
1st method
You can have a route with an optional user id:
Route::get('user/show/{id?}', 'UsersController#show')
If the show method of your controller doesn't get an id, it can use Auth::user() instead:
class UsersController extends BaseController {
public function show($id = null)
{
return View::make('userProfile')->with('user', $this->getCurrentUser($id));
}
public function getCurrentUser($id)
{
$this->currentUser = $id ? User::find($id) : Auth::user();
}
}
Then in your view you will be able to always
{{ $user->name }}
2nd method
You could also have a BaseController which does that automatically for you using View::share():
class BaseController extends Controller {
public function __construct()
{
parent::__construct();
$this->shareUser();
}
public function shareUser($id = null)
{
View::share('user', $id ? User::find($id) : Auth::user());
}
}
Then in your controller you don't need to pass the user:
class UsersController extends BaseController {
public function show()
{
return View::make('userProfile');
}
public function thisIsAMethodOverridingIt($id)
{
$this->shareUser($id);
return View::make('userProfile');
}
}
It would even better to have this provided by a Service, but you'll have to read about Service Providers and Facades to make it happen.
And you are still able to do that:
{{ $user->name }}
Because View::share() will send that variable to all your views.
3rd method
If you just need your user everywhere, use a global View::composer():
View::composer('*', function($view)
{
$view->with('currentUserName', Auth::check() ? Auth::user()->firstname : '');
});
You can put this in your routes.php or, better create a file for this purpose, something like app/composers.php and load it in your app/start/global.php:
require app_path().'/composers.php';
As always, you can use it in your view, this way:
{{ $user->currentUserName }}
If you just need it for a couple of views, you can
View::composer(array('profile','dashboard'), function($view)
{
$view->with('currentUserName', Auth::check() ? Auth::user()->firstname : '');
});
This is actually a recent problem that I encountered while working on an API.
The way I handled it, was to introduce /me endpoints, so for example, you'd have:
Route::group(['before' => 'auth'], function() {
Route::get('/user/show/{id}', 'UsersController#show');
Route::get('/me', 'UsersController#show');
}
You'll notice that both routes point to the same code, but have different addresses. This means that you can simplify requests by using the /me convention, without having to duplicate code. You'll also notice that I enclosed these in a group which applies the auth filter. This basically just makes sure that the user is authed, and while it may not be required for the first one, it'd definitely be required for the second.
Then your show method would look like this:
public function show($id = false)
{
$user = $this->getUserOrMe($id);
return View::make('myview', ['user' => $user]);
}
This would require the below function:
private function getUserOrme($id)
{
return $id !== false ? User::find($id) : Auth::user();
}
A controllers methods should be accessed independently of each other, meaning that once the User object is returned, all the relevant code for the current request has access to it. Storing the User object in a class property would just over engineering.
Hope that helps.