Валидация данных вложенных документов MongoDB в Yii2

    Возможно, всё что я напишу ниже – очевидно, и все этим пользуются давно, но я вот недавно только это понял и придумал, так что, может, кому и пригодится.


    Yii2 и расширение yii2-mongodb к сожалению, не работает с вложенными документами, тем самым оставляя за бортом существенное преимущество документоориентированной БД.
    В документации предлагают использовать расширение для вложенных документов, но можно обойтись и без него.
    Предположим, у нас есть модель, формирующая PDF-файл для загрузки, и мы хотим следить за количеством его скачиваний, IP-адресами скачавших и, например, временем, когда файл был загружен.


    Для простоты я предполагаю, что сам файл хранится в строке, но это, конечно, может быть совсем не так – он может лежать где-то в хранилище или формироваться функцией.


    Основная модель (часть)


    /**
     * @property string $pdf_data       стока с данным, которая потом преобразуется в файл
     * @property array $downloads_data  здесь хранятся сведения о загрузках
     *
     */
    Class PdfData extends \yii\mongodb\ActiveRecord
    
    /** @inheritdoc */
    public static function collectionName()
    {
        return [‘database’, ‘pdf’]
    }
    
    /** @inheritdoc */
    public function attributes()
    {
        return [
            ‘pdf_data’,
            ‘downloads_data’
        ];
    }

    Дополнительная модель – для проверки и присвоения значений элементам массива


    
    use \MongoDB\BSON\UTCDateTime
    
    /**
     * Класс для формирования сведений о факте загрузки файла
     */
    class DowmnloadData extends \yii\base\Model
    {
        /** @var \MongoDB\BSON\UTCDateTime $datetime */
        public $datetime;
        /** @var string $clientIp */
        public $clientIp;
        /** @var string $clientHost */
        public $clientHost;
        /** @var string $clientUserAgent */
        public $clientUserAgent;
        /** @var string $referer */
        public $referer;
        /** @var bool $result */
        public $result = false;
    
        /** @inheritdoc */
        public function rules()
        {
            return [
                ['datetime', 'default', 'value' => function() 
                       { return new UTCDateTime(strtotime("now") * 1000); }],
                ['clientIp', 'default', 'value' => function() 
                       { return Yii::$app->request->getUserIP(); }],
                ['clientHost', 'default', 'value' => function() 
                       { return Yii::$app->request->getUserHost(); }],
                ['clientUserAgent', 'default', 'value' => function() 
                       { return Yii::$app->request->getUserAgent(); }],
                ['referer', 'default', 'value' => function() 
                       { return Yii::$app->request->getReferrer(); }],
                ['result', 'boolean'],
            ];
        }

    Далее, в действии контроллера, которое отдает файл наружу, примерно следующее:


    // -- skip --
    
    /**
     * @param string $id идентификатор основной модели
     * @return null
     * @throws \yii\web\NotFoundHttpException
     */
    public function actionDownload($id)
    {
        if(($model = PdfData::findOne($id)) === null)
            throw new \yii\web\NotFoundHttpException(Yii::t('app', 'File not found'));
    
        $downloadData = new DowmnloadData();
        if(!empty($model->pdf_data))
        {
            $downloadData->result = true;
            $downloadData->validate(); // Так мы добиваемся присвоения значений по-умолчанию
            $data = $model->downloads_data; // Забрали существующие сведения о загрузках
            $data[] = $downloadData->attributes; // Добавили к ним новые
            // array_values для гарантироанного сохранения массива (а не объекта) в mongodb
            $model->updateAttributes(['downloads_data' => array_values($data)]); 
    
            // Отправляем файл
            Yii::$app->response->sendContentAsFile($model->pdf_data, 'we are the champions.pdf', [
                'mimeType' => 'text/xml',
                'inline' => true
            ]);
        }
        return null;
    }

    Таким образом внутри массива downloads_data основной модели мы имеем все атрибуты, которые придумали в DowmnloadData, и можем их потом как угодно показывать и анализировать, не умножая сверх необходимого при этом ни атрибуты основной модели, ни число коллекций в БД.

    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 7

      0
      А где, собсно, валидация вложенных атрибутов? Я ожидал чего-то в стиле:
      public function rules() {
          return [
              [['field.child1.child2'], 'required'],
              [['anotherField.hisChild'], 'string', 'max' => 20],
          ];
      }
      

      Ну или чего-то в таком стиле. А тут виден только булев валидатор на поле result и использование валидаторов для загрузки значений по умолчанию, что немного вообще мимо кассы.
        –1
        Так по обстоятельствам. В данном-то случае и нужно только автоприсвоение и ничего более, но кто ж мешает что-то ещё добавить? И, например, в форму вывести.
        Я же показываю принцип, а не пишу пошаговое руководство.
          0
          Так не видно принципа ведь :)

          Может стоит хотя бы поменять заголовок? Не хочу обидеть, но сюда просится «Использование модели для предварительной валидации данных для другой модели» и это довольно стандартный подход, когда, скажем, форма регистрации создаётся через отдельную модель, со своими полями и правилами, а не путём запихивания валидаторов и сценариев в модель пользователя.
            0

            Я же говорю:


            Возможно, всё что я напишу ниже – очевидно, и все этим пользуются давно, но я вот недавно только это понял и придумал, так что, может, кому и пригодится.

            Мне лично как-то неочевидно было до сих пор, к сожалению :)


            И да, насчет заголовка – конечно, мы в данном случае используем \yii\base\Model для валидации (и присвоения значений) другой модели (что, в общем, в примерах есть), но и привязка к mongodb и её вложенным документам тоже важна, как по мне. Именно про это речь.

          +1
          Я делал себе недавно такой валидатор: https://gist.github.com/paulzi/ad27c4689475ca442a2ea5880d659ff3

          Использовать так:
          public function rules() {
              return [
                  ['params'], CompositeValidator::className(), 'rules' => [
                      [['orderCallback'], 'match', 'pattern' => '|^https?://.+|'],
                      [['orderCallback'], 'default', 'value' => null],
                      [['calc', 'calcOasisPriceType'], 'filter', 'filter' => 'boolval'],
                      [['factor'], 'filter', 'filter' => 'floatval'],
                      [['calcOasisFactor'], 'default', 'value' => null],
                      [['fields[].title'], 'required'],
                  ]],
              ];
          }
          


          Если будет интересно, могу оформить в composer.

          Правда есть тонкость, чтобы в Yii2 корректно показывалось в каком именно поле ошибка, нужно переопределить Html::getAttributeName(), а то там вырезается вложенность:
              public static function getAttributeName($attribute)
              {
                  if (preg_match('/(^|.*\])([\w\.]+)(\[.*|$)/', $attribute, $matches)) {
                      return $matches[2] . $matches[3];
                  } else {
                      throw new InvalidParamException('Attribute name must contain word characters only.');
                  }
              }
          
            0
            Отличная задумка. Я сам уже пытался сделать нечто подобное, но мои попытки разбились о то, что придётся переписывать код из Model и Validator, что отвечает за создание и подключение валидатора. Тебя, судя по коду, это не смутило и ты заставил это работать :)

            Оформи как Composer пакет, я постараюсь тоже сделать свой вклад.
              0
              На самом деле многое взял из стандартного EachValidator. Ок, будет время — сделаю)

        Only users with full accounts can post comments. Log in, please.