Связь Yii Model ManyToMany с еще двумя моделями

прежде всего прошу прощения за любую ошибку в письме

Хорошо, позвольте мне объяснить

У меня есть 3 модели: музыкальная модель, модель MusicLink и модель MusicGenre. В MusicController у меня есть действие "Создать".

Затем в форме у меня есть поля «Музыкальная модель» и 2 других поля. Select2 виджет, который позволяет вам выбрать несколько жанров, и другое поле для ссылок (URL-адреса музыкального файла), которые могут быть динамически клонированы плагином jQuery «RelCopy» (максимум до 6 полей)

Музыка может иметь несколько жанров, а также несколько ссылок

Моя проблема в том, как правильно сохранить все эти поля? Надеюсь, я достаточно ясно выразился.

Вот код, который у меня есть до сих пор:

MusicController.php

/**
* Creates a new model.
* If creation is successful, the browser will be redirected to the 'view' page.
*/
public function actionCreate()
{
$music = new Music;
$genre = new MusicGenre;
$link = new MusicLink;

// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($music);

if(isset($_POST['Music']) && isset($_POST['MusicGenre']))
{
    CActiveForm::validate($genre);
    CActiveForm::validate($link);
    $music->attributes=$_POST['Music'];
    if($music->save()){
        foreach(explode(',',$_POST['MusicGenre']['genre_id']) as $gen)
        {
            $genre=new MusicGenre;
            $genre->genre_id = $gen;
            $genre->music_id = $music->id;
            if($genre->validate())
                $valid[]=true;
            else
                $valid[]=false;
        }

        foreach($_POST['Music']['links'] as $lnk)
        {
            $link=new MusicLink;
            $link->link = $lnk;
            $link->music_id = $music->id;
            $link->host = MusicLink::model()->getHoster($lnk);
            if($link->validate())
                $valid[]=true;
            else
                $valid[]=false;
        }
        if(!in_array(false,$valid)){
            $genre->save();
            $link->save();
            $this->redirect(array('view','id'=>$music->id));
        }
    }
}

$this->render('create',array(
'music'=>$music,
'genre'=>$genre,
'link'=>$link,
));
}

/**
* Updates a particular model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id the ID of the model to be updated
*/
public function actionUpdate($id)
{
$music = $this->loadModel($id);
$genre = MusicGenre::model()->findByAttributes(array('music_id'=>$id));//new MusicGenre;
$link = MusicLink::model()->findAll('music_id=:mID',array(':mID'=>$id));;
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($music);

if(isset($_POST['Music']) && isset($_POST['MusicGenre']))
{
    $music->attributes=$_POST['Music'];
    if($music->save()){
        foreach(explode(',',$_POST['MusicGenre']['genre_id']) as $gen)
        {
            //$genre = MusicGenre::model()->findByAttributes(array('music_id'=>$music->id));
            $genre->genre_id = Genre::model()->nameToId($gen);
            $genre->music_id = $music->id;
            $valid[]=$genre->save();
        }
        foreach($_POST['Music']['links'] as $lid => $lnk)
        {
            $link = MusicLink::model()->find('music_id=:mID AND id=:lID',array(':mID'=>$music->id,':lID'=>$lid));
            if($link == null){
                $link=new MusicLink;
                $link->link = $lnk;
                $link->music_id = $music->id;
                $link->host = MusicLink::model()->getHoster($lnk);
                $valid[]=$link->save();
            }else if($link->link != $lnk){
                $link->link = $lnk;
                $link->music_id = $music->id;
                $link->host = MusicLink::model()->getHoster($lnk);
                $valid[]=$link->save();
            }
        }
        if(!in_array(false,$valid))
            $this->redirect(array('view','id'=>$music->id));
    }
}
$this->render('update',array(
'music'=>$music,
'genre'=>$genre,
'link'=>$link,
));
}

\music_form.php

<?php $form=$this->beginWidget('bootstrap.widgets.TbActiveForm',array(
    'id'=>'music-form',
    'type' => 'horizontal',
    'customCssErrors' => 'inline',
    'enableAjaxValidation'=>false,
)); ?>
<fieldset>

<legend>
<?php echo $music->isNewRecord ? Yii::t('b2r','Create Music') : Yii::t('b2r','Update Music') ;?>
</legend>
<p class="help-block well well-small"><?php echo Yii::t('b2r','Fields with <span class="required">*</span> are required.'); ?>
</p>
<?php
$models = array($music,$genre);
if (is_array($link))
    foreach ($link as $lnk)
        $models[] = $lnk;
