Using an ID and username for slugs (route model binding) - laravel

So by default I do not require a user to create a username on registration. So their username won't be set, and instead their UID will be their id rather than a username like so:
/**
* Get the Username
*/
public function getUsernameAttribute()
{
if($this->attributes['username']){
return $this->attributes['username'];
}
return $this->id;
}
So whenever they visit a URL that is has route model binding, it will get their information (the ID)
But now let's say I want to use a username for the route model biding as well in case they have updated and set a username I can do that like so:
/**
* Get the route key for the model.
*
* #return string
*/
public function getRouteKeyName()
{
return 'username';
}
Which works for usernames. Now my problem is, it no longer works for those without usernames (ids) and instead is looking for usernames:
where username = ?
Due to the way route model binding works, and the load order, I can not do an if statement to check if their username exists, because, well, how would laravel know to get either an ID or username.
Is there a way around this without creating a pull-request? Have any of you experienced something like this?

If you scroll past the getRouteKeyName section in the docs, you'll find a section on customising the resolution logic.
https://laravel.com/docs/5.5/routing#explicit-binding
This allows you to specify the query that is run to find a model. E.g.
// RouteServiceProvider.php
public function boot()
{
parent::boot();
Route::bind('user', function ($value) {
return User::where(function ($query) use ($value) {
return $query->where('id', $value)->orWhere('username', $value);
})
->firstOrFail();
});
}

Related

Laravel middleware with a where function

I want a middleware on my website for: People can edit their own posts but others posts. I tried this:
I get all posts that have the same post->user_id and user_id
$matches = Post::where('user_id', auth()->user()->id);
This gives back an array of posts that match the condition
Now what I want is to check if you are on a post that matches this condition, if the post->user_id and user_id do not match abort.
This is what I have, but you still can get on posts where the condition is NOT met.
if (!$matches){
abort(403);
}
return $next($request);
Abort when the criteria is not met and return the request when it is met
Instead of using middleware why not use the Policy, and since you will edit a post you can also use the Form Request. I suggest you to use Form Request
then edit the authorize() and add the condition there.
Okay lets say you are using Route Model Binding
//route
Route::put('/post/{post}', ['PostController','update']);
//controller
public function update(Post $post, UpdatePostRequest $request) {...}
You can directly check if the user is the owner inside the authorize(). Assuming that you define the relationship between the post and user
// app\Models\User.php
public function posts() : HasMany {...}
// app\Models\Post.php
public function user() : BelongsTo {...}
//request
class UpdatePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
//true if the Auth::user() own the post. otherwise false.
return $this->post->user()->is(Auth::user());
}
/**
* Get the validation rules that apply to the request.
*
* #return array<string, mixed>
*/
public function rules()
{
return [
// Your validation rules
];
}
It works if you will update the post but if just want to prevent the user from accessing the post they do not own. put this in your middleware.
if(! $request->post->user()->is(Auth::user())) {
abort(403);
}
return $next($request);
If you're inside a post, I guess you will get the Post ID inside your request as well. Something like http://127.0.0.1:5500/posts/1
Then you can get both Post and User ID. Use both values to determine whether the user has authorized the post or not.
Example:
Assume you have added the post ID URL Param as post_id
$match = Post::where('user_id', auth()->user()->id)->where('id', $request->route('post_id'));
if (!$match){
abort(403);
}
return $next($request);

Laravel show with relationships

