framework: Unexpected behavior in firstOrNew method in belongs to many relationships

  • Laravel Version: v9.17.0
  • PHP Version: 8.1.7
  • Database Driver & Version: MySQL 8.0.29

Description:

There is an unexpected behavior in firstOrNew method in belongsToMany relationships. When I try to firstOrNew a related model which it doesn’t exist, I get the first record of database which I shouldn’t.

Steps To Reproduce:

I have a ContentPost model which has a belongsToMany relationship with UploaderFile and the pivot table is called uploader_attachments. This is the relation method in ContentPost:

public funciton files()
{
   return $this->belongsToMany(UploaderFile::class, "uploader_attachments", "model_id", "file_id")
                    ->withPivot("group", "model_name")
                    ->withTimestamps()
                    ->where("model_name", "ContentPost")
            ;
}

When I try to find a UploaderFile with a original_name which doesn’t exist, It returns a new empty instance as below:

# executed query on uploader-file model:
(new UploaderFile)->where("original_name","something not exists")->firstOrNew();

# results:
App\Models\UploaderFile {
     id: 0,
 }

But when I execute this query on the ContentPost’s files relation method. It gives me the first record.

# executed query on content-post files relation method:
(new ContentPost)->files()->where("original_name","something not exists")->firstOrNew();

# results:
App\Models\UploaderFile {
     id: 1,
     original_name: "24432660_0.jpg",
     extension: "jpg",
     mime_type: "image/jpeg",
     given_name: "1578837071_SW4zL94LHGn1ZYb5VrHHkFljJ8PCHjH6BCYfMGAx",
     private: 0,
     created_at: "2020-01-12 17:21:11",
     updated_at: "2020-01-12 17:21:11",
     deleted_at: null,
     created_by: 2,
     updated_by: 0,
     deleted_by: 0,
     meta: "{"versions":["panel_thumb"]}",
   }

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 16 (11 by maintainers)

Most upvoted comments

There is a real unexpected behavior when even a model has been attached to the specified model. As you see in this image, my model (ContentPost) already has a file. It has been attached to an UploaderFIle with ID of 207, but in firstOrNew method it still gets the first record of UploaderFiles!!

ContentPost::first()->files()->first();
  // returns the first correct attached file 
App\Models\UploaderFile {#5407
     id: 207,
     original_name: "35699887_0.jpeg",
     extension: "jpeg",
     mime_type: "image/jpeg",
     given_name: "1657699399_3BhUGrLSe8anxQrxBKhnJ8S3ZVYBFwvPneeqjOCQ",
     private: 0,
     created_at: "2022-07-13 12:33:19",
     updated_at: "2022-07-13 12:33:19",
     deleted_at: null,
     created_by: 0,
     updated_by: 0,
     deleted_by: 0,
     meta: "{"versions":["panel_thumb"]}",
     converted: 0,
     pivot: Illuminate\Database\Eloquent\Relations\Pivot {#5408
       model_id: 70,
       file_id: 207,
       group: "",
       model_name: "ContentPost",
       created_at: "2022-07-23 17:46:55",
       updated_at: "2022-07-23 17:46:55",
     },
   }

ContentPost::first()->files()->firstOrNew();
   // returns the not-attached, not correct file. first record of database:
App\Models\UploaderFile {#5429
     id: 1,
     original_name: "24432660_0.jpg",
     extension: "jpg",
     mime_type: "image/png",
     given_name: "1578837071_SW4zL94LHGn1ZYb5VrHHkFljJ8PCHjH6BCYfMGAx",
     private: 0,
     created_at: "2020-01-12 17:21:11",
     updated_at: "2020-01-12 17:21:11",
     deleted_at: null,
     created_by: 2,
     updated_by: 0,
     deleted_by: 0,
     meta: "{"versions":["panel_thumb"]}",
     converted: 0,
   }

We always expect the first and firstOrNew methods to behave the same when a model exists, but they have different behaviors.

Hi @driesvints, thanks for your answering. I think you misunderstood the problem. You know, it is not even about a where clause. Normal belongsToMany relationship can cause this problem.

Please check this image which is taken from the tinker:

(new ContentPost())->files()->first(); 
    // returns null
(new ContentPost())->files()->firstOrNew();
   // returns first record of table:
App\Models\UploaderFile {#5443
     id: 1,
     original_name: "24432660_0.jpg",
     extension: "jpg",
     mime_type: "image/jpeg",
     given_name: "1578837071_SW4zL94LHGn1ZYb5VrHHkFljJ8PCHjH6BCYfMGAx",
     private: 0,
     created_at: "2020-01-12 17:21:11",
     updated_at: "2020-01-12 17:21:11",
     deleted_at: null,
     created_by: 2,
     updated_by: 0,
     deleted_by: 0,
     meta: "{"versions":["panel_thumb"]}",
     converted: 0,
   }

This happens because no ContentPosts are loaded, so it will grab the first record. it is the same as when you fetch it directly from UploaderFile: image

(new UploaderFile())->firstOrNew();
   // returns first record:
App\Models\UploaderFile {#5424
     id: 1,
     original_name: "24432660_0.jpg",
     extension: "jpg",
     mime_type: "image/jpeg",
     given_name: "1578837071_SW4zL94LHGn1ZYb5VrHHkFljJ8PCHjH6BCYfMGAx",
     private: 0,
     created_at: "2020-01-12 17:21:11",
     updated_at: "2020-01-12 17:21:11",
     deleted_at: null,
     created_by: 2,
     updated_by: 0,
     deleted_by: 0,
     meta: "{"versions":["panel_thumb"]}",
     converted: 0,
   }

The problem is when you already loaded a content-post model and you know it doesn’t have any files. But in firstOrNew method, it is loading the first record still: image

// I am loading the first ContentPost
ContentPost::first()->files()->first(); 
    // returns null
ContentPost::first()->files()->firstOrNew();
   // returns first record of table:
App\Models\UploaderFile {#5443
     id: 1,
     original_name: "24432660_0.jpg",
     extension: "jpg",
     mime_type: "image/jpeg",
     given_name: "1578837071_SW4zL94LHGn1ZYb5VrHHkFljJ8PCHjH6BCYfMGAx",
     private: 0,
     created_at: "2020-01-12 17:21:11",
     updated_at: "2020-01-12 17:21:11",
     deleted_at: null,
     created_by: 2,
     updated_by: 0,
     deleted_by: 0,
     meta: "{"versions":["panel_thumb"]}",
     converted: 0,
   }

In Laravel8, the same code was returning an empty instance of the UploaderFile, which it was correct.