Cascading soft deletes with laravel 4 not working as expected - php

I'm attempting to make cascading soft deletes work by overriding each model's delete() function, which I think should cascade down from Project to Version to Link, but the problem is it doesn't seem to do that at all. The idea would be that deleting a project would also delete all the versions, which would delete all the links and clear their cached versions, but using $this->versions()->delete(); doesn't seem to actually call the delete() method in the Version model.
Any ideas on how to get this working as I expect it to?
class Project extends Eloquent {
protected $softDelete = true;
public function versions()
{
return $this->hasMany('Version');
}
public function delete()
{
$this->versions()->delete();
return parent::delete();
}
}
class Version extends Eloquent {
protected $softDelete = true;
public function project()
{
return $this->belongsTo('Project', 'project_id');
}
public function links()
{
return $this->hasMany('Link');
}
public function delete()
{
$this->links()->delete();
return parent::delete();
}
}
class Link extends Eloquent {
protected $softDelete = true;
public function version()
{
return $this->belongsTo('Version', 'version_id');
}
public function delete()
{
Cache::forget($this->id);
return parent::delete();
}
}

You want to use the models events instead of overriding the core functions. To quote the docs:
Eloquent models fire several events, allowing you to hook into various points in the model's lifecycle using the following methods: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored.
What you want to do is hook into these. You can do this a couple of ways (see the docs here). Here's an example by setting a model boot method:
public static function boot()
{
parent::boot();
/**
* Deleting
* - Called before delete()
* - Passes the instance in as param
*/
static::deleting(function($project){
// Get the project versions ids as an array
$ids = $project->versions()->lists('id');
// Delete the versions
Version::whereIn('id', $ids)->delete();
});
}
You can then do the same in your other models. Hope that helps!

$this->versions()->delete(); as well as Version::whereIn('id', $ids)->delete(); does a delete() call on a query builder, not the eloquent models, which means model events don't get fired. Instead, you should do:
foreach($this->versions as $version) { $version->delete(); }

Related

Persisting Eloquent Builder Instance in Laravel Livewire

there is an instance of \Illuminate\Database\Eloquent\Builder that I would like to persist when instantiate (during the mount lifecycle hook) so that I can use it for queries. The problem is I would not be able to set it as public property since it will throw a Livewire\Exceptions\PublicPropertyTypeNotAllowedException (as follows). I would not be able to save it in session as well since it is a PDO instance and PDO instances can't be serialized or unserialized.
Any idea guys?
Edit:
Controller:
public function index() {
$tableQuery = User::query();
return view('table', compact('tableQuery'));
}
table.blade.php:
<livewire:table
:tableQuery=$tableQuery
>
App\Http\Livewire\Table:
class Table extends Component{
private $tableQuery;
public function mount($tableQuery){
$this->tableQuery = $tableQuery;
}
public function render(){
return view('livewire.table');
}
}
you can use Model inside livewire
class Table extends Component{
public $tableQuery;
public function mount(){
$this->tableQuery = User::query();;
}
public function render(){
return view('livewire.table');
}
}

How to pass parameter to a laravel elequent model's event observer

I have a model in laravel and I want to do something after the first time which an object of my model is created. the simplest way is to add a static boot method inside my model's class like the code below:
class modelName extends Model
{
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
});
}
}
so far so good! the problem is: the ONLY parameter that created method accepts is the model object itself(according to the documentation) :
Each of these methods receives the model as their only argument.
https://laravel.com/docs/5.5/eloquent#events
I need more arguments to work with after model creation. how can I do that?
Or is there any other way to do something while it's guaranteed that the model has been created?
laravel version is 5.5.
You're close. What I would probably do would be to dispatch an event right after you actually create the model in your controller. Something like this.
class WhateverController
{
public function create()
{
$model = Whatever::create($request->all());
$anotherModel = Another::findOrFail($request->another_id);
if (!$model) {
// The model was not created.
return response()->json(null, 500);
}
event(new WhateverEvent($model, $anotherModel));
}
}
I solved the issue using static property in eloquent model class:
class modelName extends Model
{
public static $extraArguments;
public function __construct(array $attributes = [],$data = [])
{
parent::__construct($attributes);
self::$extraArguments = $data ;
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
self::$extraArguments; // is available in here
});
}
}
It works! but I don't know if it may cause any other misbehavior in the application.
Using laravel events is also a better and cleaner way to do that in SOME cases.but the problem with event solution is you can't know if the model has been created for sure and it's time to call the event or it's still in creating status ( and not created status).

Laravel Eloquent belongsToMany() return first()

