Laravel trigger soft delete on multi level relationships - laravel

I am trying to delete a parent record which then deletes a multi level relationship.
I have 3 tables: accounts -> services -> service_items
When I delete an account I want the services to be deleted and also all the service_items related to that service.
In my Account model I have the following code
public function delete()
{
$this->services()->delete();
return parent::delete();
}
This is working, when the account is deleted all the related services are deleted, but the service items are not being removed.
I have the following function in my Service model
public function delete()
{
$this->service_items()->delete();
return parent::delete();
}
It seems like this function isn't being triggered when I delete an account. I know I can loop over the services and delete each service_item but I was wondering if there was a way to just trigger the delete function in the Service model?
NOTE: If I directly delete a service the service_items are deleted

Alright, so here is the solution.
Instead of deleting each service_item individually, you should use the concept of the hook of the Laravel by declaring the booted method under the model
Account.php
<?php
class Account extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::deleted(function($account) {
$account->services()->delete();
});
}
/**
* List of services associated with an account.
*/
public function services()
{
// or you may have some other relationship
return $this->hasMany('App\Service');
}
}
Service.php
<?php
class Service extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::deleted(function($service) {
$service->items()->delete();
});
}
/**
* List of items associated with an account.
*/
public function items()
{
// or you may have some other relationship
return $this->hasMany('App\ServiceItem');
}
}
So whenever you delete an account then it will delete all services associated with that account and in turn, it will delete all the service items associated with each service.

Related

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 relationship with additional where statement

I know I can define a relationship by
Class Users extends Model{
function profile(){
return $this->hasOne(Profile::Class);
}
}
is there a way like adding extra query to the relationship like other than foreign key and local key that is available to define, I want to only get those records of Profile model that field active contains a value of 1. Profile model has a field named active. Any help, ideas is greatly appreciated, thank you in advance.
you can simply try
return $this->hasOne(Profile::Class)->where('active', 1);
but better approach will be using Scope like this.
create a folder app/Scopes and add a new file ActiveUserOnly.php
place this code there
namespace App\Scopes;
use \Illuminate\Database\Eloquent\Builder;
use \Illuminate\Database\Eloquent\Scope;
use \Illuminate\Database\Eloquent\Model;
class ActiveUsersOnly implements Scope {
/**
* #inheritdoc
*
* #param Builder $builder
* #param Model $model
*
* #return Builder|void
*/
public function apply( Builder $builder, Model $model ) {
return $builder->where( 'active', '=', true );
}
}
add this code to the top of Profile model.
use App\Scopes\ActiveProfilesOnly;
add this code in your Profile model.
protected static function boot() {
parent::boot();
static::addGlobalScope( new ActiveProfilesOnly() );
}
then this code will work in your User model.
Class Users extends Model{
function profile(){
return $this->hasOne(Profile::Class);
}
}

Separate roles/permissions if user belongs to many organizations

