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