📜 ⬆️ ⬇️

Automatic fluid interface and ArrayIterator in PHP models

This method does not claim originality, but it seems to me that it can be useful in understanding the principles of operation of such systems (see, for example, Varien_Object , written by the Magento developers, the idea was taken primarily from there) and may be useful in projects where I do not really want to connect heavy frameworks, but I need to somehow systematize the code.

It is difficult to imagine a fairly large project in which there would be no work with data models. I will say more: in my experience, about three-quarters of the entire code is creating, loading, modifying, saving or deleting records. Whether it is a user registration, the conclusion of the last ten articles or work with the admin - all this is minor work with the basic operations of the models. And, accordingly, such code should be written and read quickly and should not clog the programmer’s head with technical details: he (the programmer) should think about the logic of the application, and not about the next UPDATE request.

Instead of the preface


In my opinion, to make the code easy to read, it (besides, of course, the coding standards and the understandability of algorithms) should be as close as possible to natural language. Download this product, set this name for it, set such a price, save it. In addition, if it is possible to avoid repetition in the code, be it a piece of code or just the name of a variable (when working with one object), then it should be avoided. In my case, the “flowing interface” saved me from constantly tedious copying of the variable name.

Fluid interface


It would be logical to separate flies from cutlets and bring the “flowing interface” into a separate class, in case it needs to be used not only in models:
abstract class Core_Fluent extends ArrayObject {} 

')
Before starting to write, I decided how I want to see the final code that I will use. It turned out this:
 $instance->load($entity_id) ->setName('Foo') ->setDescription('Bar') ->setBasePrice(250) ->save(); 

At the same time, I wanted the data to be stored with keys of the form “name”, “description”, “base_price” (this would make it much easier to implement interaction with the database, and my coding standard would require it).

In order not to write the same type methods in each model, you should use “Magic Methods” ( Magic Methods ), in particular, the __call () method. You could also use the __get () and __set () methods, but I went through using the ArrayIterator .

So, the __call method, which will determine what was caused and what to do next:
 ... //     CamelCase  __ //      StackOverflow,       const PREG_CAMEL_CASE = '/(?<=[AZ])(?=[AZ][az])|(?<=[^AZ])(?=[AZ])|(?<=[A-Za-z])(?=[^A-Za-z])/'; //      protected $_data = array(); public function __call($method_name, array $arguments = array()) { //   ,    ,      //        (setBasePrice    set  BasePrice) if(!preg_match('/^(get|set|isset|unset)([A-Za-z0-9]+)$/', $method_name, $data)) { //    ,    throw new Core_Exception('Method '.get_called_class().'::'.$method_name.' was not found'); } //        (BasePrice => base_price)    $property $property = strtolower(preg_replace(self::PREG_CAMEL_CASE, '_$0', $data[2])); //   ,      switch($data[1]) { case 'get': { // $object->getBasePrice():    return $this->get($property); } break; case 'set': { // $object->setBasePrice():    return $this->set($property, $arguments[0]); } break; case 'unset': { // $object->getBasePrice():     return $this->_unset($property); } break; case 'isset': { // $object->getBasePrice(): ,       return $this->_isset($property); } break; default: { } } // ,    ,   ,    " " return $this; } ... 


Get , set , _isset, and _unset methods


