MongoDb's embedded documents mapping for ActiveRecord in Yii2

  1. The scenario
  2. The Model

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 `Modelyou havecommon\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


```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```:


```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
<?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:


```php
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:


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

```

and call it ```$model->addressCity```