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

onairmarc's avatar

Route Model Binding Misbehaves on PHP 8.1.18

Hello Everyone,

I know the simplest answer to this question is to run the exact same version of PHP locally that I would in staging, but unfortunately that is currently not possible due to third-party vendor constraints. Locally, I am running PHP 8.1.22 and the highest version the third-party vendor currently runs PHP 8.1.18.

Here's the issue:

When I visit route /crm/contacts/23 locally, everything functions as normal, but when I visit the same route in staging, I get an Ignition Error Page saying "Attempt to read property "first_name" on null"

Route

Route::prefix('/crm')->group(function () {
        Route::get('/', function () {
            return redirect(route('crm.contacts.index'));
        });
        Route::prefix('/contacts')->controller(ContactController::class)->group(function () {
            Route::get('/', 'index')->name('crm.contacts.index');
            Route::post('/', 'store')->name('crm.contacts.store');
            Route::get('create', 'create')->name('crm.contacts.create');
            Route::get('/{id}', 'show')->name('crm.contacts.show');
            Route::get('/{id}/edit', 'edit')->name('crm.contacts.edit');
            Route::patch('/{id}/edit', 'update')->name('crm.contacts.update');
        });
    });
// More Routes for Other Things Live Here
});

Contact Controller Show Method:

public function show($id)
    {
        $contact = Contact::find($id);

        return view('crm.contacts.show', [
            'contact' => $contact,
        ]);
    }

The Question:

Is there a way that I can run the exact same version locally that staging uses? I'm using Valet for my local environment. Or, is there something that I can do in my code to mitigate this issue in staging?

Thanks!

0 likes
33 replies
LaryAI's avatar
Level 58

The issue you're experiencing is likely due to the difference in PHP versions between your local environment (PHP 8.1.22) and the staging environment (PHP 8.1.18). One possible solution is to update your local environment to match the PHP version used in staging.

If updating your local PHP version is not an option, you can modify your code to handle the case where the contact with the given ID is not found. Currently, your show method assumes that the find method will always return a valid contact object. However, in some cases, it may return null, resulting in the "Attempt to read property 'first_name' on null" error.

To mitigate this issue, you can use the findOrFail method instead of find in your show method. This method will throw a ModelNotFoundException if the contact with the given ID is not found. You can then catch this exception and handle it appropriately, such as by displaying a custom error message or redirecting the user.

Here's an example of how you can modify your show method:

use Illuminate\Database\Eloquent\ModelNotFoundException;

public function show($id)
{
    try {
        $contact = Contact::findOrFail($id);
    } catch (ModelNotFoundException $e) {
        // Handle the case where the contact is not found
        // For example, you can display a custom error message or redirect the user
        return response('Contact not found', 404);
    }

    return view('crm.contacts.show', [
        'contact' => $contact,
    ]);
}

By using findOrFail, you ensure that an exception is thrown if the contact is not found, allowing you to handle the error gracefully.

Remember to import the ModelNotFoundException class at the top of your controller file:

use Illuminate\Database\Eloquent\ModelNotFoundException;

This modification should help mitigate the issue in your staging environment. However, it's still recommended to keep your local environment in sync with the staging environment to avoid any potential compatibility issues.

onairmarc's avatar

The reply from our AI friend @laryai allows me to catch the exception but still does not allow me to display the view properly. The question still stands.

JussiMannisto's avatar

This has nothing to do with PHP versions. The Contact with this id just doesn't exist in your staging DB, so the $contact variable you're passing to the view is null. You're then trying to read its first_name property in the view which causes this error.

JussiMannisto's avatar

Also, this is not route model binding. You're just using an id parameter in the route.

onairmarc's avatar

I'm sorry, I did forget to mention that I have verified that the ID does exist in both databases.

onairmarc's avatar

@JussiMannisto