The implementation of these methods is not difficult, their action is obvious from the name:
 ... public function get($code) { if($this->_isset($code)) { return $this->_data[$code]; } //     ,      NULL return NULL; } public function set($code, $value) { $this->_data[$code] = $value; return $this; } public function _unset($code) { unset($this->_data[$code]); return $this; } public function _isset($code) { return isset($this->_data[$code]); } ... 


ArrayIterator


In addition to the above-mentioned approach, I decided to add the ability to work with an object as with a normal associative (and not only, but this is another story) array: for this there is an ArrayIterator . Of course, it was more correct to call the methods described in the previous section, so that it would not have to be duplicated, but, first, there was already a need to think about backward compatibility, because there was code using these methods directly and there was quite a lot of it, but second, in my opinion, one thing is the implementation of the ArrayIterator, and the other is the implementation of the fluid interface.

 ... public function offsetExists($offset) { return $this->_isset($offset); } public function offsetUnset($offset) { return $this->_unset($offset); } public function offsetGet($offset) { return $this->get($offset); } public function offsetSet($offset, $value) { return $this->set($offset, $value); } public function getIterator() { return new Core_Fluent_Iterator($this->_data); } ... 

And, accordingly, the class Core_Fluent_Iterator :
 class Core_Fluent_Iterator extends ArrayIterator {} 


Everything. Now with any class inherited from Core_Fluent such manipulations are available:
 class Some_Class extends Core_Fluent {} $instance = new Some_Class(); $instance->set('name', 'Foo')->setDescription('Bar')->setBasePrice(32.95); echo $instance->getDescription(), PHP_EOL; // Bar echo $instance['base_price'], PHP_EOL; // 32.95 echo $instance->get('name'), PHP_EOL; // Foo // name => Foo // description => Bar // base_price => 32.95 foreach($instance as $key => $value) { echo $key, ' => ', $value, PHP_EOL; } var_dump($instance->issetBasePrice()); // true var_dump($instance->issetFinalPrice()); // false var_dump($instance->unsetBasePrice()->issetBasePrice()); // false 


Model


Now the model itself, a special case of application of the above mechanism.
 abstract class Core_Model_Abstract extends Core_Fluent {} 


First you need to add the basis for CRUD (create, load, modify and delete). The logic (work with the database, files and anything else) will be lower in the hierarchy, here you need to do only the most basic:

 ... //   ,    protected $_changed_properties = array(); // .   save()        //      ,     //     (   ) public function create() { return $this; } //  public function load($id) { $this->_changed_properties = array(); return $this; } //    public function loadFromArray(array $array = array()) { $this->_data = $array; return $this; } //  public function save() { $this->_changed_properties = array(); return $this; } //  public function remove() { return $this->unload(); } //    public function unload() { $this->_changed_properties = array(); $this->_data = array(); return $this; } //     public function toArray() { return $this->_data; } ... 

Finally, override set () by adding an array of changed properties.
 ... public function set($code, $value) { $this->_changed_properties[] = $code; return parent::set($code, $value); } ... 

Now from this class you can inherit various adapters to databases, files or APIs, from which, in turn, inherit the already final data models.

The full code of all three files under the spoiler.
Full code of all three files
Core / Fluent.php
 <?php abstract class Core_Fluent extends ArrayObject { const PREG_CAMEL_CASE = '/(?<=[AZ])(?=[AZ][az])|(?<=[^AZ])(?=[AZ])|(?<=[A-Za-z])(?=[^A-Za-z])/'; protected $_data = array(); public function __call($method_name, array $arguments = array()) { if(!preg_match('/^(get|set|isset|unset)([A-Za-z0-9]+)$/', $method_name, $data)) { throw new Core_Exception('Method '.get_called_class().'::'.$method_name.' was not found'); } $property = strtolower(preg_replace(self::PREG_CAMEL_CASE, '_$0', $data[2])); switch($data[1]) { case 'get': { return $this->get($property); } break; case 'set': { return $this->set($property, $arguments[0]); } break; case 'unset': { return $this->_unset($property); } break; case 'isset': { return $this->_isset($property); } break; default: { } } return $this; } public function get($code) { if($this->_isset($code)) { return $this->_data[$code]; } return NULL; } public function set($code, $value) { $this->_data[$code] = $value; return $this; } public function _unset($code) { unset($this->_data[$code]); return $this; } public function _isset($code) { return isset($this->_data[$code]); } /** * Implementation of ArrayIterator */ public function offsetExists($offset) { return $this->_isset($offset); } public function offsetUnset($offset) { return $this->_unset($offset); } public function offsetGet($offset) { return $this->get($offset); } public function offsetSet($offset, $value) { return $this->set($offset, $value); } public function getIterator() { return new Core_Fluent_Iterator($this->_data); } } ?> 


Core / Fluent / Iterator.php
 <?php class Core_Fluent_Iterator extends ArrayIterator {} ?> 


Core / Model / Abstract.php
 <?php abstract class Core_Model_Abstract extends Core_Fluent { protected $_changed_properties = array(); public function set($code, $value) { $this->_changed_properties[] = $code; return parent::set($code, $value); } public function create() { return $this; } public function load($id) { $this->_changed_properties = array(); return $this; } public function loadFromArray(array $array = array()) { $this->_data = $array; return $this; } public function save() { $this->_changed_properties = array(); return $this; } public function remove() { return $this->unload(); } public function unload() { $this->_changed_properties = array(); $this->_data = array(); return $this; } public function toArray() { return $this->_data; } } ?> 



Instead of conclusion


It turned out quite voluminous, but mainly because of the code. If this topic is interesting, then I can describe the implementation of the collection (some sort of record array with the ability to load with filtering and collective (batch) actions) on the same mechanism. Both collections, and these models are taken from the framework I am developing, therefore, it is more correct to consider them in a complex, but I did not overload the already voluminous article.
Of course, I will be glad to hear your opinion or reasoned criticism.

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


All Articles