else
    $models[] = $link;

//die(var_dump($models));
?>
<?php echo $form->errorSummary($models); ?>

<div class="span6 first">
    <?php echo $form->textFieldRow($music,'artist',array('class'=>'span12','maxlength'=>255)); ?>

    <?php echo $form->textFieldRow($music,'title',array('class'=>'span12','maxlength'=>255)); ?>

    <?php echo $form->textAreaRow($music,'desc',array('rows'=>3, 'cols'=>60, 'class'=>'span12')); ?>

    <?php echo $form->select2Row($genre, 'genre_id', array(
                        'asDropDownList' => false,
                        'val' => MusicGenre::model()->getMusicGenresNames($music->id),
                        'options' => array(
                            'data' => Genre::model()->getGenres(),
                            'placeholder' => 'Escreva um ou mais categorias, separadas por virgulas',
                            'containerCssClass' => 'span12',
                            'tokenSeparators' => array(','),
                            'multiple'=>true,
                            'width'=>'none',
                            'initSelection' => 'js:function (element, callback) {
                                  var val = [];
                                  $(element.val().split(",")).each(function () {
                                      val.push({id: this, text: this});
                                  });

                                  callback(val);
                            }'
                        )
                      )); ?>
    <?php echo $form->maskedTextFieldRow($music,'length',array('mask'=>'99:99?:99','htmlOptions'=>array('class'=>'span12','maxlength'=>8)),array('hint'=>Yii::t('b2r','No formato {f1} ou {f2}',array('{f1}'=>'<i class="label label-info">MM:SS</i>','{f2}'=>'<i class="label label-info">HH:MM:SS</i>')))); ?>

    <?php echo $form->maskedTextFieldRow($music,'size',array('mask'=>'?~~~.~~','charMap'=>array('~'=>'^[0-9]+(\.)?[0-9]{0,2}$'),'placeholder'=>'0','htmlOptions'=>array('class'=>'span12','maxlength'=>8)),array('hint'=>Yii::t('b2r','No formato {f1} ou {f2}',array('{f1}'=>'<i class="label label-info">xx.xx</i>','{f2}'=>'<i class="label label-info">xxx.xx</i>')))); ?>

</div>
<div class="span6">
    <div class="controls">
        <ul class="thumbnails">
            <li class="span5">
                <a class="thumbnail">
                <?php echo CHtml::image('/images/capa.gif',Yii::t('b2r','Previsualização da Imagem'),
                        array('id'=>'previewHolder','width'=>'170px','height'=>'170px')); ?>
                </a>
            </li>
        </ul>
    </div>

    <?php echo $form->textFieldRow($music,'image',array('class'=>'span8','maxlength'=>255),array('controlCss'=>'skipcopy','append'=>'<a href="#" id="findcover" data-toggle="tooltip" title="'.Yii::t('b2r','Procurar capa no Google').'"><i class="icon-circle-arrow-right"></i></a>')); ?>

    <?php
    if ($music->isNewRecord){
        echo $form->textFieldRow($link,'link',
            array('name'=>'MusicLink[links][0]','value'=>'','maxlength'=>255, 'class'=>'span8'),
            array('controlCss'=>'copy clonable','append'=>'<a id="copylink" href="#" rel=".copy"><i class="icon-plus"></i></a> ')
        );
    }else{
        $last = count($link)-1;
        foreach ($link as $k => $v)
            die(var_dump($link));
            $id = (is_null($v)) ? $k : $v->id;
            if($k == $last)
                echo $form->textFieldRow($v,'link',
                    array('name'=>'MusicLink[links]['.$id.']','maxlength'=>255, 'class'=>'span8'),
                    array('controlCss'=>'copy clonable','label'=>'<span class="required">*</span>','append'=>'<a id="copylink" href="#" rel=".copy"><i class="icon-plus"></i></a> ')
                );
            else
                echo $form->textFieldRow($v,'link',
                    array('name'=>'MusicLink[links]['.$id.']','maxlength'=>255, 'class'=>'span8'),
                    array('controlCss'=>'clonable')
                );
    }
    ?>