App\Models\Crm\Contact {#1853 ▼ // app/Http/Controllers/Crm/ContactController.php:67
  #connection: "mysql"
  #table: "contacts"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: array:1 [▶]
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #escapeWhenCastingToString: false
  #attributes: array:16 [▶]
  #original: array:16 [▶]
  #changes: []
  #casts: array:1 [▶]
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: array:1 [▶]
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: []
  #visible: []
  +fillable: array:10 [▶]
  #guarded: array:1 [▶]
  #forceDeleting: false
onairmarc's avatar

@JussiMannisto

#attributes: array:16 [▼
    "id" => 23
    "tenant_id" => 1
    "first_name" => "Marc"
    "last_name" => "Beinder"
    "company_id" => null
    "email" => "[email protected]"
    "key_contact" => 1
    "key_reason_id" => 1
    "lifecycle_stage_id" => 1
    "crm_classification_id" => 1
    "dq_reason_id" => null
    "lead_source_id" => 1
    "crm_type_id" => 1
    "created_at" => "2023-07-24 20:37:24"
    "updated_at" => "2023-08-05 16:33:17"
    "deleted_at" => null
JussiMannisto's avatar

@onairmarc Ok, so the $contact exists. But it is null when you're trying to read its first_name in the view. You have to find where that error occurs and determine why that is.

I can try to help if you show where in your view that error occurs.

onairmarc's avatar

@JussiMannisto Thanks! Here's the view file:

<x-layout>
    <div class="px-4 sm:px-6 lg:px-8">
        <div class="sm:flex sm:items-center">
            <div class="sm:flex-auto">
                <x-heading.page>{{ $contact->first_name }} {{ $contact->last_name }}</x-heading.page>
            </div>
            <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
                <a href="{{ route('crm.contacts.edit', $contact->id) }}">
                    <x-button.primary>Edit {{ $contact->first_name }} {{ $contact->last_name }}</x-button.primary>
                </a>
            </div>
        </div>
        <div class="grid grid-rows-1 grid-flow-col gap-4 mt-4 justify-start">
            <div class="text-xs">
                {{ $contact->email }}
            </div>
            @isset($contact->company->name)
                |
                <div class="text-xs">
                    {{ $contact->company->name ?? 'Acme Inc.' }}
                </div>
            @endisset
            @isset($contact->crmClassification->name)
                |
                <div class="text-xs">
                    <strong>Contact Class: </strong>
                    {{ $contact->crmClassification->name ?? 'Silver' }}
                </div>
            @endisset
            @isset($contact->leadSource->name)
                |
                <div class="text-xs">
                    <strong>Lead Source: </strong>
                    {{ $contact->leadSource->name ?? 'Silver' }}
                </div>
            @endisset
            @isset($contact->lifecycleStage->name)
                |
                <div class="text-xs">
                    <strong>Lifecycle Stage: </strong>
                    {{ $contact->lifecycleStage->name ?? 'Silver' }}
                </div>
            @endisset
        </div>
    </div>

</x-layout>
onairmarc's avatar

@JussiMannisto Locally I get the following:

App\Models\Crm\Contact {#1853 ▼ // resources/views/crm/contacts/show.blade.php
  #connection: "mysql"
  #table: "contacts"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: array:1 [▶]
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #escapeWhenCastingToString: false
  #attributes: array:16 [▶]
  #original: array:16 [▶]
  #changes: []
  #casts: array:1 [▶]
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: array:1 [▶]
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: []
  #visible: []
  +fillable: array:10 [▶]
  #guarded: array:1 [▶]
  #forceDeleting: false
}
  #attributes: array:16 [▼
    "id" => 23
    "tenant_id" => 1
    "first_name" => "Marc"
    "last_name" => "Beinder"
    "company_id" => null
    "email" => "[email protected]"
    "key_contact" => 1
    "key_reason_id" => 1
    "lifecycle_stage_id" => 1
    "crm_classification_id" => 1
    "dq_reason_id" => null
    "lead_source_id" => 1
    "crm_type_id" => 1
    "created_at" => "2023-07-24 20:37:24"
    "updated_at" => "2023-08-05 16:33:17"
    "deleted_at" => null
  ]