I have a Laravel 5.5 application and I have the following models.
Jobs
JobSheets
Users
I have a relation on my Users model
public function jobs() {
$this->hasMany('App\Job','user_id')->with('jobSheets');
}
I have a relation on my Jobs model
public function jobSheets() {
$this->belongsToMany('App\JobSheet','jobs_jobsheets','job_id','sheet_id');
}
I'm trying to return all my current logged in users' jobs with the latest job sheet.
A job can have many job sheets but I only want to return the latest.
I've tried in my controller
return Auth::user()->jobs;
However that returns all the jobs sheets against the job.
I've also tried creating another method on my job model.
public function latestJobsheet()
{
return $this->jobsheets->first();
}
and amending my jobs method on the user model to:
public function jobs() {
$this->hasMany('App\Job','user_id')->with('latestJobsheet');
}
but I get the following error:
Call to undefined method
Illuminate\Database\Query\Builder::addEagerConstraints()
You could split the BelongsToMany relationship and add a model for the pivot table (jobs_jobsheets):
class Job {
public function latestJobSheetPivot() {
return $this->hasOne(App\JobSheetPivot::class)->orderBy('sheet_id', 'DESC');
}
}
class JobSheetPivot {
protected $table = 'jobs_jobsheets';
public function jobSheet() {
return $this->belongsTo(App\JobSheet::class);
}
}
class User {
public function jobs() {
return $this->hasMany(App\Job::class)->with('latestJobSheetPivot.jobSheet');
}
}
Assuming your jobs_jobsheets table contains an id column it would probably be best to order the jobSheets returned from newest to oldest.
Here's what that would look like:
public function jobSheets()
{
$this->belongsToMany('App\JobSheet','jobs_jobsheets','job_id','sheet_id')->orderBy('id', 'DESC');
}
If you want to limit the number of results returned you can chain ->limit($num_results) to the end of the belongsToMany method
You can override the $with property on your Job model and have it eager load the relation latestJobsheet. From there, when you access the jobs relation on your User model, you should have the latest job sheet available to you. How you determine the latest job sheet is a separate matter.

Laravel observer for pivot table