<?php
$this->widget('ext.jqrelcopy.JQRelcopy',array(

 //the id of the 'Copy' link in the view, see below.
 'id' => 'copylink',

  //add a icon image tag instead of the text
  //leave empty to disable removing
 'removeText' => '<i class="icon-remove"></i>',

 //htmlOptions of the remove link
 'removeHtmlOptions' => array('style'=>'margin-left:2px;padding:3px 10px;','class'=>'btn btn-small btn-danger'),

 //options of the plugin, see http://www.andresvidal.com/labs/relcopy.html
 'options' => array(

       //A class to attach to each copy
      'copyClass'=>'newcopy',

      // The number of allowed copies. Default: 0 is unlimited
      'limit'=>6,

      //Option to clear each copies text input fields or textarea
      'clearInputs'=>true,

      //A jQuery selector used to exclude an element and its children
      'excludeSelector'=>'.skipcopy',

      //Additional HTML to attach at the end of each copy.
      //'append'=>CHtml::tag('span',array('class'=>'hint'),'You can remove this line'),
       //'jsAfterNewId' => "if(typeof $(this > input).attr('name') !== 'undefined'){ $(this > input).attr('name', $(this > input).attr('name').replace('new', 'new_'+counter));}",

   )
));
?>
</div>
<?php $collapse = $this->beginWidget('bootstrap.widgets.TbCollapse',array('htmlOptions'=>array('class'=>'span12 first'))); ?>
    <div class="accordion-group">
        <div class="accordion-heading">
            <a class="accordion-toggle" data-toggle="collapse"
            data-parent="#accordion2" href="#collapseOne">
            Avançado
            </a>
        </div>
        <div id="collapseOne" class="accordion-body collapse">
            <div class="accordion-inner">
                <?php echo $form->textFieldRow($music,'bitrate',array('class'=>'span9')); ?>
            </div>
        </div>
    </div>
<?php $this->endWidget(); ?>
</fieldset>

<div class="form-actions">
    <?php $this->widget('bootstrap.widgets.TbButton', array(
            'buttonType'=>'submit',
            'type'=>'primary',
            'label'=>$music->isNewRecord ? Yii::t('b2r','Create') : Yii::t('b2r','Save'),
        )); ?>
</div>
<?php $this->endWidget(); ?>
<?php Yii::app()->clientScript->registerScript('script', "
   $('#Music_image').change(function() {
       $('#previewHolder').attr('src',$(this).val());
   });
   $('#findcover').click(function() {
       var q = $('#Music_artist').val();
       q += ' - '+$('#Music_title').val();
       q += ' cover';
       window.open('https://www.google.pt/search?q='+escape(q)+'&tbm=isch', '_blank');
   });
"
, CClientScript::POS_READY);?>

ИЗМЕНИТЬ

С этим кодом я могу сохранить все правильно, но например, если я не выбираю Жанр, или не ввожу ссылку на файл, сохраняет только музыку. Но, например, если отсутствует обязательное поле Music Model, а также MusicGenres или MusicLink, проверяются все три модели. Если ошибки относятся только к модели MusicGenre или MusicLink, музыка сохраняется, но не эти 2 модели


Я использую Yii версии 1.1.14 с YiiBoostrap и YiiBoilerplate.

Вот ссылки на плагины:

http://www.andresvidal.com/labs/relcopy.html

http://ivaynberg.github.io/select2/

http://yiibooster.clevertech.biz/


person I-NOZex    schedule 13.03.2014    source источник
comment
Извините, но я не понимаю, в чем ваша проблема. Ваш код, кажется, работает. Возможно, вам следует заключить все сохранения в транзакцию.   -  person Isidoro Arroyo Barrantes    schedule 14.03.2014
comment
работает, да, но если я не выбираю какой-либо жанр или не пишу ссылку на музыку, она сохраняется и не показывает ошибки в форме   -  person I-NOZex    schedule 14.03.2014


Ответы (2)


Вы можете использовать компонент esaverelatedbehavior, разработанный sluderitz, для сохранения связанных моделей, скачать его можно здесь

После установки вышеуказанного расширения вы можете сделать так

$music->attributes=$_POST['Music'];

if (isset($_POST['MusicGenre']))
{
    $music->musicgenre= $_POST['MusicGenre'];
}
if ($music->saveWithRelated('musicgenre'))
    $this->redirect(array('view', 'id' => $model->id));

Примечание. Мы можем сделать $music->musicgenre, потому что musicgenre — это имя отношения.

Подробное решение для сохранения нескольких связанных моделей использование вышеуказанного расширения доступно в моем сообщении в блоге.

person Tahir Yasin    schedule 19.06.2014

Хорошо, теперь я знаю, чего ты хочешь.

