we are developing an application based on Laravel Spark. as part of this we want to tie resources to a specfic team.
I know that we can add a global scope such as:
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class TeamScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->where('team_id', '=',Auth()->user()->currentTeam->id );
}
}
but according to the docs we have to add that to each model that we want to restrict like so:
protected static function boot()
{
parent::boot();
static::addGlobalScope(new TeamScope);
}
my issue with this is that it will be possible to create future models and forget to apply this code. Which could give us a security hole?
is there any way to enforce the scope across the board?
I am not sure if there's a way to globally add the Scope.
In my particular application, we have had to add more responsiblities to our Models. So we created a BaseModel class that extends Laravel's Illuminate\Database\Eloquent\Model.
All new Models then extends the BaseModel instead of Laravel's one.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BaseModel extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new TeamScope);
}
}
For example:
<?php
namespace App;
class Attribute extends BaseModel
{
}
You could also have a trait that you can just use to add this scope to your Model. For example:
trait HasTeamScope
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new TeamScope);
}
}
}
... and then you can easily re-use that in your Model.
For example:
<?php
namespace App;
class Attribute extends BaseModel
{
use HasTeamScope;
}
Now, based on your question, you might also forget to extend the BaseModel in the first instance or add the Trait in the second one whenever you create a new model.
To solve this, you could easily create a new command to produce models that will use your own stub (which extends the BaseModel or adds the trait whenever you create a new model)
You could create your own base model with the desired global scope that future models would extend.
You should create trait with boot function. Trait named BelongsToTeam.
And in all models add only: use BelongsToTeam;
Related
I'm using Laratrust to manage these roles:
Patient
Doctor
I've a class called User which is the main entity, then I have a specific class for each role: Patient and Doctor.
Problem
To retrieve a list of users with the role of doctor I have to write:
User::whereRoleIs('doctor')->get();
The problem's that I have defined some relationship within the class Doctor, eg:
<?php
namespace App\Models;
use App\Models\Boilerplate\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Doctor extends User
{
use HasFactory;
protected $table = 'users';
public function patients()
{
return $this->belongsToMany(Patient::class, 'invites', 'doctor_id', 'user_id');
}
}
and I cannot access to the relationship patients from the User model.
Is there a way to call Doctor:with('patients')->get() and return automatically the users which have the role of Doctor?
So if I type: Doctor:all() the result must be equal to User::whereRoleIs('doctor')->get()
How can I do this?
Splitting data like this by model isn't really the intended use for models. Models in Laravel contain data on a per-table basis. In order to achieve what you want I would either make a DocterService that has methods in it to retrieve docter users by calling User::whereRoleIs('doctor') or just use this method straight away.
If you really want to use the model though you can use scopes. (https://laravel.com/docs/9.x/eloquent#query-scopes) Create a new scope that includes the whereRoleIs('doctor') method
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class DoctorScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->whereRoleIs('doctor');
}
}
and apply it to the model by adding the following to the model:
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::addGlobalScope(new DoctorScope);
}
Trying this for hours now and I don't see the error. I have a model 'User' and a model 'Round'. I want to define a n:m-relation with a model 'Flight' as pivot model.
User.php
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class User extends Authenticatable implements MustVerifyEmail, HasMedia
{
use Notifiable;
use InteractsWithMedia;
/*
.....
*/
public function rounds() {
return $this->belongsToMany(Round::class)->using(Flight::class);
}
}
Round.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Round extends Model
{
/*
.....
*/
public function users() {
return $this->belongsToMany(User::class)->using(Flight::class);
}
}
Flight.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class Flight extends Pivot
{
public $incrementing = true;
/*
.....
*/
}
I made several migrations and seeder.
RelationSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Round;
class RelationSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
$round = Round::find(1);
$round->users()->sync([1]);
}
}
When running artisan migrate:refresh --seed all tables are created as expected, but the following error occurs on seeding
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'tour.round_user' doesn't exist (SQL: select * from round_user where round_id = 1)
Obviously Laravel is looking for a standard named pivot-table and not for the desired flights-table.
I am using Laravel 8 in a Docker-Container with Sail.
Where is my mistake?
I found my mistake. With using the pivot model, the name conventions for the intermediate table stay valid. It works with
public function rounds() {
return $this->belongsToMany(Round::class, 'flights')->using(Flight::class);
}
and same thing in the inverse definition
public function users() {
return $this->belongsToMany(User::class, 'flights')->using(Flight::class);
}
Thank you everybody who is helping here - I solved 1000s of problems reading your posts!
I want to add a global scope to my project. and use it in some models.
so I create this code: (in app/scopes folder)
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class GameStoreScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* #param Builder $builder
* #param Model $model
* #return void
*/
public function apply(Builder $builder, Model $model)
{
//$storeId = \request()->header('Store');
//dd("asdad");
$builder->where('game_store_id', '=', 1);
}
}
And use it to my model like this:
<?php
namespace App;
use App\Scopes\GameStoreScope;
use Illuminate\Database\Eloquent\Model;
class Player extends Model
{
protected $guarded = [];
protected static function boot()
{
parent::boot();
static::addGlobalScope(new GameStoreScope);
}
but after run my project. always get this FatalError : Class 'App\Scopes\GameStoreScope' not found
Fix namespace ref link https://laravel.com/docs/8.x/eloquent#query-scopes
namespace App\Scopes;
use App\Scopes\GameStoreScope;
use Illuminate\Database\Eloquent\Model;
then run composer dump-autoload
I have a global scope that is used for a few models. Here is the scope class:
<?php
namespace App\Scopes;
use App\Models\UserMeta;
use Auth;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class WorkplaceScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$user_active_workplace = UserMeta::where(['user_id' => Auth::id(), 'meta_key' => 'active_workplace'])->first();
$builder->where('workplace_id', $user_active_workplace->meta_value);
}
}
I am assigning the scope by overriding the model's boot method:
protected static function boot()
{
parent::boot();
static::addGlobalScope(new WorkplaceScope);
}
The problem is that I make a query inside the scope and that query is executed a lot of times for no reason. I was thinking of using primitive binding inside a service provider, but I can't figure out how to do it properly. I tried adding this inside the AppServiceProvider:
$this->app->when('App\Scopes\WorkplaceScope')
->needs('$user_active_workplace_meta_value')
->give(1);
but I don't know how to hint the service provider that the WorkplaceScope expects a $user_active_workplace_meta_value variable. Any idea how can I do this?
I added External Scope for User Model. Simply created Scope, with the name DeveloperScope
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Auth;
class DeveloperScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
if(Auth::user()->id != 1){
$builder->where('id', '<>', 1);
}
}
}
Then, I called this scope for User model.
User Model
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Scopes\DeveloperScope;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use SoftDeletes;
use Authenticatable, CanResetPassword;
protected $table = 'users';
protected $fillable = ['first_name', 'last_name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
protected $dates = ['deleted_at'];
protected static function boot()
{
parent::boot();
static::addGlobalScope(new DeveloperScope);
}
}
It works well, when I don't use Auth::class inside DeveloperScope class. The reason is that, I just want to hide the main user for another users for all Eloquent methods. Of course, I can use session instead of Auth and retrieve user id. But it is still interesting for me, why browser gives an error: ERR_EMPTY_RESPONSE while using Auth::class ?
The method what you are looking for is exactly Auth::hasUser(). The method was added in Laravel 5.6 through my PR. (So you need to upgrade Laravel first)
[5.6] Add ability to determine if the current user is ALREADY authenticated without triggering side effects by mpyw · Pull Request #24518 · laravel/framework
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class DeveloperScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
if (!Auth::hasUser() || Auth::user()->id != 1) {
$builder->where('id', '<>', 1);
}
}
}
Just call Auth::hasUser() to prevent Auth::user() from causing side effects.
Try injecting the Illuminate\Auth\Guard class and then call the user method on it. e.g.:
$this->auth->user();
You should be fine with just type-hinting it since Laravel will automatically new up an instance for you.
For more info check out the API: https://laravel.com/api/5.2/Illuminate/Auth/Guard.html