Call to a member function addEagerConstraints() on null because of a conditional relationship

Viewed 663

I have to deal with a Laravel 7 application that has a sub-optimal database design, leading to the error mentioned in the title.

The database looks like this:

mains
- id
- sub_type

subs_a
- id
- main_id

subs_b
- id
- main_id

Then I have a class Main with method sub:

public function sub()
{
    switch($this->sub_type) {
        case 'a':
            return $this->hasOne('SubTypeA');
            break;
        case 'b':
            return $this->hasOne('SubTypeB');
            break;
        default:
            return null;
    }
}

This code works in 99% of all cases, but Laravel sometimes loads an empty instance of Main and then tries to load the relations. That doesn't work, because the default of method sub is null.

Restructuring the database is on the to-do list, but that isn't of any help right now.

What option do I have to stop Laravel from trying to load the sub relation on an empty object?

3 Answers

i know it's some kind of expected, but have tried to return an empty relation?

public function sub()
{
    switch($this->sub_type) {
        case 'a':
            return $this->hasOne('SubTypeA');
            break;
        case 'b':
            return $this->hasOne('SubTypeB');
            break;
        default:
            return $this->newQuery();  // or newQueryWithoutScopes()
    }
}

thank to this answer. it should prevent the error of addEagerConstraints() on null.

Few things I would like to point out about it is, your relationship should not be conditional, after making the default relationship what you want to make you can define scope instead and manage stuffs or use withDefault return something in failure case.

About the error calling a member function on null:- below is another example

<?php
class MyClass{
    function bar(){
        echo "something";
    }
}

class MyAnotherClass{
    function foo(){
        if (1>2) {
            $obj = new MyClass();
            $obj->x = $x;
            $obj->y = $y;
            $obj->save();
            return $obj;
        } else {
            return null;
        }
    }
}

$myAnotherObj = new MyAnotherClass();
$myClass = $myAnotherObj->foo();
$myClass->bar()
?>

Instead of doing it, I would prefer throwing an exception and handling it, so that I will get specific reason for failure, in Laravel rescue helper function you can choose to use.

<?php
class MyClass{
    function bar(){
        echo "something";
    }
}

class MyAnotherClass{
    function foo(){
        if (1>2) {
            $obj = new MyClass();
            $obj->x = $x;
            $obj->y = $y;
            $obj->save();
            return $obj;
        } else {
            throw new Exception("could not create my class object", 100); // Better to create custom exception class here
        }
    }
}

$myAnotherObj = new MyAnotherClass();
try {
    $myClass = $myAnotherObj->foo();
    $myClass->bar();
} catch(Exception $e) {
    echo $e->getMessage();
}
?>

if for me the data is not so important I will think of creating an empty object

<?php
    class MyClass{
        function bar(){
            echo "something";
        }
    }
    
    class MyAnotherClass{
        function foo(){
            $obj = new MyClass();
            if (1>2) {
                $obj->x = $x;
                $obj->y = $y;
                $obj->save();  
            } 
            return $obj;
        }
    }
    
    $myAnotherObj = new MyAnotherClass();
    $myClass = $myAnotherObj->foo();
    $myClass->bar()
  ?>

but if you are making an operation with that object properties, then the properties will be null instead of object, so based on the how discipline you will be when using it, you can take decision.

How I would have like to handled your situation?

Exception class

<?php

namespace App\Exceptions;

use Exception;

class SubTypeNotFound extends Exception {
    public function report()
    {
        \Log::debug('Could not find this subtype');
    }
}

?>

Model class

<?php
class Mains extends Model
{
    public function subA()
    {
        return $this->hasOne(SubTypeA::class);
    }    

    public function subB()
    {
        return $this->hasOne(SubTypeB::class);
    }

    public function scopeSub($query, $type)
    {
        return $query
          ->when($type === 'a',function($q){
              return $q->with('subA');
         })
         ->when($type === 'b',function($q){
              return $q->with('subB');
         }),function($q){
             throw SubTypeNotFound();
         });
    }
}
?>

while retrieving it

try {
    $sub = Mains::sub('a')->get();
} catch(SubTypeNotFound $e) {
    return $e->getMessage();
}

If you have $this->sub_type you can avoid using the type parameter.

You can simply put if condition before switch case.

public function sub()
{
    if($this->sub_type){
        switch($this->sub_type) {
            case 'a':
                return $this->hasOne('SubTypeA');
                break;
            case 'b':
                return $this->hasOne('SubTypeB');
                break;
            default:
                return null;
        }
    }else{
        return null
    }       
}
Related