mocking hasMany relation in laravel 5.1 unit test - php

For a unit test in laravel 5.1 I am trying to test a cascading delete function of the Client model, which, with the recursive flag set, should also delete all users associated with the client.
I want to use a mock user adn test only wether the delete function on the user is called, so I wont have to use the database, and to apply the same principle to other tests in the future.
at the moment the test fails because I cannot find a way to make the client model retreive the associated user without firing a query.
I think I need to mock the hasMany relation defining function of the client, but i have not found a way.
the client model:
class Client extends Model
{
protected $table = 'clients';
protected $fillable = [];
public function casDelete($recursive = false){
if($recursive) {
$users = $this->users()->get();
foreach($users as $user) {
$user->casDelete($recursive);
}
}
$this->delete();
}
public function users(){
return $this->hasMany('App\User');
}
}
the user model:
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['name', 'email', 'password', 'client_id'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['password', 'remember_token'];
public function casDelete($recursive = false){
$this->delete();
}
public function client(){
return $this->belongsTo('App\Client');
}
}
the test:
class ClientModelTest extends TestCase
{
use DatabaseTransactions;
function testCasDelete(){
$client = factory(Client::class)->create();
$user = factory(User::class)->make(['client_id' => $client->id]);
$observer = $this->getMock('user');
$observer->expects($this->once())->method('casDelete');
$client->casDelete(true);
}
}

When you are using DatabaseTransactions, this mean you want to persist the data in the database. And when you are using create() from the factory still you are using the database, so either you should not use the database at all or if you want you to use the database then you can simply solve the problem. but what I can suggest is this solution, which I'm not using the database init.
$user = \Mockery::mock();
$user->shouldReceive('casDelete')->andReturnNull();
$queryMock = \Mockery::mock();
$queryMock->shouldReceive('get')->andReturn([$user]);
$clientMock = \Mockery::mock(Client::class)->makePartial();
$clientMock->shouldReceive('users')->andreturn($queryMock);
$clientMock->casDelete(true);
This way you can be sure that you have called casDelete on each user model.
this is a very simple test case, you can extend it in the way you like base on what you want to achieve.

Related

Laravel delete linked model's data by user_id in booted function of User model

I've read part of the Laravel docs for events and closures for models, I've got various models in my project whereby a user may have data linked to them in another table by a user_id column, the user_id column that I have in my various tables is structured as an unsigned integer (I'm aware I could've gone with a foreignId column by kind of a legacy approach here)
It looks like:
$table->integer('user_id')->unsigned()->nullable()->index();
I'd like to delete user data by their ID within these other tables and rather than creating a delete function and grabbing each model I want to delete data against, I've utilised the closure booted function and what I believe to be an event to listen and delete related model data, but I experience an error when trying to delete my user account, other data in other tables isn't deleted, the error I get is:
Call to undefined method App\Models\User::releationship()
My user model looks like:
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject, MustVerifyEmail
{
use Notifiable, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'first_name', 'last_name', 'email', 'password'
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token'
];
/**
* The attributes that should be cast to native types.
*
* #var array
*/
protected $casts = [
'email_verified_at' => 'datetime'
];
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* #return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* #return array
*/
public function getJWTCustomClaims()
{
return [];
}
/**
* Route notifications for the Slack channel.
*
* #param \Illuminate\Notifications\Notification $notification
* #return string
*/
public function routeNotificationForSlack($notification)
{
$url = $this->slack_webhook;
$webhook = (isset($url) && !empty($url)) ? $url : null;
return $webhook;
}
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::deleted(function ($model) {
$model->relationship()->delete();
});
}
}
And an example (of many) model I have, UptimeChecks looks like:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UptimeChecks extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'uptime_checks';
/**
* Join user table
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
All is then kicked off by a deleteAccount function in my API, which is deleting the user's account, but isn't deleting data in other tables. What am I missing and how could I do a check to make sure other data is deleted before confirming to the user that their account and linked data is gone?
/**
* Delete account
*
* #return Response
*/
public function deleteAccount(Request $request)
{
// attempt to delete account
try {
$user = User::findOrFail(Auth::id());
$user->delete();
// everything went okay!
return response()->json(['success' => true, 'message' => 'Your account has been deleted'], 200);
} catch (Exception $e) {
// catch the error
return response()->json(['success' => false, 'message' => 'We was unable to delete your account at this time'], 422);
}
}
In Laravel, when doing $model->relationship()->delete(); you will need to have the relationship defined and relationship() seems like it is copy pasted code snippet. Simply add the relationship to your User model.
class User extends Authenticatable implements JWTSubject, MustVerifyEmail
{
...
public function uptimeChecks() {
return $this->hasMany(UptimeChecks::class);
}
}
Now you can access and delete the relationship in your boot method.
$model->uptimeChecks()->delete();
You need to create a function in User.php
public function uptimeCheck()
{
return $this->hasOne('App\UptimeChecks');
}
and change the boot function
$model->uptimeCheck()->delete();
This way you need to do for all related relations.
This probably should be: $model->user()->delete() instead. There's nothing else.
If this shouldn't be the intention, reconsider the direction of the relationship.

