Most likely, most programmers working with modern web frameworks implementing the MVC scheme encountered such a small difficulty: caching the fragment of View.
Good frameworks offer tools for full page caching, fragmentary, or action caching. I recently watched the
90th edition of the
Railscasts podcast, which was specifically dedicated to fragmentary caching in Ruby on Rails and a respected author solved the problem, as it seemed to me, not optimally.
I will describe the situation.
We are in the page template and want to cache part of it, for example, a list of new products. So far so good, we use convenient means built into the framework and surround the block in two or three lines - hurray, it is cached. But - chu !, the controller knows nothing about this and continues to do its job of preparing the data for the View. Naturally, after all, checking for the presence of a cache is already done from a template, and by that time the controller worked.
The podcast author shows an ugly solution - transferring the code to prepare the data into a template and then, of course, sweeps it away as “ugly”. What it offers is to transfer this code to the model. That is, a special method is created in the product model that selects new products, and this method is called from the template.
This is better than the first option, but still not good enough, since the model has to implement things that may be needed in one place only, and when changing the interface of the site it may be unnecessary and most likely remain in the code just to hang out.
')
My decision
I work with my PHP framework, and I will write an example in PHP, but the solution is simple and implemented in any script language.
view.php :
<code>
...
<? if! (cacher :: start ('Cache_Name')) {?>
<ul>
<? foreach ($ latest as $ item) {?>
<li> <? = $ item-> name ();?>: <? = $ item-> price ();?> </ li>
<? }?>
</ ul>
<? cacher :: end (); }?>
...
</ code>
controller.php :
<code>
...
$ latest = new model_collection ('product');
$ latest-> load_by ($ condition, $ order, $ limit);
$ this-> export ('latest', $ latest);
...
</ code>
The load_by (...) method performs one or more database queries and forms a set of models of the Product class. That is, resources are spent on the request, and even the memory on the model instances.
It would be nice to somehow remember what we want to do, and do it only if there is no cache.
Let's write it.
utils.php :
<code>
...
class prepared extends stdClass // tiny class for storing the prepared operation
{
// I will not complicate an example with getters and setters
public $ obj, $ method, $ args;
}
class utils
{
...
public static function prepare ($ obj, $ method, $ args = null)
{
$ res = new prepared ();
// method takes an unlimited number of parameters
$ args = func_get_args ();
$ res-> obj = array_shift ($ args);
$ res-> method = array_shift ($ args);
// remember all other parameters
$ res-> args = $ args;
return $ res;
}
public static function run ($ prepared)
{
// insurance: the template should not think whether the real data came from, or the workpiece
if (! ($ prepared instance_of prepared)) return $ prepared;
return call_user_func_array (array ($ prepared-> obj, $ prepared-> method), $ prepared-> args);
}
...
}
...
</ code>
The
run()
method is simplified, at the
prompt davojan .
Using
controller.php :
<code>
...
$ latest = new model_collection ('product');
// do not ship anything immediately
// $ latest-> load_by ($ condition, $ order, $ limit);
// remember what we want to do in the variable for the template itself
$ latest = utils :: prepare ($ latest, 'load_by', $ condition, $ order, $ limit);
$ this-> export ('latest', $ latest);
...
</ code>
view.php :
<code>
...
<? if! (cacher :: start ('Cache_Name')) {?>
<?
// only here we execute the planned, and the template does not need to know what exactly is being done
$ latest = utils :: run ($ latest);
?>
<ul>
<? foreach ($ latest as $ item) {?>
<li> <? = $ item-> name ();?>: <? = $ item-> price ();?> </ li>
<? }?>
</ ul>
<? cacher :: end (); }?>
...
</ code>
Suppose, in your framework, the goods would have to be loaded by the static method. Please do the following:
controller.php :
<code>
...
// do not ship anything immediately
// $ latest = Product :: get_latest (...);
// remember what we want to do in the variable for the template itself
$ latest = utils :: prepare ('Product', 'get_latest', ...);
$ this-> export ('latest', $ latest);
...
</ code>
In the template, even nothing needs to be changed.
I use this method in many places and until he let me down. Disadvantage: it is not yet possible to prepare a set of operations, but in such perverted cases it is already possible to add a method somewhere.
I would be happy to comment.
Update
In the comments, they indicate to me the presence of components and the ability to cache them entirely. I have to explain - the note is not about that. I will give another example from real life.
The page with the list of news, the action 'index' of the controller 'news'.
<code>
...
$ news = new model_collection ('news'); // or yours
$ news-> load_by ($ conditions, $ order, $ limit);
$ this-> export ('news', $ news);
...
</ code>
A template with a list of news is embedded in the layout, in which there is still a bunch of components (new products, exchange rates, etc.). Components are cached entirely, naturally. But we have to execute the “main” action, we most often cannot cached the entire page.
This is where the described approach comes in handy - the data cannot be obtained immediately, but only prepared. You can, of course, bring the output of the news directly into another action, but in this way we will almost double the number of action games, which is clearly inconvenient.
So it should be clearer.