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 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 ¶
```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```
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?
Label and validation of embedded document
Hi, thank you for your wiki, but for me doesn't work. :(
And I have lost all labels of sub-document set in the attributeLabels() function
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.