I can not add methods to User Model in Laravel 5.4 ..!

I'm using Laravel 5.4. I have the methods I created in the User model. When I want to create an object from the User model and invoke my own methods, I can not get to the methods I added.
The User Model is derived from the Authenticable class at 5.4, which was derived earlier from the Model class. I think the problem is about it. What I really want to do is to set up the belong_to, has_many structure to relate the user model to the other models.
But with the User model I do not do that. What do you recommend ?
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
public function getMakale()
{
return $this->hasMany(Makale::class, 'user_id', 'id');
}
}
$author = User::first();
return method_exists($author,'getMakale');
//eventually turns false
I believe you are potentially setting an accessor rather than a relationship. In this case I would think you want to name that function something like:
public function haberler()
{
return $this->hasMany(Makale::class, 'user_id', 'id');
}
or public function makales(). Prefixing your function name with get or set will have unintended consequences in Laravel.

Laravel 5.2 ACL how to have multiple permissions with the same name and avoid having one role per user

Okay, so I'm trying to implement an ACL using Laravel on an Intranet and I'm having some problems with permissions growing rapidly out of control. So first off, here's what I've got:
My five tables defining my users, my roles and my permissions like this:
tblIntranetUser
UserID
Name
FirstName
Username
tblIntranetRoles
RoleID
RoleName
Description
tblIntranetPermissions
PermissionID
PermissionName
Description
tblIntranetRoles_Permissions
RoleID
PermissionID
tblIntranetUsers_Roles
UserID
RoleID
And also I have the AuthServiceProvider as well as the Permission and Role models:
class Permission extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblIntranetPermissions';
protected $primaryKey = 'PermissionID';
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['PermissionID', 'PermissionName', 'Description'];
public function roles()
{
return $this->belongsToMany('App\Role', 'tblIntranetRoles_Permissions', 'PermissionID', 'RoleID');
}
public function detachAllRoles()
{
$roles = $this->roles;
foreach($roles as $role){
$role->permissions()->detach($this);
}
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\User;
class Role extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblIntranetRoles';
protected $primaryKey = 'RoleID';
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['RoleID', 'RoleName', 'Description'];
public function permissions()
{
return $this->belongsToMany('App\Permission', 'tblIntranetRoles_Permissions', 'RoleID', 'PermissionID');
}
public function givePermissionTo(Permission $permission)
{
return $this->permissions()->save($permission);
}
public function getUsers()
{
$users = User::orderBy('UserID')->get();
$roleusers = collect();
foreach($users as $user){
if($user->hasRole($this->name)){
$roleusers->push($user);
}
}
return $roleusers;
}
public function detachAllUsers()
{
$users = $this->getUsers();
foreach($users as $user){
$user->roles()->detach($this);
}
}
public function detachAllPermissions()
{
$permissions = $this->permissions;
foreach($permissions as $permission){
$permission->roles()->detach($this);
}
}
}
namespace App\Providers;
use App\Report, App\Permission;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
];
/**
* Register any application authentication / authorization services.
*
* #param \Illuminate\Contracts\Auth\Access\Gate $gate
* #return void
*/
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
foreach ($this->getPermissions() as $permission){
$gate->before(function ($user) {
if ($user->isSuperAdmin()) {
return true;
}
});
$gate->define($permission->name, function($user) use ($permission){
return $user->hasRole($permission->roles);
});
}
}
protected function getPermissions()
{
return Permission::with('roles')->get();
}
}
So, thanks to this, I was able to create various roles and assign permissions to them which allows them to access certain sections of the Intranet as well as see certain reports. For example, I can define the following:
Role: Analyst
Access: Section 1, 2, 3
Reports: 1,15,41
Role: Developer
Access: All sections
Reports: All reports
It would be fine if every analyst could see and access the same sections... but of course that's not the case. Same goes for developers. Following this model, it basically means I need to have one role for every user as well as one permission for every possible element on the Intranet. Given that there's roughly 200 reports available as well as about 30 users, this makes for a lot of "show_report_1", "show_report_2", "show_section_1", "show_section_2" permissions (Laravel identifies permissions by name).
So, in order to make things a bit more... orderly I guess, I've been wondering if there wouldn't be a way to have one permission named "show_report" with the reportID stored in another field and to avoid having one role per user.
I'm not sure of the "proper" way to do this, but you could add an extra row to one of your pivot tables (probably the role_permission one) and use that to store more specific data about the permission. (eg. sections they can access)
Check out here for accessing pivot values: https://laravel.com/docs/5.5/eloquent-relationships#many-to-many
$role = App\Role::find(1);
foreach ($role->permissions as $permission) {
echo $permission->pivot->permission_settings; // [1,2,3]
}
That way you could have a single permission of "access_section", and then just check the pivot to see what sections they can access.
(There is probably a better or 'proper' way to do this though)