I have an application where a user can belong to multiple organizations. I want to set it up in a way that a user can have different roles/permissions for each organization. I am using Laravel and plan on implementing Spatie/laravel-permission. What is the best way to implement this?
I have tried setting up two guards, one for the main user account and another for the pivot model between the user and the organization they log into. So basically when they log into the app using the main user model, I ask them which organization they would like to log into, when they choose the organization I will then also set up an auth session on the pivot model that links the user to the organization and access the roles off that model. This works, but having to manage the auth sessions is kind of a pain.
// User Model
class User extends Authenticatable
{
public function organizationUsers()
{
return $this->hasMany(OrganizationUser::class);
}
}
// OrganizationUser Model
class Organziationuser extends Authenticatable
{
use HasRoles;
public $guard_name = 'organization_user';
public function organization()
{
return $this->belongsTo(Organization::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
I would expect a user to be able to log into the application using a single login, but also be able to have different permissions for different organizations.
I have got around this issue by doing the following. I would welcome feedback on peoples views of this method!
Note: Currently I am only using the model_has_roles table with Spatie permissions and always use $user->can('Permission') to check permissions.
Our company model has the following relationships and method
class Company extends Model
{
public function owner(): HasOne
{
return $this->hasOne(User::class, 'id', 'user_id');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(
User::class, 'company_users', 'company_id', 'user_id'
)->using(CompanyUser::class);
}
public function addTeamMember(User $user)
{
$this->users()->detach($user);
$this->users()->attach($user);
}
}
We modify the pivot model to have the Spatie HasRoles trait. This allows us to assign a role to the CompanyUser as opposed to the Auth User. You also need to specify the default guard or Spatie permissions squarks.
class CompanyUser extends Pivot
{
use HasRoles;
protected $guard_name = 'web';
}
On the user model, I have created the HasCompanies Trait. This provides the relationships and provides a method for assigning the roles to the new company user. Additionally, it overwrites the gate can() method.
A user can belong to many companies, but can only have one active company at a time (i.e. the one they are viewing). We define this with the current_company_id column.
It is also important to ensure the pivot table ID is pulled across (which it will not be as standard) as this is now what we are using in the Spatie model_has_roles table.
trait HasCompanies
{
public function companies(): HasMany
{
return $this->hasMany(Company::class);
}
public function currentCompany(): HasOne
{
return $this->hasOne(Company::class, 'id', 'current_company_id');
}
public function teams(): BelongsToMany
{
return $this->belongsToMany(
Company::class, 'company_users', 'user_id', 'company_id'
)->using(CompanyUser::class)->withPivot('id');
}
public function switchCompanies(Company $company): void
{
$this->current_company_id = $company->id;
$this->save();
}
public function assignRolesForCompany(Company $company, ...$roles)
{
if($company = $this->teams()->where('companies.id', $company->id)->first()){
/** #var CompanyUser $companyUser */
$companyUser = $company->pivot;
$companyUser->assignRole($roles);
return;
}
throw new Exception('Roles could not be assigned to company user');
}
public function can($ability, $arguments = [])
{
if(isset($this->current_company_id)){
/** #var CompanyUser $companyUser */
$companyUser = $this->teams()->where('companies.id', $this->current_company_id)->first()->pivot;
if($companyUser->hasPermissionTo($ability)){
return true;
}
// We still run through the gate on fail, as this will check for gate bypass. i.e. Super User
return app(Gate::class)->forUser($this)->check('InvalidPermission');
}
return app(Gate::class)->forUser($this)->check($ability, $arguments);
}
}
Now we can do something like this:
Create the role & permission
/** #var Role $ownerRoll */
$ownerRoll = Role::create(['name' => 'Owner']);
/** #var Permission $permission */
$permission = Permission::create([
'name' => 'Create Company',
'guard_name' => 'web',
]);
$ownerRoll->givePermissionTo($permission);
Create a new company with an owning user and then switch this company to that owner's active company.
public function store(CompanyStoreRequest $request)
{
DB::transaction(function () use($request) {
/** #var User $owner */
$owner = User::findOrFail($request->user_id);
/** #var Company $company */
$company = $owner->companies()->create($request->validated());
$company->addTeamMember($owner);
$owner->assignRolesForCompany($company, 'Owner');
$owner->switchCompanies($company);
});
return redirect()->back();
}
So this all works, my main concerns are that:
We are overwriting the can method. There may be other authorization methods/gate functions that are not caught.
We have 2 sets of model_permissions. The Auth user and the company user. I think I need to build in some checks to ensure that only the correct kinds of users can be assigned to the roles. At this stage, all administrator users would have permissions assigned to their auth user, while any users who own a company should only have permissions on the company user model

Amending foreign key on a Laravel relationship

In Laravel (v5.7.12), I have two models - User and Project.
A user has an id and can have many projects. A project has an owner_id.
I can't seem to configure the relationship correctly. In my user model, I have:
/**
* Get the projects associated with the user.
*/
public function projects()
{
$this->hasMany('\App\Project', 'owner_id', 'id');
}
In my project model, I have:
/**
* Get the owner associated with the user.
*/
public function owner()
{
$this->belongsTo('\App\User', 'id', 'owner_id');
}
But calling either $user->projects() or $project->owner() returns null.
How should I configure my non-standard relationship keys?
You forgot to return the method:
public function projects()
{
return $this->hasMany('\App\Project', 'owner_id');
}
Do this also for the second one:
public function owner()
{
return $this->belongsTo('\App\User', 'owner_id');
}

Relationships in InfyOm Generator

I have News and NewsCategories models which I have generated CRUD for using the relationship option.
I now need to generate a select list for the News model to select the NewsCategory it belongs to.
I know how to do this in the model but no idea how to do it using the repository pattern.
I can't see any examples in the docs so any help with this would be appreciated.
Thanks
NewsRepository
/**
* Configure the Model
**/
public function model()
{
return News::class;
}
News Model
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
**/
public function newsCategory()
{
return $this->belongsTo(NewsCategory::class);
}
News Controller
/**
* Show the form for creating a new News.
*
* #return Response
*/
public function create()
{
return view('news.create');
}
/**
* Store a newly created News in storage.
*
* #param CreateNewsRequest $request
*
* #return Response
*/
public function store(CreateNewsRequest $request)
{
$input = $request->all();
$news = $this->newsRepository->create($input);
Flash::success('News saved successfully.');
return redirect(route('news.index'));
}
If your repository extends InfyOm\Generator\Common\BaseRepository. The repository should update the model relations by it self. Just pass the relation values alongside the other inputs with the correct keys.
However, for deleting and reading (let's call them actions), you will need to query your data.
You can do that using repository methods, scope queries, or criteria classes.
(and call those filters).
Repository Methods:
// inside your controller
// some repository filtering method
$this->repository->whereHas('newsGroup', function($query){...});
$this->repository->hidden(['field_to_hide']);
...
// some action: delete, all or findWhere...
$this->repository->delete();
Scope Queries are callbacks that apply some queries on the model eloquent and return it.(unlike Eloquent scopes which accept and return Database\Eloquent\Builder)
$this->repository->scopeQuery(
function ($model){ return $model->where(...);
});
Or your
// some action: delete, update or findWhere...
$this->repository->delete();
The Criteria Way: you will create a class responsible on querying. It is an overkill for the simple use-cases.
// inside the controller
$this->repository->pushCriteria(new NewsBelongingToCategory ($group_id));
// App\Criteria\NewsBelongingToCategory.php
class NewsBelongingToCategory implements CriteriaInterface {
private $group_id;
public function __construct($group_id){
$this->group_id = $group_id;
}
public function apply($model, NewsRepositoryInterface $repository)
{
$group_id = $this->group_id;
$model = $model->whereHas('newsCategory',
function ($query) use ($group_id){
$query->where('group_id', '=', $group_id);
});
return $model;
}
}
// in your controller
$this->repository->delete();
Note that some actions ignore specific filters. For example, delete(id) and update($attributes, $id) does not use criteria, in the other hand lists($column, $key) does not use scopes.

Resources