📜 ⬆️ ⬇️

Using Yii's ActiveRecord in a time manager game

Hello!

Today I want to tell you how the work with the cache was implemented in a social time manager game. You can consider this article as a continuation of this one .

Let me remind you that the project uses php (Yii), mysql and memcached. There are a lot of entities in the project, each of which has its own model, which is inherited from CActiveRecord.
')
Model files are stored as follows. In the models folder, create the base folder. When we generate a model through Gii, we indicate that we need to put it in the models / base folder and add Base to the class name. Then we create in models a similar class without Base, which is inherited from the base class and has only the model () method in it.

By the way, I will say in advance that the basic models are not inherited from CActiveRecord, but from ExtActiveRecord - we extend CActiveRecord to our needs. But more about that later. So far, no difference.

Example:
models / base / BaseUser.php - standard class that is generated via Gii
models / User.php - a class that inherits from BaseUser and has a model () method in it
/** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return User the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } 



This scheme is used to ensure that in case of repeated generation of the model file, not to lose your code and simply not to fill in the space with the standard code from Yii.

Do not forget to add in the config 'application.models.base. *'.

Let's move on to the topic of the post and set the tasks that we want to solve:
  1. Reduce the number of requests to the database to update
  2. Reduce the number of requests to the database per sample




Reducing the number of requests to the database for updating


As you remember from the last article, we use a queue to execute commands. And for a particular user, it may be necessary to execute more than 2 commands in sequence. For example, we receive a pack of 3 teams: increase experience, buy a building and change the player's name. Suppose that experience, money, and name are stored in one user table.

The implementation of queue processing is such that the teams know nothing about each other and in the standard implementation, each team will load the user model, modify it and save.

We will now do so that the user will be loaded upon first access to it, and saved only after all the commands are executed. To do this, we will make a register of models.
This is the kind of thing that we will turn to to get the User model, instead of writing User :: model () -> findByPk ().

The model registry will be a component and will be registered in the components config.
 'components' => array( // ... 'modelRegistry'=>array( 'class' => 'ModelRegistry' ) // ... ) 


The class itself looks like this.
 class ModelRegistry { protected $registries = array(); public function init() {} /** *     * @param string $name * @param mixed $attr * @return ExtActiveRecord */ public function & registry($name, $attr = array()) { $key = $name . md5(serialize($attr)); if (!isset($this->registries[$key])) { $model = ucfirst($name); $obj = $model::model(); if (!is_array($attr)) $attr = array($attr); $this->registries[$key] = call_user_func_array(array(&$obj, 'registry'), $attr); } //         &    return $this->registries[$key]; } /** *   */ public function saveAll() { foreach ($this->registries as $obj) { $obj->save(); } } } 


Each of our models that we want to receive through the model registry will have a registry method that will return an object. User in this case looks like this.

 /** * @property integer $id * @property integer $exp * @property integer $money * @property integer $name */ class User extends BaseUser { /** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return User the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } /** *    * @param int $userID * @return User|bool */ public function registry($userID) { if ($obj = $this->findByPk($userID)) { $res = $obj; } else { $res = false; } return $res; } } 


What it all led to. For example, we have a controller that calls two methods that modify the user.

  /** * @var ModelRegistry */ protected $reg; public function actionRun() { $userID = 1; $this->reg = &Yii::app()->modelRegistry; $this->firstChange($userID); $this->secondChange($userID); $this->reg->saveAll(); } public function firstChange($userID) { //   .         // & ,        $user = &$this->reg->registry('user', $userID); $user->exp = 10; } public function secondChange($userID) { //    ,     .     // &  $user = &$this->reg->registry('user', $userID); $user->money = 20; } 


Actually there will be one access to the database on select and one on update.
Here we face an extra challenge. If you call this code a second time, the user fields will remain the same, but the saving will still be made. We need to make sure that our ActiveRecord is saved only if the object has tolerated changes.

This is where our ExtActiveRecord comes in, which we used to extend CActiveRecord.

 class ExtActiveRecord extends CActiveRecord { protected $_oldAttributes = array(); /** *   ,       */ public function registry() {} /** *     */ public function memoryAttributes() { $this->_oldAttributes = $this->attributes; } /** *    .     *           ,   false * @return array|false */ protected function getChanges() { $res = array(); if (empty($this->_oldAttributes)) { $res = false; } else { foreach ($this->_oldAttributes as $key => $value) { if ($this->$key != $value) { $res[] = $key; } } } return $res; } /** *    * @return bool */ public function save() { if (($attr = $this->getChanges()) === false) { $res = parent::save(); } elseif ($attr) { $res = $this->update($attr); } else { $res = false; } return $res; } } 


And update the registry method in the User model

 public function registry($userID) { if ($obj = $this->findByPk($userID)) { $res = $obj; } else { $res = false; } if ($res) { //     $res->memoryAttributes(); } return $res; } 


Actually now only insert or update of the modified model fields will be performed.
I leave you space for creativity and let you figure out how to create a new user and place it in the model registry during the execution of the script.

I showed you how to store any objects in the registry. But sometimes there are situations when we need to keep a list there. For example, for each user we have 10 cars. And we want the registry to have not 10 machines, but one object containing all the machines. To do this, use the ModelList class, which stores models of machines.

 class ModelList { /** * @var array   ExtActiveRecord */ public $list = array(); /** *    * @param array|bool $list   ExtActiveRecord * @return ModelList */ public static function make($list = array()) { if (!is_array($list) && empty($list)) { $list = array(); } $obj = new ModelList(); $obj->list = $list; return $obj; } /** *     * @param ExtActiveRecord $obj */ public function pushObject($obj) { $this->list[] = $obj; } /** *      * @param string $name */ public function callMethod($name) { foreach ($this->list as &$obj) { $obj->$name(); } } /** *    */ public function save() { $this->callMethod('save'); } } 


And here is the model of the car
 <?php /** * @property integer $id * @property integer $user_id * @property integer $car_id * @property integer $speed */ class Car extends BaseCar { /** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return Car the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } /** *      * @param int $userID * @return ModelList */ public function registry($userID) { $list = $this->findAllByAttributes(array('user_id'=>$userID)); $res = ModelList::make($list); //      $res->callMethod('memoryAttributes'); return $res; } /** * ,   . ,      ModelList  pushObject * @param int $userID * @param int $carID * @return Car */ public static function make($userID, $carID) { $obj = new Car(); $obj->user_id = $userID; $obj->car_id = $dict->area_id; $obj->speed = 10; return $obj; } } 


Actually now, when we execute the following code,
 $carList = &Yii::app()->modelRegistry->registry('car', 1); 

then we get an object of the class ModelList, which will contain all the player's machines. They can also be changed (without forgetting to refer to the link in $ carList-> list) and then saved through the model registry with the standard saveAll.

Since the article is quite large, I will tell you about caching all this in the next article.
I can say that in ideal conditions with caching this implementation does not apply for the same data twice, even if after the first time they were updated.

This version of working with models is conveniently used in our project, but it may not suit you.
All I wanted was just to show what tasks there are and how they can be solved.

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


All Articles