I've got a observer that has a update method:
ObserverServiceProvider.php
public function boot()
{
Relation::observe(RelationObserver::class);
}
RelationObserver.php
public function updated(Relation $relation)
{
$this->cache->tags(Relation::class)->flush();
}
So when I update a relation in my controller:
public function update(Request $request, Relation $relation)
{
$relation->update($request->all()));
return back();
}
Everything is working as expected. But now I've got a pivot table. A relation belongsToMany products.
So now my controller method looks like this:
public function update(Request $request, Relation $relation)
{
if(empty($request->products)) {
$relation->products()->detach();
} else {
$relation->products()->sync(collect($request->products)->pluck('id'));
}
$relation->update($request->all());
return back();
}
The problem is that the observer is not triggered anymore if I only add or remove products.
How can I trigger the observer when the pivot table updates aswel?
Thanks
As you already know, Laravel doesn't actually retrieve the models nor call save/update on any of the models when calling sync() thus no event's are created by default. But I came up with some alternative solutions for your problem.
1 - To add some extra functionality to the sync() method:
If you dive deeper into the belongsToMany functionality you will see that it tries to guess some of the variable names and returns a BelongsToMany object. Easiest way would be to make your relationship function to simply return a custom BelongsToMany object yourself:
public function products() {
// Product::class is by default the 1. argument in ->belongsToMany calll
$instance = $this->newRelatedInstance(Product::class);
return new BelongsToManySpecial(
$instance->newQuery(),
$this,
$this->joiningTable(Product::class), // By default the 2. argument
$this->getForeignKey(), // By default the 3. argument
$instance->getForeignKey(), // By default the 4. argument
null // By default the 5. argument
);
}
Or alternatively copy the whole function, rename it and make it return the BelongsToManySpecial class. Or omit all the variables and perhaps simply return new BelongsToManyProducts class and resolve all the BelongsToMany varialbes in the __construct... I think you got the idea.
Make the BelongsToManySpecial class extend the original BelongsToMany class and write a sync function to the BelongsToManySpecial class.
public function sync($ids, $detaching = true) {
// Call the parent class for default functionality
$changes = parent::sync($ids, $detaching);
// $changes = [ 'attached' => [...], 'detached' => [...], 'updated' => [...] ]
// Add your functionality
// Here you have access to everything the BelongsToMany function has access and also know what changes the sync function made.
// Return the original response
return $changes
}
Alternatively override the detach and attachNew functions for similar results.
protected function attachNew(array $records, array $current, $touch = true) {
$result = parent::attachNew($records, $current, $touch);
// Your functionality
return $result;
}
public function detach($ids = null, $touch = true)
$result = parent::detach($ids, $touch);
// Your functionality
return $result;
}
If you want to dig deeper and want to understand what's going on under the hood then analyze the Illuminate\Database\Eloquent\Concerns\HasRelationship trait - specifically the belongsToMany relationship function and the BelongsToMany class itself.
2 - Create a trait called BelongsToManySyncEvents which doesn't do much more than returns your special BelongsToMany class
trait BelongsToManySyncEvents {
public function belongsToMany($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) {
if (is_null($relation)) {
$relation = $this->guessBelongsToManyRelation();
}
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$relatedKey = $relatedKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
return new BelongsToManyWithSyncEvents(
$instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
);
}
}
Create the BelongsToManyWithSyncEvents class:
class BelongsToManyWithSyncEvents extends BelongsToMany {
public function sync($ids, $detaching = true) {
$changes = parent::sync($ids, $detaching);
// Do your own magic. For example using these variables if needed:
// $this->get() - returns an array of objects given with the sync method
// $this->parent - Object they got attached to
// Maybe call some function on the parent if it exists?
return $changes;
}
}
Now add the trait to your class.
3 - Combine the previous solutions and add this functionality to every Model that you have in a BaseModel class etc. For examples make them check and call some method in case it is defined...
$functionName = 'on' . $this->foreignKey . 'Sync';
if(method_exists($this->parent), $functionName) {
$this->parent->$functionName($changes);
}
4 - Create a service
Inside that service create a function that you must always call instead of the default sync(). Perhaps call it something attachAndDetachProducts(...) and add your events or functionality
As I didn't have that much information about your classes and relationships you can probably choose better class names than I provided. But if your use case for now is simply to clear cache then I think you can make use of some of the provided solutions.
When I search about this topic, it came as the first result.
However, for newer Laravel version you can just make a "Pivot" model class for that.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PostTag extends Pivot
{
protected $table = 'post_tag';
public $timestamps = null;
}
For the related model
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->using(PostTag::class);
}
and you have to put your declare your observer in EventServiceProvider
as stated in Laravel Docs
PostTag::observe(PostTagObserver::class);
Reference: Observe pivot tables in Laravel
Just add:
public $afterCommit = true;
at the beginning of the observer class.. It will wait until the transactions are done, then performs your sync which should then work fine..
Please check Laravel's documentation for that.
It seems this solutions was just added in Laravel 8.

Use Laravel touches without global scopes

Concept Problem:
I have a very simple problem when using the touches attribute, to automatically update timestamp on a depending model; it correctly does so but also applies the global scopes.
Is there any way to turn this functionality off? Or to ask specifically for automatic touches to ignore global scopes?
Concrete Example:
When an ingredient model is updated all related recipes should be touched. This works fine, except we have a globalScope for separating the recipes based on locales, that also gets used when applying the touches.
Ingredient Model:
class Ingredient extends Model
{
protected $touches = ['recipes'];
public function recipes() {
return $this->belongsToMany(Recipe::class);
}
}
Recipe Model:
class Recipe extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new LocaleScope);
}
public function ingredients()
{
return $this->hasMany(Ingredient::class);
}
}
Locale Scope:
class LocaleScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$locale = app(Locale::class);
return $builder->where('locale', '=', $locale->getLocale());
}
}
If you want to explicitly avoid a global scope for a given query, you may use the withoutGlobalScope() method. The method accepts the class name of the global scope as its only argument.
$ingredient->withoutGlobalScope(LocaleScope::class)->touch();
$ingredient->withoutGlobalScopes()->touch();
Since you're not calling touch() directly, in your case it will require a bit more to make it work.
You specify relationships that should be touched in model $touches attribute. Relationships return query builder objects. See where I'm going?
protected $touches = ['recipes'];
public function recipes() {
return $this->belongsToMany(Recipe::class)->withoutGlobalScopes();
}
If that messes with the rest of your application, just create a new relationship specifically for touching (heh :)
protected $touches = ['recipesToTouch'];
public function recipes() {
return $this->belongsToMany(Recipe::class);
}
public function recipesToTouch() {
return $this->recipes()->withoutGlobalScopes();
}
You can define a relationship in the model and pass parameters to it like following:
public function recipes($isWithScope=true)
{
if($isWithScope)
return $this->belongsToMany(Recipe::class);
else
return $this->recipes()->withoutGlobalScopes();
}
then use it like this recipes->get(); and recipes(false)->get();

Categories