В методе создания, когда вы проверяете допустимость массива родов или ссылок, вы можете сгенерировать сообщение об ошибке и перенаправить на действие обновления и попросить пользователя вставить данные родов или ссылок.

В действии Создать:

if($music->save()){

     ......

    if(!in_array(false,$valid)){
        $genre->save();
        $link->save();
        $this->redirect(array('view','id'=>$music->id));
    } 
    else {
        Yii::app()->user->setFlash('error',"Some cool warning message");
        $this->redirect(array('update','id'=>$music->id)));
    }
}
     ......

В ActionUpdate сделайте то же самое:

........
if(!in_array(false,$valid))
        $this->redirect(array('view','id'=>$music->id));
else
        Yii::app()->user->setFlash('error',"Some cool warning message");

Теперь в файле просмотра обновления (не в _form) вы должны показать сообщение об ошибке:

<?php if(Yii::app()->user->hasFlash('error')):?>
<div class="flash-error">
    <?php echo Yii::app()->user->getFlash('error'); ?>
</div>
<?php endif; ?>

Для DIV "flash-error" определен CSS по умолчанию. См. раздел введите здесь описание ссылки.

Я надеюсь, что это поможет вам.

ДРУГОЕ ВОЗМОЖНОЕ РЕШЕНИЕ может заключаться в определении массива для родов и ссылок в музыкальной модели. затем присвойте значения формы этим переменным перед вызовом $music->saave. В музыкальной модели вы должны написать правила валидации для массивов и в beforeSave() послеSave() Напишите код для вставки родов и строк ссылок.

person Isidoro Arroyo Barrantes    schedule 14.03.2014
comment
Спасибо за ответ. Я знаю, что не буду откровенен, даже на португальском трудно объяснить, на английском я не понимаю, как это сделать, я отредактирую вопрос, чтобы иметь возможность объяснить, что я хочу - person I-NOZex; 14.03.2014
comment
Не беспокойтесь о своем английском. Я из Испании, и мой английский хуже твоего. Вы проверяете мои предложения? Я думаю, что это решит вашу проблему. - person Isidoro Arroyo Barrantes; 14.03.2014
comment
Я не проверял, потому что это не совсем то, что я хочу. Планирую показать, что ошибки нормально через $form-> ErrorSummary(); - person I-NOZex; 14.03.2014
comment
Но результат тот же. Когда пользователь добавляет новый музыкальный элемент без жанра, выполняется перенаправление на представление обновления, показывающее только что вставленные музыкальные данные с сообщением об ошибке, предлагающим пользователю ввести пропущенные данные. Я думаю, что это сработает, но вы можете проверить жанры и ссылки в Музыкальной модели. - person Isidoro Arroyo Barrantes; 14.03.2014
comment
привет, приятель, у меня есть решение, из-за его подсказки другого возможного решения. Только не хватает детали. как я могу получить идентификатор МУЗЫКАЛЬНОЙ МОДЕЛИ в файле beforeSave? ---------- не могли бы вы отредактировать сообщение, чтобы я мог принять ответ? - person I-NOZex; 15.03.2014
comment
Ты прав. В методе breforeSave у вас нет доступа к первичному ключу, потому что он еще не создан. Поэтому вам нужно будет перезаписать метод afterSave. Я отредактирую свой ответ. - person Isidoro Arroyo Barrantes; 16.03.2014
comment
но это как проблема, если это какая-либо ошибка при сохранении связанных моделей в модели afterSave of Music, как мне сделать какой-то откат? :/ - person I-NOZex; 21.03.2014
comment
Если ошибка запускается при вставке таблиц родов или ссылок, вы можете удалить значения, вставленные в таблицу музыки, или, может быть, если ваша база данных позволяет транзакции, лучшим решением будет сделать все операции в транзакции и откатиться, если есть какая-либо ошибка . - person Isidoro Arroyo Barrantes; 23.03.2014
comment
Я тоже думал об этом, но, может быть, первый вариант немного неэффективен, нет? а как использовать отдельные транзакции? можно запустить в actionCreate и закончить транзакцию на afterSave? - person I-NOZex; 24.03.2014
comment
Я не думаю, что это было неэффективно, если у вас нет тяжелого доступа к вашей базе данных. Транзакции находятся на уровне базы данных, поэтому вы можете запустить транзакцию в методе действия и зафиксировать или откатить ее после сохранения. - person Isidoro Arroyo Barrantes; 24.03.2014
comment
хорошо, я попробую транзакции таким образом и посмотрю, сработает ли это: D, спасибо - person I-NOZex; 25.03.2014