phplaraveleloquentlaravel-relations

How to load a relationship in Laravel without foreign key?


I have this model:

App\Controllers\ImageController.php

 .....
    $image = new Image;   
    $image->id = "pre_" . (string) Str::ulid()->toBase58();     
    $image->filename = $filename
    $image->post_id= $post_id;
    $image->save();
    
    return $image->load("post:id,status,subject,body");

it is working fine, it returns the following:

{
  "id": "pre_1BzVrTK8dviqA9FGxSoSnU",
  "type": "document",
  "filename": "v1BzVc3jJPp64e7bQo1drmL.jpeg",
  "post_id": "post_1BzVc3jJPp64e7bQo1drmL",
  "post": {
    "id": "post_1BzVc3jJPp64e7bQo1drmL",
    "status": "active"
    ....
  }
 ....
}

App\Models\Image.php

class Image extends Model
{
    use HasFactory;

    public $incrementing = false; 

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

The issue here is that I don't want it returns the "post_id" field, I would like to get the following:

{
      "id": "pre_1BzVrTK8dviqA9FGxSoSnU",
      "type": "document",
      "filename": "v1BzVc3jJPp64e7bQo1drmL.jpeg",
      "post": {
        "id": "post_1BzVc3jJPp64e7bQo1drmL",
        "status": "active"
        ....
      }
     ....
    }

I tried this:

return $image->select(["id", "type", "filename"])->load("post:id,status,subject,body");

and it returns an error:

"message": "Call to undefined method Illuminate\Database\Eloquent\Builder::load()",

What can I do?


Solution

  • The code you've tried

    return $image->select(["id", "type", "filename"])
        ->load("post:id,status,subject,body");
    

    has actually multiple issues and is mixing a few things up.

    The first issue is that you are already have an image model and try to select only a few columns, which will actually create a new query for another image. What you can do instead is select only the required columns before loading the image, e.g.:

    $image = Image::query()->select(['id', 'type', 'filename'])->find($id);
    

    The problem with this solution leads us to the second problem though: When you don't select the post_id, Laravel won't be able to load the relationship due to the missing foreign key.

    If the query statement was correct, it would actually execute two queries:

    SELECT id, type, filename FROM images
    
    SELECT id, status, suject, body FROM posts WHERE id = ?
    

    But since you've not included the post_id in the first query, it is not available as parameter for the second query and it fails to load the relationship data.


    Possible solution #1: API resources

    Use an API resource to transform the image before returning it. This is best practice as it allows you to decouple the internal data structure from the public data structure. It will also prevent unwanted API changes when the internal data structure changes (new column is added, a column is renamed, a column is deleted, ...).

    In your case, the solution could be as simple as:

    class ImageResource extends JsonResource
    {
        public function toArray(Request $request): array
        {
            return [
                'id' => $this->id,
                'type' => $this->type,
                'filename' => $this->filename,
                'post' => [
                    'id' => $this->post->id,
                    'status' => $this->post->status,
                    'subject' => $this->post->subject,
                    'body' => $this->post->body,
                ]
            ];
        }
    }
    
    $image->load('post');
    
    return new ImageResource($image);
    

    Possible solution #2: Hide fields

    You can define hidden fields in your models using the $hidden property. These fields will not be serialized to JSON:

    class Post extends Model
    {
        protected $hidden = ['private_field'];
    }
    

    Alternatively, you can use $visible to define a whitelist instead of a blacklist. Only the properties in the $visible array will be serialized:

    class Post extends Model
    {
        protected $visible = ['id', 'status', 'subject', 'body'];
    }
    

    This solution will obviously affect the entire application and may therefore be not ideal.

    Possible solution #3: Temporarily set hidden/visible properties

    Instead of permanently setting $hidden or $visible, you can also temporarily set them in your controller:

    $image->load('post');
    
    $image->setVisible(['id', 'type', 'filename']);
    $image->post->setVisible(['id', 'status', 'subject', 'body']);
    
    return $image;