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

richardhulbert's avatar

hasOne Eloquent problem

In my application I have special Eloquent Models called RevisionModels. Unlike conventional eloquent models, which update a row when a user makes a change, a RevisionModel row is cloned and any new data is stored in a new row in the database.

Rows are bound together using a field called prime.

The RevisionModel

The RevisionModels are subclassed into models that we can use in the application. They have three fields that the subclass inherits:

  • prime used to group rows together.
  • branch_id _ used to separate rows into branches so that users can create other versions of the application/site_
  • user_id is used to attribute changes to a user

So a sub model called Page might look like this:

class Page extends RevisionsModel
{
    use SoftDeletes, HasFactory, TableSearchTrait;

    protected $fillable = [
        'title',
        'slug',
        'blocks',
        'status',
        'template_id',
        'category_id'
    ];

    protected $casts = [
        'blocks' => 'array',
        'meta' => 'array',
    ];

    public function __construct(array $attributes = []) {
        $this->fillable = parent::mergeRevisionFillable($this->fillable); //add the revision model specific fields
        parent::__construct(self::class, $attributes);
    }
    
    ...
}

This approach means that the application has a record of every change, with attribution and branches let users make versions of a Page (RevisionModel) that won't interfere with the main branch of the site.

So for instance when a users visits a page called About They are looking at:

//quasi code
Page::where('prime','=',2)->where('branch_id','=',$users->branch_id)->latest();

Or in English; the last Page row where the branch is x and the prime is y.

Relationships

Relationships work where there is a relationship with a conventional models for instance

    /**
     *This is the owner of the revision
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function owner() {
        return $this->hasOne(User::class, 'id', 'user_id');
    }

The Problem

But relationships with another RevisionModel (in this instance Template are more complex:

 /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function template() {
        return $this->hasOne(Template::class,'prime','template_id')->latestOfMany();
    }

This almost works - it is saying; Give me the last Template where Template's prime is the template_id of this Page row.

Fine if there is only one branch However if I edit the template on another branch I don't want to see that template row. In other words:

Give me the last template where the prime = the pages template id and also the template's branch_id = the pages branch_id

if I do this:

 /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function template() {
    $sub = Template::selectRaw('max(id) as max_id ')->where('branch_id','=',$this->branch_id);
     return  Template::fromSub($sub, 'sub')->join('templates', 'templates.id', '=', 'sub.max_id')->first();
    }

It I can call

Page::find(26)->template();

And it will give me the template however I cannot do this:

Page::find(26)->load('template');

it returns Call to a member function addEagerConstraints() on null.

This is because the template() returns a collection and not a hasOne In SQL terms I need something like this:

SELECT P1.title, P1.id as page_id, P1.slug,T2.* 
FROM (SELECT max(P.id) as max_id From Pages P where P.branch_id=1 GROUP BY P.prime) sub
INNER JOIN pages P1 ON sub.max_id= P1.id
INNER JOIN (SELECT max(T.id) as max_id, T.prime From templates T where T.branch_id=2 GROUP BY T.prime) T1 ON T1.prime = P1.template_id
INNER JOIN templates T2 on T2.id = T1.max_id

I guess my problem is how to write a custom hasOne function and how that might be implemented

0 likes
1 reply
richardhulbert's avatar

So i have rejected the AI suggestion as it returns null

   /**
     * Get the latest template for the current page's template_id and branch_id.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function template() {
        return $this->hasOne(Template::class, 'prime', 'template_id')
            ->where('branch_id', $this->branch_id)
            ->latestOfMany();
    }

You would think this would work but I think that the problem is the use of latestOfMany() which forms a subquery that is used in the search using the max_id or created_at. Really I need to override latestOfMany to include a where branch_id = ? Or give up and refactor without relying on eager loading.

Please or to participate in this conversation.