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

kearney's avatar

Eager Loading throws error: Trying to get property of non-object

I've posted the question here on stack overflow, but basically, I'm having an issue where eager loading isn't able to correctly associate the keys across models (I think)

All my code is on Stack Overflow, but any help is appreciated. Here's all my code

Here's my 2 migration files

create_authors_table

<?php

# database/migrations/1970_01_01_000001_create_authors_table

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAuthorsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('authors', function (Blueprint $table) {
            $table->string('author_id');
            $table->string('first_name');
            $table->string('last_name');
            $table->timestamps();

            $table->primary('author_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('authors');
    }
}

create_books_table

<?php

# database/migrations/1970_01_01_000002_create_books_table

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBooksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->string('book_id');
            $table->string('title');
            $table->string('author_id');
            $table->timestamps();

            $table->foreign('author_id')->references('author_id')->on('authors');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('books');
    }
}

Next, here's my routes

<?php

# routes/web.php
Route::get('/books', 'BookController@index');

** My 2 models**

Author.php

<?php

# app/Models/Author.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    protected $primaryKey = 'author_id';

    public function book()
    {
        return $this->hasMany('App\Models\Book');
    }
}
<?php

# app/Models/Book.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $primaryKey = 'book_id';

    public function author()
    {
        return $this->belongsTo('App\Models\Author', 'author_id');
    }
}

Here's my BookController

<?php

# app/Http/Controllers/BookController.php

namespace App\Http\Controllers;

use App\Services\BookService;
use Illuminate\Http\Request;
use App\Models\Book;

class BookController extends Controller
{
    public function index()
    {
        $books = Book::with('author')->get();
        

        $results = [];
        foreach ($books as $book) {
            $result = new \stdClass();
            $result->title = $book->title;
            $result->author_first_name = $book->author->first_name;
            $result->author_last_name = $book->author->last_name;
            $results[] = $result;
        }

        dd($results);
    }
}

Here's some test data to fill the database with:

insert into authors (author_id, first_name, last_name, created_at) 
values 
(
  'ceefb81f-48bb-ff07-1002-27fd8b7272b4', 'Charles', 'Coal', '2018-02-11 00:00:00'
);

INSERT INTO books (book_id, title, author_id, created_at)
VALUES 
(
  'a4e58f2a-36f5-f2fb-960c-c60e2689b337', 'Killing Coal', 
  'ceefb81f-48bb-ff07-1002-27fd8b7272b4',
  '2018-02-11 00:00:00'
),
(
  'a4e58f2a-36f5-f2fb-960c-c60e2689b338', 'Cleaning Coal', 
  'ceefb81f-48bb-ff07-1002-27fd8b7272b4',
  '2018-02-11 00:00:01'
),
(
  'a4e58f2a-36f5-f2fb-960c-c60e2689b339', 'Using Coal', 
  'ceefb81f-48bb-ff07-1002-27fd8b7272b4',
  '2018-02-11 00:00:02'
);

I've turned on logging in MySQL and can see that 2 queries are being run

# turn on logging by running the following in MySQL
SET GLOBAL log_output = "FILE";
SET GLOBAL general_log_file = "/tmp/mysql.logfile.log";
SET GLOBAL general_log = 'ON';

And here's the output of the MySQL log file

2018-02-11T18:52:21.426064Z   262 Prepare   select * from `books`
2018-02-11T18:52:21.426348Z   262 Execute   select * from `books`
2018-02-11T18:52:21.426539Z   262 Close stmt    
2018-02-11T18:52:21.430560Z   262 Prepare   select * from `authors` where `authors`.`author_id` in (?)
2018-02-11T18:52:21.430689Z   262 Execute   select * from `authors` where `authors`.`author_id` in ('ceefb81f-48bb-ff07-1002-27fd8b7272b4')

Any help is appreciated

0 likes
11 replies
Snapey's avatar

change these two lines and report back

            $result->author_first_name = $book->author->first_name ?? 'missing';
            $result->author_last_name = $book->author->last_name ?? 'missing';
Snapey's avatar
Snapey
Best Answer
Level 122

ok, try this instead

add public $incrementing=false; into models using non-incrementing primary keys

skliche's avatar

Just so you know why this happens:

By default the primary key is auto-incrementing, so it is treated as an integer. Therefor an automatic cast to an int is automatically set up for the primary key. This automatic cast is performed whenever this attribute's value is retrieved from the model.

When Laravel tries to match the author to the book it tries to retrieve the attribute value for author_id from the successfully loaded record. Since a cast is set up, it tries to cast your string to an int which returns 0 and Laravel is unable to match the 0 to the actual ID.

The fun part is that Laravel successfully loaded the author but was unable to connect it to the book.

As @Snapey already mentioned, the solution is to set $incrementing to false. Now Laravel won't treat your primary key as an int. This means the cast is not set up and when Laravel retrieves the attribute value for the key the real value (string) is returned instead of 0 and everyone is happy.

Have a look at getAttributeValue() in /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:

if ($this->hasCast($key)) {
        return $this->castAttribute($key, $value);
}

... and getCasts() in the same file:

if ($this->getIncrementing()) {
        return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
}

The default for $keyType is int (in Model.php).

Why such a long story? There are a lot of things going on behind the scenes. Whenever you don't stick to the framework's assumptions (like using non-numeric keys) you better know what the consequences are or you are left wondering what the hell is going on.

1 like
kearney's avatar

@Snapey Thanks a million! That was the issue

So, literally, all I did was add public $incrementing = false; to my models (like so)

<?php
# app/Models/Book.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $primaryKey = 'book_id';
    public $incrementing = false;

    public function author()
    {
        return $this->belongsTo('App\Models\Author', 'author_id');
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    protected $primaryKey = 'author_id';
    public $incrementing  = false;

    public function book()
    {
        return $this->hasMany('App\Models\Book');
    }
}

That was it, nothing else

kearney's avatar

@skliche Thanks for explaining. I'm a NodeJS developer who is trying to get back into PHP.

One of the things that us API developers tend to do is not use integer keys, but instead use GUIDs. I can write SQL all day - stored procedures, LEFT JOIN, RIGHT JOIN, INNER JOIN, NATURAL JOIN, but, I can't seem to use a basic ORM :'-(

LOL! Thanks for all the tidbits of info - this has definitely helped me

1 like
skliche's avatar

Now you know how I felt when I used GUIDs as keys in Laravel the first time and wondered what had happened to my relations ...

Snapey's avatar

By the way, the trick in the first reply is also worth considering. Suppose you have a book with no author, your code will blow up with an error if you try to access the property of a null object.

The ?? allows you to return a default value incase the relationship is not present.

kearney's avatar

@Snapey well, I have an API and one of the things it checks before inserting a record into the books table is to verify that the GUID passed in for the author is valid. So, I know that the book will always have an author. The second reason I linked the 2 tables together with a foreign key was to prevent an author from being deleted while still tied to a book

But. you're right, I should be validating all returned data and if something should happen, display something nice like, "missing" instead of giving the user a nice stack trace

Snapey's avatar

Well hopefully your project will extend beyond books and authors so it may come in handy yet!

Glad you got it sorted.

kearney's avatar

@Snapey I'm creating a simple portfolio project. I'm trying to learn Laravel, and so was just using the example code from Laravel.com. I also wanted to show my own programming style, hence the refusal to use integer primary keys

Please or to participate in this conversation.