I'm trying to simulate what Ardent package is doing. Which is validating a model right before saving.
I've created this BaseModel (According to Laravel Testing decoded book). And added this code :
class BaseModel extends Eloquent {
protected static $rules = [];
public $errors = [];
public function validate(){
$v = Validator::make($this->attributes, static::$rules);
if($v->passes()) {
return true;
}
$this->errors = $v->messages();
return false;
}
public static function boot(){
parent::boot();
static::saving(function($model){
if($model->validate() === true){
foreach ($model->attributes as $key => $value) {
if(preg_match("/[a-zA-Z]+_confirmation/", $key)){
array_splice($model->attributes, array_search($key, array_keys($model->attributes)), 1);
}
}
echo "test"; //This is for debugging if this event is fired or not
return true;
} else {
return false;
}
});
}
}
Now, this is my Post model :
class Post extends BaseModel {
public static $rules = array(
'body' => 'required',
'user_id' => 'required',
);
}
In this test i'm expecting it to fail. Instead, it passes ! , $post->save() returns true !
class PostTest extends TestCase {
public function testSavingPost(){
$post = new Post();
$this->assertFalse($post->save());
}
}
When i tried to throw an echo statement inside the saving event. It didn't appear, So i understand that my defined saving event is not invoked. I don't know why.
check out this discussion: https://github.com/laravel/framework/issues/1181
you'll probably need to re-register your events in your tests.
class PostTest extends TestCase {
public function setUp()
{
parent::setUp();
// add this to remove all event listeners
Post::flushEventListeners();
// reboot the static to reattach listeners
Post::boot();
}
public function testSavingPost(){
$post = new Post();
$this->assertFalse($post->save());
}
}
Or, better yet, you should extract the event registration functionality out of the boot function into a public static method:
class Post extends Model {
protected static boot()
{
parent::boot();
static::registerEventListeners();
}
protected static registerEventListeners()
{
static::saving(...);
static::creating(...);
...etc.
}
}
And then call Post::flushEventListeners(); Post::registerEventListeners(); in the setUp() test method.
The saving event looks fine for me. The validation fails, so $post->save() returns false. Your test passes because you expect $post->save() to be false (assertFalse), which in this case is correct.
Try these tests instead.
public function testSavingInvalidPost() {
$post = new Post();
$this->assertFalse($post->save());
}
public function testSavingValidPost() {
$post = new Post();
$post->body = 'Content';
$post->user_id = 1;
$this->assertTrue($post->save());
}
Related
Im having trouble trying to get my model observer to work.. It is working as expected for create and deleted, but not for updating. Im guessing the event never fires. The thing is all of then are being done exactly the same way. Any ideas?
Below, my observer.
class GenericObserver extends AbstractObserver {
protected $events;
public function __construct(Dispatcher $dispatcher){
$this->events = $dispatcher;
}
public function saved($model) {
dd($this->events);
$user_id = Auth::user()->usr_id;
$user_nome = Auth::user()->usr_nome;
$user_email = Auth::user()->usr_email;
dd($model);
}
public function deleted($model) {
$user_id = Auth::user()->usr_id;
$user_nome = Auth::user()->usr_nome;
$user_email = Auth::user()->usr_email;
echo($model->getTable());
dd($model->getKeyName());
}
public function updated($model) {
$user_id = Auth::user()->usr_id;
$user_nome = Auth::user()->usr_nome;
$user_email = Auth::user()->usr_email;
dd($model);
}
public function saving($model){
echo 'Saving';
}
public function deleting($model){
echo 'Deleting';
}
public function updating($model){
echo 'Updating';
}
And here, my model class
Aplicacao extends Model {
protected $table = 'gst_aplicacoes';
protected $primaryKey = 'app_id';
protected $fillable = ['app_nome', 'app_key', 'app_observacao'];
public static function table() {
$model = new static;
return $model->getTable();
}
public static function boot() {
parent::boot();
Aplicacao::observe(new GenericObserver(new Dispatcher));
}
If anyone ever faces this issue, the reason the event was not firing was because the update method, only fire its events when the update happens directly on the model, since i was using an intermediary repository to represent my model, it wasn't working.
for more details.
https://github.com/laravel/framework/issues/11777#issuecomment-170388067
Observer work on only save() method. And it's not working on function of query builder that you call by magic methods. So on update() and created() observer is not work.
This will work:
Model::find($id)->update(['column' => $value]);
Having this parent:
class BaseModel extends Eloquent {
protected static $rules = [];
public static function boot() {
parent::boot();
static::saving(function($model) {
return $model->validate(); // <- this executes
});
}
}
How can I be able to still do the same on the child model?
class Car extends BaseModel {
protected static $rules = [];
public static function boot() {
parent::boot();
static::saving(function($model) {
$model->doStuff(); // <- this doesn't execute
});
}
}
The saving() in the child only executes if I remove the saving() on the parent. I need both!
I found out the solution and it's actually pretty simple.
Here is the behavior of the *ing Eloquent events, depending on the return type:
return null or no return: the model will be saved or the next saving callback will be executed
return true: the model will be saved but the next saving callback will NOT be executed
return false: the model will not be saved and the next saving callback will not be executed
So, the solution for this problem will be simply:
class BaseModel extends Eloquent {
protected static $rules = [];
public static function boot() {
parent::boot();
static::saving(function($model) {
if(!$model->validate())
return false; // only return false if validate() fails, otherwise don't return anything
});
}
}
I have a model with this code:
<?php
use Illuminate\Database\Eloquent\SoftDeletingTrait;
class Intervention extends Eloquent {
use SoftDeletingTrait;
protected $fillable = array('start_date','stove_id','description','operation_mode','store_id','user_id','intervention_status_id','code');
public function operations()
{
return $this->hasMany('InterventionOperation');
}
public function store()
{
return $this->belongsTo('Store');
}
public function stove()
{
return $this->belongsTo('Stove');
}
public function user()
{
return $this->belongsTo('User');
}
public function statues()
{
return $this->hasMany('InterventionStatus');
}
then the boot
public static function boot()
{
parent::boot();
static::creating(function($intervention)
{
exit("creating");
});
static::created(function($intervention){
exit("created");
});
static::updating(function($intervention)
{
exit("updating");
});
}
the controller:
$intervention = new \Intervention(\Input::all());
$status = \Status::find(\Input::get('status')['id']);
$interventionStatus = new \InterventionStatus();
$interventionStatus->change_status_date = new \DateTime();
$interventionStatus->status()->associate($status);
$interventionStatus->description = "";
$user = \Auth::user();
$store = $user->store;
$intervention->store()->associate($store);
$intervention->user()->associate($user);
$intervention->request_date = new \DateTime();
$intervention->save();
...
but when save model, creating callback is not call.
I have try put exit("test") after parent::boot(); and exit is triggered.
If I put event's code in app/start/global.php it work.
I have try use the code in another model and work.
I do not know why it does not work.
Resolved:
I recreated the database and now everything works. Probably, in the various attempts to save, some relationship was skipped.
Thank you all for the help!
I think this has something to with the namespaces and registering the correct class in the event. Let's hack the source code a bit :)
In: /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
Add:
public function getAllEvents()
{
return array_keys($this->listeners);
}
And call/dump Event::getAllEvents();
Try this for both cases (boot in the model and in the global.php) and compare.
I'm using laravel (4.2) framework to develop a web application (PHP 5.4.25). I've create a repository-interface that was implemented with eloquent-repository, I use that repository inside a UserController:
# app/controllers/UsersController.php
use Gas\Storage\User\UserRepositoryInterface as User;
class UsersController extends \BaseController {
protected $user;
public function __construct(User $user) {
$this->user = $user;
}
public function store() {
$input = Input::all();
$validator = Validator::make(Input::all(), $this->user->getRoles());
if ( $validator->passes() ) {
$this->user->getUser()->username = Input::get('username');
$this->user->getUser()->password = Hash::make(Input::get('password'));
$this->user->getUser()->first_name = Input::get('first_name');
$this->user->getUser()->last_name = Input::get('last_name');
$this->user->getUser()->email = Input::get('email');
$this->user->save();
return false;
} else {
return true;
}
}
}
My Repository implementation:
namespace Gas\Storage\User;
# app/lib/Gas/Storage/User/EloquentUserRepository.php
use User;
class EloquentUserRepository implements UserRepositoryInterface {
public $_eloquentUser;
public function __construct(User $user) {
$this->_eloquentUser = $user;
}
public function all()
{
return User::all();
}
public function find($id)
{
return User::find($id);
}
public function create($input)
{
return User::create($input);
}
public function save()
{
$this->_eloquentUser->save();
}
public function getRoles()
{
return User::$rules;
}
public function getUser()
{
return $this->_eloquentUser;
}
}
I've also create a UsersControllerTest to testing the controller and all works fine, the user was added to the DB. After I mocked my UserRepositoryInterface because I don't need to test the DB insert, but I just want to test the controller
class UsersControllerTest extends TestCase {
private $mock;
public function setUp() {
parent::setUp();
}
public function tearDown() {
Mockery::close();
}
public function mock($class) {
$mock = Mockery::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
public function testStore() {
$this->mock = $this->mock('Gas\Storage\User\UserRepositoryInterface[save]');
$this->mock
->shouldReceive('save')
->once();
$data['username'] = 'xxxxxx';
$data['first_name'] = 'xxxx';
$data['last_name'] = 'xxxx';
$data['email'] = 'prova#gmail.com';
$data['password'] = 'password';
$data['password_confirmation'] = 'password';
$response = $this->call('POST', 'users', $data);
var_dump($response->getContent());
}
}
My ruote file:
Route::resource('users', 'UsersController');
When I run the test I get the following error:
Mockery\Exception\InvalidCountException : Method save() from Mockery_0_Gas_Storage_User_UserRepositoryInterface should be called
exactly 1 times but called 0 times.
Why the mocked method save has not be called?
What is wrong?
EDIT: without partial mock all works fine, now the question is: why with partial mock it doesn't work?
Thanks
Looking back at your code, it seems like you should be able to use partial mocks just by changing your mock function to something like this:
public function mock($class) {
$mock = Mockery::mock($class);
$ioc_binding = preg_replace('/\[.*\]/', '', $class);
$this->app->instance($ioc_binding, $mock);
return $mock;
}
You are telling the mock to expect the save() method, but the save() is on the Eloquent model inside the Repository, not the Repository you are mocking.
Your code is currently leaking details of the implementation of the Repository.
Instead of calling:
$this->user->getUser()->username = Input::get('username');
You need to pass an instance of the User into the Repository:
$this->user->add(User::create(Input::all());
Or you pass the array of Input into the Repository and allow the Repository to create a new User instance internally:
$this->user->add(Input::all());
You would then mock the add() method in your test:
$this->mock->shouldReceive('add')->once();
The comments about Laravel not being suited for mocking or unit testing are wrong.
I'm new to Laravel and ORM's in general. How could i hook into Eloquent to fire code before and after a save of any model? I know i can do the following for specific models but i'm looking at figuring out how to do this for every model.
class Page extends Eloquent {
public function save()
{
// before save code
parent::save();
// after save code
}
}
Using laravel models own life cycle events may solve this easy
/**
* model life cycle event listeners
*/
public static function boot(){
parent::boot();
static::creating(function ($instance){
//
});
static::created(function ($instance){
//
});
}
There's even a better way of accomplishing this! Create an observer for, lets say a model called House:
class HouseObserver {
public function saving(House $house) {
// Code before save
}
public function saved(House $house) {
// Code after save
}
}
Now register the observer with the House model by adding the line House::observe(new HouseObserver) somewhere. The line can be added in the boot method of the model:
class House extends Eloquent {
// Lots of model code
public static function boot() {
parent::boot();
self::observe(new HouseObserver);
}
}
More info can be found here.
You can create a BaseModel class that extends eloquent and then have all your models extend BaseModel. Here's an example:
abstract class Elegant extends Eloquent{
/* Save ****************************/
public function preNew() {}
public function postNew() {}
public function preSave() { return true; }
public function postSave() {}
public function save($validate=true, $preSave=null, $postSave=null)
{
$newRecord = !$this->exists;
if ($validate)
if (!$this->valid()) return false;
if($newRecord)
$this->preNew();
$before = is_null($preSave) ? $this->preSave() : $preSave($this);
// check before & valid, then pass to parent
$success = ($before) ? parent::save() : false;
if ($success)
is_null($postSave) ? $this->postSave() : $postSave($this);
if($newRecord)
$this->postNew();
return $success;
}
public function onForceSave(){}
public function forceSave($validate=true, $rules=array(), $messages=array(), $onForceSave=null)
{
if ($validate)
$this->valid($rules, $messages);
$before = is_null($onForceSave) ? $this->onForceSave() : $onForceSave($this); // execute onForceSave
return $before ? parent::save() : false; // save regardless of the result of validation
}
/** Soft Delete ****************************/
public function preSoftDelete() { return true; }
public function postSoftDelete() { }
public function softDelete($val = true, $preSoftDelete=null, $postSoftDelete=null)
{
if ($this->exists)
{
$before = is_null($preSoftDelete) ? $this->preSoftDelete() : $preSoftDelete($this);
$success = null;
if($before) {
$this->set_attribute(static::$softDelete, $val);
$success = $this->save(false);
}
else
$success = false;
if ($success)
{
is_null($postSoftDelete) ? $this->postSoftDelete() : $postSoftDelete($this);
}
return $success;
}
}
/** Hard Delete ****************************/
public function preDelete() { return true;}
public function postDelete(){}
public function delete( $preDelete=null, $postDelete=null)
{
if ($this->exists)
{
$before = is_null($preDelete) ? $this->preDelete() : $preDelete($this);
$success = ($before) ? parent::delete() : false;
if ($success)
{
is_null($postDelete) ? $this->postDelete() : $postDelete($this);
}
return $success;
}
}
}