JussiMannisto's avatar

@onairmarc You said it works locally, but what about staging? That's where you're getting the error.

Did you also run the earlier dd($contact) locally?

onairmarc's avatar

@JussiMannisto I just promoted this code to staging, and I get the boilerplate Laravel 404 page.

Here is the current show method:

public function show($id)
    {
        $contact = Contact::findOrFail($id);
        return view('crm.contacts.show', [
            'contact' => $contact,
        ]);
    }
JussiMannisto's avatar

@onairmarc So the model doesn't exists. Either there's no row matching that ID, or the model has been soft-deleted.

JussiMannisto's avatar

@onairmarc Wait, how? You said that local and staging are running different PHP versions so they are clearly separate environments. Where is the DB located?

onairmarc's avatar

@JussiMannisto The DB is on Digital Ocean. Being a solo dev, I commonly have Local and Staging use the same DB at the beginning of new projects while I'm setting everything up.

JussiMannisto's avatar

@onairmarc Well Laravel tells you that the model doesn't exists. I'd check the environment configuration to see if you're actually connected to the same DB.

You can place this line at the start of your controller to see the actual contents of your contacts table in your staging environment:

dd(Contact::all());

onairmarc's avatar

@jussimannisto

Local

Illuminate\Database\Eloquent\Collection {#1845 ▼ // app/Http/Controllers/Crm/ContactController.php:65
  #items: array:2 [▼
    0 => App\Models\Crm\Contact {#1858 ▼
      #connection: "mysql"
      #table: "contacts"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: array:1 [▶]
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:16 [▼
        "id" => 22
        "tenant_id" => 1
        "first_name" => "Marc"
        "last_name" => "Beinder"
        "company_id" => null
        "email" => "[email protected]"
        "key_contact" => 0
        "key_reason_id" => null
        "lifecycle_stage_id" => null
        "crm_classification_id" => null
        "dq_reason_id" => null
        "lead_source_id" => null
        "crm_type_id" => null
        "created_at" => "2023-07-24 19:45:31"
        "updated_at" => "2023-07-24 19:45:31"
        "deleted_at" => null
      ]
      #original: array:16 [▶]
      #changes: []
      #casts: array:1 [▶]
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      +fillable: array:10 [▶]
      #guarded: array:1 [▶]
      #forceDeleting: false
    }
    1 => App\Models\Crm\Contact {#1856 ▼
      #connection: "mysql"
      #table: "contacts"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: array:1 [▶]
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:16 [▼
        "id" => 23
        "tenant_id" => 1
        "first_name" => "Marc"
        "last_name" => "Beinder"
        "company_id" => null
        "email" => "[email protected]"
        "key_contact" => 1
        "key_reason_id" => 1
        "lifecycle_stage_id" => 1
        "crm_classification_id" => 1
        "dq_reason_id" => null
        "lead_source_id" => 1
        "crm_type_id" => 1
        "created_at" => "2023-07-24 20:37:24"
        "updated_at" => "2023-08-05 16:33:17"
        "deleted_at" => null
      ]
      #original: array:16 [▶]
      #changes: []
      #casts: array:1 [▶]
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      +fillable: array:10 [▶]
      #guarded: array:1 [▶]
      #forceDeleting: false
    }
  ]
  #escapeWhenCastingToString: false
}

Staging

