'Make an Eloquent immutable model magically record edits in another model/table and retrieve them

Is it possible to have a models such as Cars and Overrides where I retain the original record in Cars, but substitute any update in Overrides (signified by col/val in overrides table).

Ideally this would be global anytime the record is retrieved.

So upon record entry to cars it would be:

id color make model
1 blue ford f150

But after creating an override:

The original record would remain car:blue,ford,f150 in cars but any columns that have a corresponding override are always displayed out when the record is fetched.

Immutable original record in cars

id color make model
1 blue ford f150

Its overrides:

id model_id column_override value_override
1 1 color red
2 1 make chevy

Virtually retrieved as:

id color make model
1 red chevy f150

Essentially

  • Keeping the original record the same.
  • Recording the column level change history.
  • But always showing the record with the latest override when requested via Laravel.

(ie). no changes show in cars. This would be different than Auditing since you would always want the overrides to show up.



Solution 1:[1]

Even though we discussed that you prefer to make some of Laravel's magic happen, I'll try to make my suggested approche clearer.

The foundation of the solution

Tables structure

cars table

id color ... make model created_at

overrides table

car_id id color ... make model created_at

Eloquent relationships

class Cars extends Model{
  // ...

  public function overrides()
  {
    return $this->hasMany(Overrides::class);
  }

  public function latestOverride()
  {
    return $this->hasOne(Overrides::class)->latest();
  }

  // ...
}

A use case scenario

  • First care insertion
id color make model created_at
1 blue ford f150 now()

The 1st car row from now on is immutable, no code will be made to change it.

car_id id color make model created_at
  • First override is the user changing the color to red
car_id id color make model created_at
1 1 red ford f150 now()
  • Second override is the user changing the color to brown and make to chevy
car_id id color make model created_at
1 1 red ford f150 TIMESTAMP
1 2 brown ford chevy now()

Execution

Car creation

The creation of the Car is straight forward Car::create($data)

Overriding

public function storeOverride(StoreOverrideRequest $request ,Car $car){
  
  $latest_override = $car->latestOverride()->first();

  // Compare the request using "OR" with Override first (if the car have at least one override).
  // The Car also this request may be 'blue'. But the latest override is 'red'.
  // So this request count as a new different Override.

  // The following logic is applied to all properties
  // Stop the check and create at first diff
 if($request->color !== $latest_override->color || $request->color !== $car->color){
    Override::creat($request->validated());
  }
}

Retrieving

public function showCar(Car $car){
  return [
    'car' => $car,
    'latestOverride' => $car->latestOverride()->first(),
  ];
}

Benefits

  1. No hidden logic.
  2. Easy versionning. Your user can save overrides and navigate forward and backward through his history of changes.
  3. Guaranteed few exchanges with the database.

A column/value override approche will limit you on retrieval. you will be obliged to do something like $car->overrides and then loop through the Collection to replace the changes on $car.

Notes

  • Using Eloquent getters to query the latest value from the other model would result in a cumbersome code and a huge amount of fetching queries.
  • My point of view is that magic may get too complicated and hurt your code evolution and debugging (remember Doctor Strange causing the universe of madness XD)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1