I want to override model events and found this example code but am not sure I understand it completely.
SOURCE:
http://driesvints.com/blog/using-laravel-4-model-events/
There is a static method with another static method in it...How does that work? Or is it setting a static property in the boot method somehow?
<?php
class Menu extends Eloquent {
protected $fillable = array('name', 'time_active_start', 'time_active_end', 'active');
public $timestamps = false;
public static $rules = array(
'name' => 'required',
'time_active_start' => 'required',
'time_active_end' => 'required'
);
public static function boot()
{
parent::boot();
static::saving(function($post)
{
});
}
}
static::saving() just calls the static method saving on itself (and parent classes if not existent in current class). So it is essentially doing the same as:
Menu::saving(function($post){
});
So it is registering a callback for the saving event within the boot function.
Laravel documentation on model events
Related
I can't figure out if Laravel is failing to boot my model trait, or is simply not seeing the user as being authed within the trait.
All of this code works perfectly fine when I test my app manually, but when I run unit tests I am getting a bunch of errors.
Here is the trait I've added to App\User and a few other models:
trait HasCompany
{
public static function bootHasCompany()
{
if (auth()->check()) {
static::creating(function ($model) {
$model->company_id = auth()->user()->company_id;
});
static::addGlobalScope('company_id', function (Builder $builder) {
$builder->where('company_id', auth()->user()->company_id);
});
}
}
public function company()
{
return $this->belongsTo('App\Company');
}
}
The purpose of this trait is to automatically add the logged in users company_id to any models they create, and restrict their access only to models they have created. I should mention that all App\User's have a company_id set in the database.
So as I've said, when I attempt to create a model when logged into my app everything works great. The trait works perfectly. However, unit tests don't seem to care for this trait. Here is an example test that does not work:
class RoleTest extends TestCase
{
use WithFaker;
public $user;
public function setup()
{
parent::setUp();
$this->user = App\User::create([
'company_id' => $faker->randomNumber(),
'name' => $this->faker->firstName,
'email' => $this->faker->email,
'password' => $this->faker->password,
]);
}
public function tearDown()
{
parent::tearDown();
$this->user->delete();
}
public function testAdd()
{
$response = $this->actingAs($this->user)->json('POST', 'roles/add', [
'_token' => csrf_token(),
'name' => $this->faker->word,
]);
$response->assertStatus(200)->assertJson(['flash' => true]);
}
}
I'm getting a 500 response instead of a 200 response because the model should automatically be obtaining the company_id from $this->user, but it is not. This is only happening for unit tests.
Here is the model code:
class Role extends Model
{
use HasCompany;
protected $fillable = ['company_id', 'name'];
}
Why aren't the unit tests booting the trait properly? It seems like actingAs doesn't work for authorization within traits, or is failing to boot it's traits entirely.
In your unit tests, the user model is first booted when the user is created by the setup function. At that time, no user is authenticated (as actingAs follows later). So, the auth()->check() only happens once when the user is created.
I think instead of checking if authenticated once (during boot), you should check if authenticated during the user creation.
Remove if(auth()->check()) from bootHasCompany and add it inside the Eloquent event closures like so:
static::creating(function ($model) {
if(auth()->check())
{
$model->company_id = auth()->user()->company_id;
}
});
static::addGlobalScope('company_id', function (Builder $builder) {
if(auth()->check())
{
$builder->where('company_id', auth()->user()->company_id);
}
});
I am trying to check in the constructor of a model if the currently authenticated user is allowed to access the given model, but I am finding that $this from the constructor's context is empty. Where are the attributes assigned to a model in Laravel and how should I go about calling a method once all of the attributes have been loaded?
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
var_dump($this); // empty model
$this->checkAccessible();
}
Cheers in advance
As stated in the other answers & comments, there are better ways to achieve the aims of the question (at least in modern Laravel). I would refer in this case to the Authorization chapter of the documentation that goes through both gates and policies.
However, to answer the specific question of how to call a method once a models attributes have been loaded - you can listen for the Eloquent retrieved event. The simplest way to do this within a class is using a closure within the class booted() method.
protected static function booted()
{
static::retrieved(function ($model) {
$model->yourMethod() //called once all attributes are loaded
});
}
You can also listen for these events in the normal way, using listeners. See the documentation for Eloquent events.
you can use controller filter to check whether user logged in or not and than you call any model function.
public function __construct(array $attributes = []){
$this->beforeFilter('auth', array('except' => 'login')); //login route
if(Auth::user()){
$user_id = Auth::user()->user_id;
$model = new Model($attributes);
//$model = User::find($user_id);
}
}
Binding Attributes to Model from constructor
Model.php
public function __construct(array $attributes = array())
{
$this->setRawAttributes($attributes, true);
parent::__construct($attributes);
}
As it was mentioned by Rory, the retrieved event is responsible for that.
Also, it could be formed in a much cleaner and OOP way with Event/Listener approach, especially if you need to write a lot of code or have few handlers.
As it described here, you can just create an event for the Model like
protected $dispatchesEvents = [
'retrieved' => UserLoaded::class,
];
You need to create this class, eloquent event accepts the model by default:
class UserLoaded
{
protected User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
Then here is described how to declare listener for this event. It should be somewhere in the EventListenerProvider like this:
protected $listen = [
UserLoaded::class => [
UserLoadedListener::class
],
];
The listener should just implement method handle() (check article) like:
public function handle(UserLoaded $event)
{
// your code
}
Another possibility is to register model Observer, as it´s described here
I am using Laravel and it's Validators.
I have the following code in my controller:
class ResellerController extends BaseController{
public function add() {
//some code before
$userValidator = new App\Services\Validators\UserCreateValidator();
//HERE I WANT TO REMOVE THE company KEY FROM THE RULES IN THE USERS CREATE VALIDATOR
$userValidator->removeRule('company');
//code execution continues
}
}
The UserCreateValidator extends a parent Validator class:
namespace App\Services\Validators;
class UserCreateValidator extends Validator {
public static $rules = array(
'firstName' => 'required',
'lastName' => 'required',
'email' => 'required|email|unique:users',
'company' => 'required'
);
}
And here is the base Validator class:
namespace App\Services\Validators;
abstract class Validator {
/**
* Validation rules
* #var array
*/
public static $rules;
//THIS CODE DOES NOT WORK IN THE CONTROLLER UP
public static function removeRule($ruleKey){
if(is_array($ruleKey))
{
foreach($ruleKey as $key)
{
if(!array_key_exists($key, static::$rules)) continue;
unset(static::$rules[$key]);
}
return true;
}
if(!array_key_exists($ruleKey, static::$rules)) //return false;
unset(static::$rules['company']);
return true;
}
}
The unsettting of the static::$rules[$key] in ResellerController does not work.
I can see in a XDEBUG session (after this line gets executed) that the static::$rules['company'] is still present in the UserCreateValidator as property.
I thought that Late Static Binding should solve this problem?
What is wrong?
The problem is solved. It was in the commented part in the:
if(!array_key_exists($ruleKey, static::$rules)) //return false;
The unsetting is working fine after I uncomment the return false.
Silly mistake :)
I'm overriding the create() Eloquent method, but when I try to call it I get Cannot make static method Illuminate\\Database\\Eloquent\\Model::create() non static in class MyModel.
I call the create() method like this:
$f = new MyModel();
$f->create([
'post_type_id' => 1,
'to_user_id' => Input::get('toUser'),
'from_user_id' => 10,
'message' => Input::get('message')
]);
And in the MyModel class I have this:
public function create($data) {
if (!Namespace\Auth::isAuthed())
throw new Exception("You can not create a post as a guest.");
parent::create($data);
}
Why doesn't this work? What should I change to make it work?
As the error says: The method Illuminate\Database\Eloquent\Model::create() is static and cannot be overridden as non-static.
So implement it as
class MyModel extends Model
{
public static function create($data)
{
// ....
}
}
and call it by MyModel::create([...]);
You may also rethink if the auth-check-logic is really part of the Model or better moving it to the Controller or Routing part.
UPDATE
This approach does not work from version 5.4.* onwards, instead follow this answer.
public static function create(array $attributes = [])
{
$model = static::query()->create($attributes);
// ...
return $model;
}
Probably because you are overriding it and in the parent class it is defined as static.
Try adding the word static in your function definition:
public static function create($data)
{
if (!Namespace\Auth::isAuthed())
throw new Exception("You can not create a post as a guest.");
return parent::create($data);
}
Of course you will also need to invoke it in a static manner:
$f = MyModel::create([
'post_type_id' => 1,
'to_user_id' => Input::get('toUser'),
'from_user_id' => 10,
'message' => Input::get('message')
]);
Having the following Models:
news.php
class News extends Aware {
public static $table = 'noticia';
public static $key = 'idnoticia';
public static $timestamps = false;
public static $rules = array(
'titulo' => 'required',
'subtitulo' => 'required',
);
public function images()
{
return $this->has_many('Image');
}
}
image.php
class Image extends Aware {
public static $timestamps = true;
public static $rules = array(
'unique_name' => 'required',
'original_name' => 'required',
'location' => 'required',
'news_id' => 'required',
);
public function news()
{
return $this->belongs_to('News');
}
}
Then in a controller I do the following:
$image = new Image(array(
'unique_name' => $fileName,
'original_name' => $file['file']['name'],
'location' => $directory.$fileName,
'news_id' => $news_id,
));
News::images()->insert($image);
I keep getting the following error message:
Non-static method News::images() should not be called statically,
assuming $this from incompatible context
Any ideas what am I doing wrong?
Setting public static function images() doesn't seem to be wanted, as after a refresh I get an error saying
$this when not in object context
Gordon said that by doing News::images()->insert($image); I'm doing a static call, but that's how saw to do it
You are missing some steps.
The Image belongs to News, but you're not referencing the News post you want to update.
You probably want to do:
$image = new Image(array(...));
$news = News::find($news_id);
$news->images()->insert($image);
More in the docs.
You're using $this in a function that is called statically. That's not possible.
$this becomes available only after you create an instance with new.
If you turn on strict mode you will get another error, namely that images is not a static function and thus shouldn't be called statically.
The problem is in News::images(), not in images()->insert($image);
$this can only be used within an object instance.
Class::method() calls a static method of the specified class.
In your case, you mixed both.
Your function definition for images is for an object instance:
public function images()
{
return $this->has_many('Image');
}
You are calling it as a static method:
News::images()->insert($image);
The News class would need to be instantiated or the images method be modified to support static calls.