Laravel relationship 2 layers

I have my database (=model) structure like that:
game:
lot (typeof Lot)
places (array type of Place)
place_id // just a number of a lot in some game
user_id
What should I do to call in everywhere like this:
User::find(1)->games() // returns Game collection where user has places
?
Models are:
class Place extends Model
{
protected $fillable = ['place_id', 'user_id', 'game_id'];
public function user() {
return $this->belongsTo(User::class);
}
public function game() {
return $this->belongsTo(Game::class);
}
}
User:
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['name', 'email', 'steam_id', 'avatar'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['remember_token'];
/**
* Get all of the tasks for the user.
*/
public function items()
{
return $this->hasMany(SteamItem::class);
}
public function places() {
return $this->hasMany(Place::class);
}
}
The Game:
class Game extends Model
{
protected $fillable = ['lot_id'];
public function lot() {
return $this->belongsTo(Lot::class);
}
public function places() {
return $this->hasMany(Place::class);
}
}
Now I use this code in my User class:
public function games() {
return Game::with(['places' => function ($query) {
$query->where('user_id', $this->id);
}]);;
}
It doesn't work, because I need to make it as a relationship method, but with method returns a query builder.
In the finals I must call $user->games and it should return me all the games user linked to through place.
Okay. I think I understand now.
User has many Place. Place belongs to User.
Place belongs to Game. Game has many Place.
You can try this:
$user = User::with('places.game.lot')->find(1);
This will fetch the User and eager load all the relationships. Because Place belongsTo a Game, which in turn belongs to Lot, you can then do this:
#foreach ($user->places as $place)
<img src="{{$place->game->lot->imageUrl}}" />
#endforeach
Also, place is actually a pivot table, and you can take advantage of Eloquent's many-to-many relationship, which I would recommend reading about.

Laravel 5 setup model event to "clear up" pivot tables on model delete

I am using Laravel 5 to build a user based application. Some models have a manyToMany relationship in my app and therefore I am using pivot tables.
When I delete a user from the system, I use this simple function:
/**
* Delete user.
*
* #param $id
* #return mixed
*/
public function deleteUser($id)
{
return $this->user->whereId($id)->delete();
}
However, when the user is deleted, the rows in the pivot tables (for example role_user) do not get deleted.
I have read on the laravel site that I can use model events to "clear up" my pivot tables, but i'm really unsure how I would implement that.
Can anyone point me in the right direction?
Edit
Below is my current model setup:
namespace App\Models\User;
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 App\Scopes\MultiTenantTrait;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword, MultiTenantTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'user';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['cust_id', 'first_name', 'last_name', 'email', 'status', 'activation_code'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['password', 'remember_token'];
/**
* Boot the model.
*
*/
public static function boot()
{
parent::boot();
static::deleting(function($user)
{
$user->roles()->delete();
$user->supervisors()->delete();
$user->types()->delete();
$user->rates()->delete();
$user->miscs()->delete();
});
}
...
You can add a boot method to your models, like the following:
public static function boot() {
parent::boot();
// This is a deleting event on the model
static::deleting(function($model) {
$model->... //Here your model is still available
// You could add something like this
DB::table('role_user')->where('user_id', $model->id)->delete();
})
}
But you can also extend the delete method in your models:
public function delete() {
DB::table('role_user')->where('user_id', $this->id)->delete();
parent::delete();
}

Categories