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