How do you use the show function relationships? i know this works:
public show ($id) {
Model::with('relationship')->find($id);
}
but with the new format
public show(Model $model) {
}
how do you include the relationship?
i've tried
$model->with('relationship')->get();
but it changes the value from an object to an array, what would be the proper way to do this?
Lets lazy eager load that:
public show(Model $model) {
$model->load('relationship');
}
That's not a "new format". That's in fact Route model binding which is a convenient way to work as an API. https://laravel.com/docs/9.x/routing#route-model-binding
When you have a route such as
Route::get('/users/{user}', [UserController::class, 'show']);
Your controller will receive the model already fetched from database.
If you need to use additional relationships you have 2 options (let's assume that user has a profile relationship):
Eager load on controller
public show(User $user) {
$user->load('profile');
return $user;
}
Or eager load in your RouteServiceProvider.php by using explicit binding. https://laravel.com/docs/9.x/routing#explicit-binding
/**
* Define your route model bindings, pattern filters, etc.
*
* #return void
*/
public function boot()
{
Route::bind('user', function ($value) {
return User::with('profile')->findOrFail($value);
});
}
Therefore you will have the user with it's profile in your controller

laravel model, adding attribute to model

I have a user model, and I want to add (an attribute to the user model) the user's email that it was before it was updated.
before#email.com
new#email.com
Within the user model, I have this function, I can get the before email, I was wondering I can assign some fake attribute, so I can access it like: $user->beforeEmail
protected static function booted()
{
static::saved(function ($user) {
$user->beforeEmail = $user->original['email'];
});
}
$user->beforeEmail // before#email.com
The code above is not working but provided it to help illustrate what I am trying to accomplish.
You could check if the email address has changed just before storing the new email to the db. This can be accomplished by using the saving event instead of the saved event.
protected static function booted()
{
static::saving(function ($user) {
if($user->isDirty('email')){
$user->beforeEmail = $user->email
}
});
}
Note: Your code example will not save the changes automatically since the saved event is ran after executing the query. It's possible that your code works just by adding $user->save()
Are you trying to get this value in the model or in a different class? As what you have works with a few adjustments already.
protected static function boot(){
parent::boot();
static::saved(function($user){
$user->originalEmail = $user->original['email'];
}
}
You can access originalEmail if you update the model in a controller or other class, like so:
$user = User::find(1);
$user->update([
'email' => 'email#email.com'
]);
// $user, $user->originalEmail, $user->some_test_accessor all return the correct values
I've also tested with an accessor, just to verify and it still works as though the value is available in the model. I'm not sure where you're attempting to access this value, though.
public function getSomeTestAccessorAttribute(){
return $this->originalEmail;
}
Does any of this help?

Laravel Mutator to add predefined values into database

I'm new into Laravel and I'm trying to store the user's company id on a column of the products table each time a user creates a new product. The company's id it's retrieved from the user's session. I'm trying it with Laravel's Mutator:
public function setFirstNameAttribute($value) {
$this->attributes['company_id'] = session()->get('company.id');
}
But each time I create a new Product the company id stored it's null. Seems like the function it's never executing. Is there any other resource to perform actions like this?
You must use model events - this will be executed on model creation before saving. Or you can use another events depends on you logic - see docs.
class YourModel extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::creating(function (YourModel $model) {
$model->company_id = session()->get('company.id');
});
}
}
Mutators only works when you change mutating field directly:
$model->first_name = 'new_name'
And with your code - you will lost "new_name".
I noticed that the function name is incorrect, since the accessors use "studly" cased name of the column you wish to access, it may be as simple as to change
public function setFirstNameAttribute($value)
to
public function setCompanyIdAttribute($value)

Laravel authorization policy not working on Show page

I have a laravel app using Policies to assign roles and permissions, i cant seem to access the show page and im not sure what im doing wrong?
If i set return true it still shows a 403 error as well, so im unsure where im going wrong here. The index page is accessable but the show page is not?
UserPolicy
public function viewAny(User $user)
{
if ($user->isSuperAdmin() || $user->hasPermissionTo(44, 'web')) {
return true;
}
return false;
}
public function view(User $user, User $model)
{
if ($user->isSuperAdmin() || $user->hasPermissionTo(44, 'web')) {
return true;
}
return false;
}
UserController
public function __construct()
{
$this->authorizeResource(User::class, 'user');
}
public function index()
{
$page_title = 'Users';
$page_description = 'User Profiles';
$users = User::all();
return view('pages.users.users.index', compact('page_title', 'page_description', 'users'));
}
public function create()
{
//
}
public function store(Request $request)
{
//
}
public function show($id)
{
$user = User::findOrFail($id);
$user_roles = $user->getRoleNames()->toArray();
return view('pages.users.users.show', compact('user', 'user_roles'));
}
Base on Authorize Resource and Resource Controller documentation.
You should run php artisan make:policy UserPolicy --model=User. This allows the policy to navigate within the model.
When you use the authorizeResource() function you should implement your condition in the middleware like:
// For Index
Route::get('/users', [UserController::class, 'index'])->middleware('can:viewAny,user');
// For View
Route::get('/users/{user}', [UserController::class, 'view'])->middleware('can:view,user');
or you can also use one policy for both view and index on your controller.
I had an issue with authorizeResource function.
I stuck on failed auth policy error:
This action is unauthorized.
The problem was that I named controller resource/request param with different name than its model class name.
F. ex. my model class name is Acknowledge , but I named param as timelineAcknowledge
Laravel writes in its documentation that
The authorizeResource method accepts the model's class name as its first argument, and the name of the route / request parameter that will contain the model's ID as its second argument
So the second argument had to be request parameter name.
// Here request param name is timelineAcknowledge
public function show(Acknowledge $timelineAcknowledge)
{
return $timelineAcknowledge->toArray();
}
// So I used this naming here also
public function __construct()
{
$this->authorizeResource(Acknowledge::class, 'timelineAcknowledge');
}
Solution was to name request param to the same name as its model class name.
Fixed code example
// I changed param name to the same as its model name
public function show(Acknowledge $acknowledge)
{
return $acknowledge->toArray();
}
// Changed here also
public function __construct()
{
$this->authorizeResource(Acknowledge::class, 'acknowledge');
}
I looked over Laravel policy auth code and I saw that the code actually expects the name to be as the model class name, but I couldn't find it anywhere mentioned in Laravel docs.
Of course in most of the cases request param name is the same as model class name, but I had a different case.
Hope it might help for someone.

Resources