Illuminate\Database\Eloquent\Collection {#1754 ▼ // app/Http/Controllers/Crm/ContactController.php:65
  #items: array:2 [▼
    0 => App\Models\Crm\Contact {#1767 ▼
      #connection: "mysql"
      #table: "contacts"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: array:1 [▶]
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:16 [▼
        "id" => 22
        "tenant_id" => 1
        "first_name" => "Marc"
        "last_name" => "Beinder"
        "company_id" => null
        "email" => "[email protected]"
        "key_contact" => 0
        "key_reason_id" => null
        "lifecycle_stage_id" => null
        "crm_classification_id" => null
        "dq_reason_id" => null
        "lead_source_id" => null
        "crm_type_id" => null
        "created_at" => "2023-07-24 19:45:31"
        "updated_at" => "2023-07-24 19:45:31"
        "deleted_at" => null
      ]
      #original: array:16 [▶]
      #changes: []
      #casts: array:1 [▶]
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      +fillable: array:10 [▶]
      #guarded: array:1 [▶]
      #forceDeleting: false
    }
    1 => App\Models\Crm\Contact {#1765 ▼
      #connection: "mysql"
      #table: "contacts"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: array:1 [▶]
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:16 [▼
        "id" => 23
        "tenant_id" => 1
        "first_name" => "Marc"
        "last_name" => "Beinder"
        "company_id" => null
        "email" => "[email protected]"
        "key_contact" => 1
        "key_reason_id" => 1
        "lifecycle_stage_id" => 1
        "crm_classification_id" => 1
        "dq_reason_id" => null
        "lead_source_id" => 1
        "crm_type_id" => 1
        "created_at" => "2023-07-24 20:37:24"
        "updated_at" => "2023-08-05 16:33:17"
        "deleted_at" => null
      ]
      #original: array:16 [▶]
      #changes: []
      #casts: array:1 [▶]
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      +fillable: array:10 [▶]
      #guarded: array:1 [▶]
      #forceDeleting: false
    }
  ]
  #escapeWhenCastingToString: false
}
JussiMannisto's avatar

@onairmarc This is really weird, it does show the model this way. Let's try to debug this in steps. Replace the previous dd-call with these lines:

dump($id);
dd(Contact::find($id));
onairmarc's avatar

@JussiMannisto I think we found it!

Local

"23" // app/Http/Controllers/Crm/ContactController.php:65
App\Models\Crm\Contact {#1857 ▼ // app/Http/Controllers/Crm/ContactController.php:66
  #connection: "mysql"
  #table: "contacts"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: array:1 [▶]
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #escapeWhenCastingToString: false
  #attributes: array:16 [▼
    "id" => 23
    "tenant_id" => 1
    "first_name" => "Marc"
    "last_name" => "Beinder"
    "company_id" => null
    "email" => "[email protected]"
    "key_contact" => 1
    "key_reason_id" => 1
    "lifecycle_stage_id" => 1
    "crm_classification_id" => 1
    "dq_reason_id" => null
    "lead_source_id" => 1
    "crm_type_id" => 1
    "created_at" => "2023-07-24 20:37:24"
    "updated_at" => "2023-08-05 16:33:17"
    "deleted_at" => null
  ]
  #original: array:16 [▶]
  #changes: []
  #casts: array:1 [▶]
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: array:1 [▶]
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: []
  #visible: []
  +fillable: array:10 [▶]
  #guarded: array:1 [▶]
  #forceDeleting: false
}

Staging

"servicepoint" // app/Http/Controllers/Crm/ContactController.php:65
null // app/Http/Controllers/Crm/ContactController.php:66
onairmarc's avatar

@jussimannisto I think it's getting "servicepoint" from the Domain Router:

Route::domain('{tenant_domain}.{cluster}.servicepointcloud.com')->group(function () {
    require __DIR__ . '/common.php';
});
JussiMannisto's avatar
Level 50

@onairmarc Ah. So you have multiple parameters in your route but show() uses only one. That explains it.

onairmarc's avatar

@JussiMannisto Yeah, that didn't even cross my mind 🤦🏻‍♂️. Should adding two parameters in front of $id help to resolve this?

onairmarc's avatar

@JussiMannisto Booya!! That worked! Thank you so much for your help! Love when the fix ends up being super simple!

JussiMannisto's avatar

You said earlier that you verified that the ID exists in staging. But it looks like your model supports soft-deletion. Did you also check that the model's deleted_at timestamp is null?

Snapey's avatar

when you get the error, please look for the 'Share" link on the error page. Send us the link. Almost certain the problem is not where you think

Please or to participate in this conversation.