📜 ⬆️ ⬇️

Arrays of models in MVC - tasty and hard?


The MVC paradigm largely simplifies code maintenance by separating logic and creating abstractions, but often, following the principle of Thick Model & Thin Controller (also known as Fat Model & Skinny Controller), developers have to rest on the cornerstone of using any model object, namely - in memory consumption. What is especially important when working with models that implement the ORM (or ActiveRecord pattern).
In this article I want to briefly demonstrate the standard approaches to solving this problem.


For a start, a small digression for those who do not quite understand why you need to use models if you can work directly with data from the database without unnecessary abstractions.
Actually, here are two variants of the code:
if ($row['status'] == 3) { // do something ... } 


 class Post { const AUTH_ONLY = 3; public $status; public function isAuthOnly() { return $this->status == self::AUTH_ONLY; } } if ($post->isAuthOnly()) { // do something ... } 

')
If you need to solve a problem quickly and simply - the first option is good. However, if your code will live after you, read not only by you, as well as develop and expand, then the implementation of the second option is often preferable.

The essence of the problem:
By themselves, data models are found in large numbers, as a rule, when selecting records from a database. Abstraction over database records (ORM) requires the implementation of a model with its own logic, however, creating models for each record selected is extremely resource intensive.

Most often you can hear the following recommendation:
If few records are needed, then it is possible with models, and if there are many, then it is already possible to process them with a separate query to the database with its own logic. However, with a separate query, we partially lose the layer of abstraction to the table and lose the ability to use the logic of the model.

Since an array of records of a relational database is inherently just a set of rows and fields, the first and most obvious solution is to use only one model object, and replace its parameters in the crawl loop of a sample of records.
The result is something like this:
 $post = new Post; $result = mysqli_query('SELECT * FROM posts'); while ($row = mysqli_fetch_array($result)) { $post->setAttributes($row); //     if ($post->isAuthOnly()) { // do something ... } // do something ... } 

Thus, at the cost of some load, we get all the advantages of the model without increasing memory consumption for mass sampling. Of course, provided we really need the logic of the model.

However, this solution, although transparent, is not elegant enough, since when it is re-implemented, significant duplication of code will arise. Let's call on Iterator’s strength for help:
 class PostIterator implements Iterator { private $_model; private $_result; private $_row_num = 0; private $_total_rows = 0; public function __construct(Post $model) { $this->_model = $model; } public function selectAll() { //     $this->_result = mysqli_query('SELECT * FROM posts'); $this->_total_rows = mysqli_num_rows($this->_result); } public function current () { //      mysqli_data_seek($this->_result, $this->_row_num); $data = mysqli_fetch_array($this->_result); $this->_model->setAttributes($data); return $this->_model; } public function next () { //     ++$this->_row_num; } public function key () { //    return $this->_row_num; } public function valid () { //     return ($this->_row_num < $this->_total_rows); } public function rewind () { //     mysqli_data_seek($this->_result, 0); $this->_row_num = 0; } } class Post { // ... public function getIterator() { return new PostIterator($this); } } //    $post_model = new Post; $post_iterator = $post_model->getIterator(); $post_iterator->selectAll(); foreach ($post_iterator as $post) { if ($post->isAuthOnly()) { // do something ... } // do something ... } 

I suppose additional comments are unnecessary here. As a result, we obtained an object that allows processing large enough objects of records using model logic, while maintaining a small amount of memory consumed and sufficient “transparency”.

However, the code above has a number of flaws and bottlenecks:
1. We have only one model (object) which we pass through the pseudo-reference inside the loop, as a result, its change within the loop will affect the whole subsequent loop.
2. The transition operation based on the result of a query from the database (mysqli_data_seek) consumes its part of the resources, and if we do not need an “exceptionally correct” implementation of the iterator, then we can slightly “ease” it. Since the Traversable interface will appear in mysqli_result only in PHP 5.4, we cannot directly shift responsibility for its implementation in Iterator to mysqli_result. However, in the result resource itself, this interface is implicitly implemented, which can be used.
3. Additionally, it is not necessary for us to create a model (Post) each time previously to get an iterator. This can be implemented through a static method.
4. Usually, in conjunction with the iterator, it is sometimes convenient to implement the ArrayAccess interface, however, you can easily implement this point yourself if necessary (mysqli_data_seek to help you).

As regards the above comments, we can get the following, slightly optimized code:
 class PostIterator implements Iterator { private $_model; private $_result; private $_row_num = 0; private $_total_rows = 0; private $_current_data = null; //      public function __construct($model) { $this->_model = $model; } public function selectAll() { $this->_result = mysqli_query('SELECT * FROM posts'); $this->_total_rows = mysqli_num_rows($this->_result); $this->_current_data = mysqli_fetch_array($this->_result); //  return $this; //  MethodChaining } public function current () { $model = clone $this->_model; //   $model->setAttributes($this->_current_data); return $model; } public function next () { ++$this->_row_num; $this->_current_data = mysqli_fetch_array($this->_result); //  } public function key () { return $this->_row_num; } public function valid () { return ($this->_row_num < $this->_total_rows); } public function rewind () { mysqli_data_seek($this->_result, 0); $this->_row_num = 0; $this->_current_data = mysqli_fetch_array($this->_result); //  } } class Post { // ... public static function getIterator() { $class_name = __CLASS__; return new PostIterator(new $class_name); } } //    $post_iterator = Post::getIterator()->selectAll(); foreach ($post_iterator as $post) { if ($post->isAuthOnly()) { // do something ... } // do something ... } 

As a result, we got a fairly simple “bicycle”, which, with proper “file completion”, will allow the models to be used more flexibly and with larger sample sizes.
In it, we got rid of the above disadvantages, however, there is a problem with using the internal iterator of the query result, which does not come up with standard use only in the foreach loop, but using the Iterator interface in more complex structures will not work (choose this approach or not for speed) - you decide, it all depends on the task).

Serious disadvantages of this approach:
1. More significant load on the computing resources of the machine. Therefore, it is correct to always have a cache layer behind such an implementation, in fact, like everywhere else.
2. Not every model logic can be used with a similar abstraction, for example, relational model relationships. If there are any, then they will have to either be “sewn up” inside the iterator, or they should look for some more optimal solution within the model itself.
3. Additional difficulties may arise if it is required to use a model with states, which will also require a separate implementation for the iterator.

PS:
All the above code is quite abstract from any specific task. It is given only to obtain a general idea of ​​the principle, which can be independently implemented in practice. And, of course, there is no limit to perfection.

Source: https://habr.com/ru/post/137860/


All Articles