Yii 2.0: MongoDb's embedded documents mapping for ActiveRecord in Yii2

6 followers

The scenario

You are working with MongoDb and you have embedded documents in your collection that you want to easily map in your Model for CRUD operations.

The Model

suppose to have in Mongo a collection "user" like:

{
    username: peterGower
    address:{
        city: someCity
        street: someStreet
    }
}

and as Model you have common\models\User.php that looks like

/**
 * Class User
 * @package common\models
 *
 * @property object $_id
 * @property string $username
 * @property array $address
 * @property integer $created_at
 * @property integer $updated_at
 */

now, you could also have

/**
 * Class User
 * @package common\models
 *
 * @property object $_id
 * @property string $username
 * @property string $addressCity
 * @property string $addressStreet
 * @property integer $created_at
 * @property integer $updated_at
 */

but in this case we don't want to use Mongo with hundreds of attributes in our Models (you may want to do it and it's perfectly fine by the way). So, we want to embed documents, and we're using the first approach. That means, of course, that we have to deal with custom validations. I'm using the following approach:

Custom validator

we first build a custom validator in \common\validators\EmbedDocValidator.php

namespace common\validators;
 
use yii\validators\Validator;
 
class EmbedDocValidator extends Validator
{
    public $scenario;
    public $model;
 
    /**
     * Validates a single attribute.
     * Child classes must implement this method to provide the actual validation logic.
     *
     * @param \yii\mongodb\ActiveRecord $object the data object to be validated
     * @param string $attribute the name of the attribute to be validated.
     */
    public function validateAttribute($object, $attribute)
    {
        $attr = $object->{$attribute};
        if (is_array($attr)) {
            $model = new $this->model;
            if($this->scenario){
                $model->scenario = $this->scenario;
            }
            $model->attributes = $attr;
            if (!$model->validate()) {
                foreach ($model->getErrors() as $errorAttr) {
                    foreach ($errorAttr as $value) {
                        $this->addError($object, $attribute, $value);
                    }
                }
            }
        } else {
            $this->addError($object, $attribute, 'should be an array');
        }
    }
 
}

Model for the embedded document

\common\models\Address.php

namespace common\models;
 
use yii\base\Model;
 
class Address extends Model
{
 
    /**
     * @var string $city
     */
    public $city;
 
        /**
     * @var string $street
     */
    public $street;
 
 
    public function rules()
    {
        return [
            [['city', 'street'], 'required'],           
        ];
    }
 
}

Setup the validator in the model

In common\models\User.php:

public function rules()
    {
        return [
            [['address', 'username'], 'required'],
        ['address', 'common\validators\EmbedDocValidator', 'scenario' => 'user','model'=>'\common\models\Address'],
        ];
    }

Now when the Model triggers validation, the errors of the child Model (Address.php in this case) will be added to the attribute specified in the rules (address).

The view

As php already transforms html forms into an associative array of [$key=>$value] , user/_form.php is quite easy to build:

<?php $form = ActiveForm::begin(); ?>
 
<?= $form->field($model, 'username'); ?>
 
<?= $form->field($model, 'address[city]'); ?>
 
<?= $form->field($model, 'address[street]'); ?>
 
<?php InlineActiveForm::end(); ?>

The controller

I'm just showing actionCreate() but of course also actionUpdate($id) uses the same logic:

public function actionCreate()
{
    $model = new User();
 
    if ($model->load($_POST) && $model->save()) {
        return $this->redirect(['view', 'id' => (string)$model->_id]);
    }
    return $this->render('create', [
        'model' => $model,
    ]);
}

as you can see, nothing has been modified, it's the very same code as gii's. Back in your model, if you want to get the nested attributes for GridView or other widgets, you can:

public function getAddressCity()
{
    return (isset($this->address['city']))?$this->address['city']:null;
}

and call it $model->addressCity

Total 1 comment

#18016 report it
DocSolver at 2014/08/26 06:31pm
How about embedded arrays?

Great guide!

I'm curious about the best way to deal with embedded arrays of models.

I am building a project with a datamodel that could very well be mapped to MongoDB collections with embedded arrays.

It would be nice if I could access these embedded models in the same way as Yii2's built-in related models (hasOne, hasMany...). Any suggestions on that?

Leave a comment

Please to leave your comment.

Write new article