Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Magalliu's avatar

decrement('column_name') on trashed related model

I'm trying to decrement like this on related hasOne trashed item but it throws me an error.

$review->category->decrement('total_reviews');

The error:

Call to a member function decrement() on null

Can I decrement on related model which is trashed, and how can I achieve that?

P.S. I tried doing it manually with -- operator decrementing with 1 but throws this error:

Indirect modification of overloaded property App\Models\Review::$category has no effect...

Thanks

0 likes
27 replies
tykus's avatar

Can you decrement before you delete the Review instance (maybe wrapped in a transaction to make an atomic operation):

\DB::transaction(function () {
    $review->category->decrement('total_reviews');

    $review->delete();
});

Or, assign the Category instance as a local variable first?

$category = $review->category;

$review->delete();

$category->decrement('total_reviews');
J4Wx's avatar

Or do it in the deleting event (or an observer), so that you don't need to add this code in every place you might delete a review

tykus's avatar

@j4wx an Observer (of itself) does not solve the question actually asked.

J4Wx's avatar

Of course it doesn't. That's why I replied to your existing answer with a supplement instead of posting my own.

What a bizarre statement.

Magalliu's avatar

let me give all the method so you can undertstand better my situation:

public function deletePermanentlyService(int $id) :void
    {
        $review = Review::onlyTrashed()->findOrFail($id);

        if ($review->no_yes == 'yes') {

            $review->category->decrement('total_reviews');
            $review->category->decrement('total_yes');

            $review->product->decrement('total_reviews');
            $review->product->decrement('total_yes');

        } elseif ($review->no_yes == 'no') {

            $review->category->decrement('total_reviews');
            $review->category->decrement('total_no');

            $review->product->decrement('total_reviews');
            $review->product->decrement('total_no');
        }

        $review->forceDelete();

    }
Magalliu's avatar

So the idea is that first i try to soft delete the product which soft deletes reviews also. After doing this I try to forcedelete the review decrementing from category and product which for the trashed product results in that error.

tykus's avatar

Ok, Call to a member function decrement() on null suggests that the Category instance does not exist (or at least, it does not load) - do you also delete or soft-delete the category?

Magalliu's avatar

No the category is not softdeleted. The category is ok. The error is showed on the line in which I try to decrement the product.

Magalliu's avatar

And I'm doing this test following the workflow, so softdelete the product and related review and then forcedelete the review one by one.

tykus's avatar

Exactly my point; $review->category is null, why? Even when the Review is trashed, the associated Category, if (i) the foreign key is valid on categories and (ii) the referenced Category is not itself not soft-deleted, should load; but you get null

Magalliu's avatar

I've tried also this:

public function deletePermanentlyService(int $id) :void
    {
        $review = Review::onlyTrashed()->findOrFail($id);

        $review->forceDelete();

        if ($review->no_yes == 'yes') {

            $review->category->decrement('total_reviews');
            $review->category->decrement('total_yes');

            $product = $review->product;
            $product->decrement('total_reviews');
            $product->decrement('total_yes');

        } elseif ($review->no_yes == 'no') {

            $review->category->decrement('total_reviews');
            $review->category->decrement('total_no');

            $product = $review->product;
            $product->decrement('total_reviews');
            $product->decrement('total_no);
        }

    }

Still shows me

Call to a member function decrement() on null
Magalliu's avatar

I don't really understand why, yes it throws me that error but the review itself with the last method is deleted, the category is decremented, but the product which is softdeleted is not decremented.

I may think that the category has nothing to do with that...

tykus's avatar

So it is the product, and not the category giving rise to the exception.

Because the product is soft-deleted, the result of $review->product is null; you need to load the Product using withTrashed method:

$product = $review->product()->withTrashed()->first();
Magalliu's avatar

Still same error... strange

Edit: Sorry no same error but:

Call to a member function withTrashed() on null
tykus's avatar

Do you have (i) a product relationbship on Review; (ii) a valid product_id on the Review record?

Magalliu's avatar

Yes of course.

Product model:

public function reviews()
   {
       return $this->hasMany(Review::class, 'product_id', 'id');
   }

Review model:

public function product()
    {
        return $this->belongsTo(Product::class);
    }

reviews migrations:

public function up()
    {
        Schema::create('reviews', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id')->nullable();
            $table->unsignedBigInteger('category_id');
            $table->unsignedBigInteger('status_id');
            $table->unsignedBigInteger('product_id')->nullable();
            $table->unsignedBigInteger('content_id')->nullable();
            $table->string('title')->nullable();
            $table->text('other_content')->nullable();
            $table->string('no_yes');
            $table->string('shop_name');
            $table->string('shop_address');
            $table->string('invoice')->nullable();
            $table->string('product_origin')->nullable();
            $table->string('declined_reason')->nullable();
            $table->string('approved_by')->nullable();
            $table->softDeletes();
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
            $table->foreign('category_id')->references('id')->on('categories');
            $table->foreign('status_id')->references('id')->on('statuses');
            $table->foreign('product_id')->references('id')->on('products');
            $table->foreign('content_id')->references('id')->on('contents');
        });
    }
