Laravel supports various cache drivers out of the box. One of them is Redis. I am not going to describe Redis with it's features and advantages here, it's not the purpose of this document. The only important point in scope of this document is that Redis supports tags. Feel free to google a bit what is „cache tagging”.
I also won't explain how to install Redis. If you read this article most probably you have the stack ready. If you just want to „play with it” consider using docker:
Let's consider the following diagram:
In short, an employee may work at a company or at an enterprise. Company may belong to an enterprise, both company and enterprise may have few address records attached. The many-to-many relation is used so that you don't need to alter entity table too often, instead you add another pivots when your project is growing.
Full information about a given company is the company information itself (like name, vat number etc.), all addresses attached, all employees working at that company and if the company is associated within some enterprise.
A company with 4 users, 2 addresses and an enterprise could be tagged with the following tags (the structure is entity name + id).
To store that full company information with tags in cache we can use the following:
In case of a state change of any of the related entities we need to flush also the $fullCompanyData one (that is COMPANY_1).
The easiest, but also IMO the best way to achieve it would be to have observers registered and hooking into right eloquent models.
How to register an observer? There are quite few ways of achieving this in L4.2, my favourite is with the boot() method:
The following events are fired when doing CRUD operations and we can easily hook them: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored
The entity is passed to the hook method, but you can see a getCacheTag() method called on the entity. It comes from a trait that I've added to all entities which defines our naming / tagging conventions
So we have pretty much all we need. When any object is updated it invalidates all cache that is tagged with it. For example updating an employee with id 31 will flush cache tagged with EMPLOYEE_31, that means also the $fullCompanyData tagged with EMPLOYEE_31 will be flushed.
But when we create a new Record (no matter what entity is it) we also need to invalidate cache, don't we? So let's implement the created() hook in our GenericObserver class.
It will work OK, it will flush all cache tagged by the tag. But what if it was a new Employee and what if it was attached to an existing (and very likely cached) Company?
We have to invalidate the company's cached data, as instead of n employees working at it we have n+1 now.
Unfortunately due to the way database constraints work (we have to insert the employee first, and then insert the relation between company and employee into the many-to-many PIVOT table) and more over because of PIVOTS not firing events on attach() and detach() methods it's not so easy. When trying to hook into the employee's observer created() method one could see that calling $model→companies() return empty collection. That's exactly because this event is fired right after inserting new employee, and obviously before inserting the relation. The workaround is crucial. Remove the PIVOT and implement a full model class for the PIVOT table:
Also the observer needs to behave a bit different. First of all I want just one observer handling both PIVOTS for employees→companies and employees→enterprises. Then - it doesn't need to invalidate "itself" but the corresponding object instead.
I've implemented also the deleted() method, because the same rule is valid when deleting employees.
Still reading? That means all should be clear but where did I get the list of tags when building the $fullCompanyData? I had to implement a „helper” which iterates through all related entities and generates it. I've put into my RepoAbstract class extended by all repositories, but you can put it anyware. You may also consider to refactor it a bit to be a function.