tykus's avatar

Sanity check:

You have a soft-deleted Review instance, which has a related Category, and a related soft-deleted Product? On each of the related instances, you want to decrement a total_reviews property and a total_yes or total_no property?

Magalliu's avatar

Yes that's right. Now that I'm thinking maybe the issue will be for the category too in case that the related category would be soft-deleted to...

Magalliu's avatar

If I do:

dd($review->product());

It will show me:

Illuminate\Database\Eloquent\Relations\BelongsTo {#454 ▼
  #child: App\Models\Review {#1526 ▶}
  #foreignKey: "product_id"
  #ownerKey: "id"
  #relationName: "product"
  #query: Illuminate\Database\Eloquent\Builder {#560 ▶}
  #parent: App\Models\Review {#1526 ▶}
  #related: App\Models\Product {#1439 ▶}
  #withDefault: null
}

If I try to dd($review->product); it will throw the same error.

tykus's avatar

Well, the method product() will return a BelongsTo relation; whereas the property product should return the result of the relation query. If you listen for, and dump, the executed query, we should expect to see select * from products where id = ? and deleted_at is null:

\DB::listen(function ($query) {
	dump([
		$query->sql,
		$query->bindings,
	]);
});
$review->product

The bindings should be the product_id from the Review?

Magalliu's avatar

The bindings should be the product_id from the Review? : Yes.

I've tried dd(Product::onlyTrashed()->findOrFail($review->product_id)); also but the error:

No query results for model [App\Models\Product]

So I cannot even catch the product using the $review->product_id.

tykus's avatar

onlyTrashed means that the Product must be soft-deleted, whereas withTrashed means the Product might be soft-deleted. Subtle, but important difference.

Magalliu's avatar

Yes I know, in this case the product is soft-deleted and still is not working. I tried with withTrashed also but same error as was with onlyTrashed.

Magalliu's avatar

I also tried to do this using Query Builder but the error this time says:

SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails

This is how I tried:

DB::table('products')->decrement('total_reviews', 1, ['id' => $review->product_id]);

And then I try to delete: $review->forceDelete();. The problem is on decrement a field on soft-deleted product.

What I don't understand is why I can't update a soft-deleted. Is there any way to update a soft-deleted or not?

Magalliu's avatar
Magalliu
OP
Best Answer
Level 2

It's impossible & disappointing that no one could give me even an ugly solution to my problem. Anyway I'm sharing my solution because in the future someone will need it and I don't want anyone to struggle.

My ugly solution consists on doing it all this in vanilla sql:

public function deletePermanentlyService(int $id) :void
    {
        $review = Review::onlyTrashed()->findOrFail($id); 

        $productId = $review->product_id;

        $categoryId = $review->category_id;

        DB::select("update categories set total_reviews = total_reviews-1 where id = '$categoryId'");

        DB::select("update products set total_reviews = total_reviews-1 where id = '$productId'");

        if ($review->no_yes == 'yes') {

            DB::select("update categories set total_yes = total_yes-1 where id = '$categoryId'");
            
            DB::select("update products set total_yes = total_yes-1 where id = '$productId'");

        } elseif ($review->no_yes == 'no') {

            DB::select("update categories set total_no = total_no-1 where id = '$categoryId'");

            DB::select("update products set total_no = total_no-1 where id = '$productId'");
        }

        $review->forceDelete();

    }

I hope this will help someone in the future and that laravel could give more options on working with soft-deleted models...

1 like
miakoda's avatar

what does dd($review->category) give you?

Also, is that method used in a for loop or the like? It may appear that the category is okay, but if you are doing it in a loop maybe most are okay but some of them are not.

ps. if you may register in larachat.slack.com, there is a community it may help you more with this problem.

Magalliu's avatar

Like I specified before dd($review->category) it gives me same results as dd($review->product):

Call to a member function withTrashed() on null

The problem is only when a related belongsTo is soft-deleted. I managed it in a very bad way but I had no choice at the moment and I don't have time right now to go and find a better solution.

Thanks for sharing larachat.slack.com with me.

2 likes

Please or to participate in this conversation.