Yii 1.1: Creating and updating model and its related models in one form, inc. image

22 followers

General

  • There are more than a few forum threads and several wiki articles on how to create more than 1 model record in a single form but I didn't find one complete as I wish there was, covering all that I wanted. So, after going through the process during a project I'm making, I wrote this article.
  • This article covers creating a main model and a limited number of 'related' models in a single form. It shown relevant code segments from the models, controllers, and view files, for a full coverage of the subject (well, almost full. See 'design notes' section below).
  • The code segments contains useful comments that document lots of important notes, decisions taken etc. I recommend not overlooking them.

How to use this article?

  • I recommend reading it from top to bottom. The code samples are presented in a logical order (IMHO...).
  • As already noted, I recommend reading the code segments with attention to the comments. Lots of decision making, design notes, etc is noted within those comments.

Features / Use case

  • Say hello to your 'music' feature. Lets make it a 'MusicModule' - a Yii module (as usually I like to do, for the most natural packaging for bundles that contain elements from many kinds (models, controllers, widgets, translation files, etc)).
  • For the purpose of this article the module will have two models: Artist and Song. An artist model contains some textual fields (like title, name, 'about', etc) and an image file. The Song model contains textual fields like name, lyrics, etc.
  • There should be one to many relationship between Artist and Song so an artist can have multiple songs.
  • The main model, Artist, shall have a single image file attribute. Only Jpeg files should be allowed (for simplicity). We'd like to name the file to be unrelated to the filename it was submitted with but rather as a "{PK}_artist.jpg". This naming convention has the benefit that when viewing your artists files you can immediately tell that each file is an artist main image, and associate the image to the artist by its PK. It also has its drawbacks and those are laid out in the plenty comments in the code samples.
  • We'd like to have a limit on the max possible songs an artist can have. This is good to prevent abusing of the system and for saving resources. Having a limit is generally a good practice if you'd like to protect your system from abusing.

Design notes

  • This section is best served while reading the code segments as it refers to actual design implemented in the code.
  • Having said that (above), here are some design notes:
    • Song model: 'artist_id', the model-attribute/db-column that links songs table to the artists table, on the face of it, should be a 'required field' in the model. I recommend also making it a foreign key constraint in the DB level. Yet, we validate both artist and songs objects before saving them so when we validate a new song object, there will not be an artist record yet in the DB, therefore we will not have a primary key for the artist, to be noted in the artist_id attribute of the song... (the chicken and the egg problem...). Therefore, we cannot make the 'artist_id' field in the Song model a required field and it should not be required. Still, the code needs to make sure carefully that before an attempt to save a Song object its 'artist_id' attribute will be populated. If not, assuming that DB foreign key constraint has been implemented, an exception originating in the CDbConnection will be thrown. As you'll see in the code samples in the article, this is handled just as suggested.
    • View files: Well, I'm not a front end guy. At the time of writing this I don't have a fully working front end code simply due to resource allocation preferences. It also results in basic view files used here and one missing thing on the front end arena - some JS section that will bind to click events on some 'add more songs' button. Upon clicking, it needs to copy the div with id of "extra-song" and create a blank 'new song' div from it, assigning correct 'counter' value and assign correct value to the 'name' attribute of each of the song's form elements (that includes the correct counter value).
    • The error messages emitted are i18n-able. See usage of Yii:t() in the code samples. Note however - this isn't strictly so in the view files.
    • Yii's logging facility is used (calls to Yii::log()). If you use the code as your basis I recommend configuring the log application component to your preferences in main.php.

Comments?

  • Always true: Practice (and feedback) makes perfect.
  • Feel free to PM me or comment on this page.
  • TIA!

Model files

We start slowly and simple...

Artist model

class Artist extends PcBaseArModel {
    // more code...
    /**
     * @return array relational rules.
     */
    public function relations() {
        return array(
            'songs' => array(self::HAS_MANY, 'Song', 'artist_id'),
        );
    }
    // more code...
}

Song model

class Song extends PcBaseArModel {
    // more code...
    /**
     * @return array relational rules.
     */
    public function relations() {
        return array(
            'artist' => array(self::BELONGS_TO, 'Artist', 'artist_id'),
        );
    }
    // more code...
}

Controller class

The first thing we'll do is to create an artist. Artist creation form will enable creating song records as well so we need to implement ArtistController.actionCreate()

ArtistController, create action

class ArtistController extends Controller {
    public function actionCreate() {
        /*
         * first, do some permission's check. I use Yii's RBAC facility. YMMV, and it doesn't matter. Just do a
         * permissions check here.
         */
        if (!Yii::app()->user->checkAccess('create artist')) {
            // not allowed... .
            throw new CHttpException(401);
        }
 
        $model = new Artist('insert');
        $song = new Song('insert');
        // by default, if we already submitted the form and if we didn't, we allow adding more songs
        // this will be updated down below if needed to
        $enable_add_more_songs = true;
 
        if (isset($_POST['Artist'])) {
            // start optimistically by settings a few variables that we'll need
            $all_songs_valid = true;
            $songs_to_save = array();
            $success_saving_all = true;
 
            $model->attributes = $_POST['Artist'];
 
            // use aux variable for manipulating the image file. 'icon_filename' is the attribute name in the Artist model
            $image = CUploadedFile::getInstance($model, 'icon_filename');
            // we need to put something into the icon_filename of the model since otherwise validation will fail if its
            // a 'required' field (this is 'create' scenario).
            if ($image) {
                // if its not required field and image wasn't supplied this block will not be run so
                // this is safe for both 'required field' and 'non required field' use cases.
                $model->icon_filename = "(TEMP) " . $image->name;
            }
 
            // lets start handle related models that were submitted, if any
            if (isset($_POST['Song'])) {
                if (count($_POST['Song']) > Song::MAX_SONGS_PER_ARTIST) {
                    /*
                     * server side protection against attempt to submit more than MAX_SONGS_PER_ARTIST
                     * this should be accompanied with a client side (JS) protection.
                     * If its accompanied with client side protection then going into this code block means our system
                     * is being abused/"tested". No need to give a polite error message.
                     */
                    throw new CHttpException(500, Yii::t("MusicModule.forms", "The max amount of allowed songs is {max_songs_num}", array('{max_songs_num}' => Song::MAX_SONGS_PER_ARTIST)));
                }
                // now handle each submitted song:
                foreach ($_POST['Song'] as $index => $submitted_song) {
                    // We could have empty songs which means empty submitted forms in POST. Ignore those:
                    if ($submitted_song['title'] == '') {
                        // empty one - skip it, if you please.
                        continue;
                    }
 
                    // validate each submitted song instance
                    $song = new Song();
                    $song->attributes = $submitted_song;
                    if (!$song->validate()) {
                        // at least one invalid song. We'll need to remember this fact in order to render the create
                        // form back to the user, with the error message:
                        $all_songs_valid = false;
                        // we do not 'break' here since we want validation on all songs at the same shot
                    }
                    else {
                        // put aside the new *and valid* Song to be saved
                        $songs_to_save[] = $song;
                    }
                }
 
                // while we know that songs were submitted, determine if the number of songs has exceeded its limit.
                // this will be goof when rendering back the 'create' form so no more songs-forms will be available.
                if (count($_POST['Song']) == Song::MAX_SONGS_PER_ARTIST) {
                    $enable_add_more_songs = false;
                }
            }
 
            // Done validation. Summarize all valid/invalid information thus far and act accordingly
            if ($all_songs_valid && $model->validate()) {
                /*
                 * all songs (if any) were valid and artist model is valid too. Save it all in one transaction. Save first the
                 * artist as we need its 'id' attribute for its songs records (Song.artist_id is NOT NULL in the DB level
                 * and a 'required' attribute in our model).
                 */
                $trans = Yii::app()->db->beginTransaction();
 
                try {
                    // save the artist
                    $model->save(false);
                    // handle image of artist, if supplied:
                    if ($image) {
                        // Song.getImageFsFilename() encapsulates the image filename setting details.
                        $image->saveAs($model->getImageFsFilename($image));
                        /**
                         * now update the model itself again with the full filename of the image file.
                         * this is the disadvantage of using filenames that include the PK of the entry - we need it first saved
                         * in the DB before we know what it is... . */
                        // icon_filename is a VARCHAR(512) in the DB (for long filenames) and 'string' in the Artist model
                        $model->icon_filename = $model->getImageFsFilename($image);
                        $model->save(false);
                    }
 
                    // save the songs
                    foreach ($songs_to_save as $song) {
                        $song->artist_id = $model->id;
                        $song->save(false);
                    }
 
                    // here, it means no exception was thrown during saving of artist and its songs (from the DB, for example).
                    // good - now commit it all...:
                    $trans->commit();
                }
                catch (Exception $e) {
                    // oops, saving artist or its songs failed. rollback, report us, and show the user an error.
                    $trans->rollback();
                    Yii::log("Error occurred while saving artist or its 'songs'. Rolling back... . Failure reason as reported in exception: " . $e->getMessage(), CLogger::LEVEL_ERROR, __METHOD__);
                    Yii::app()->user->setFlash('error', Yii::t("MusicModule.forms", "Error occurred"));
                    $success_saving_all = false;
                }
 
                if ($success_saving_all) {
                    // everything's done. Would you believe it?! Go to 'view' page :)
                    $success_msg = (count($songs_to_save) > 0) ? "Artist and song records has been created successfully!" : "Artist record has been created successfully!";
                    Yii::app()->user->setFlash('success', Yii::t("MusicModule.forms", $success_msg));
                    $this->redirect(array("view", "id" => $model->id));
                }
            }
        }
 
        $this->render('create', array(
            'artist' => $model,
            'songs' => (isset($songs_to_save)) ? $songs_to_save : array(new Song('insert')),
            'enable_add_more_songs' => $enable_add_more_songs,
        ));
    }
}

Now we need to implement an update action. Updating will also update both the artist and its existing songs, and will allow to add more songs (if limit not exceeded).

ArtistController, update action

class ArtistController extends Controller {
    public function actionUpdate($id) {
        /* @var Artist $model */
        $model = $this->loadModel($id);
 
        // check access
        if (!Yii::app()->user->checkAccess('edit artist')) {
            throw new CHttpException(401);
        }
 
        // does this artists exists in our DB?
        if ($model === null) {
            Yii::log("Artist update requested with id $id but no such artist found!", CLogger::LEVEL_INFO, __METHOD__);
            throw new CHttpException(404, Yii::t("MusicModule.general", 'The requested page does not exist.'));
        }
 
        // enable adding songs by default (will be changed below if needed to)
        $enable_add_more_songs = true;
 
        if (isset($_POST['Artist'])) {
            // start optimistically
            $all_songs_valid = true;
            $songs_to_save = array();
            $success_saving_all = true;
 
            $model->attributes = $_POST['Artist'];
            if (isset($_POST['Song'])) {
                if (count($_POST['Song']) > Song::MAX_SONGS_PER_ARTIST) {
                    /*
                     * server side protection against attempt to submit more than MAX_SONGS_PER_ARTIST
                     * this should be accompanied with a client side (JS) protection.
                     * If its accompanied with client side protection then going into this code block means our system
                     * is being abused/"tested". No need to give a polite error message.
                     */
                    throw new CHttpException(500, Yii::t("MusicModule.forms", "The max amount of allowed songs is {max_songs_num}", array('{max_songs_num}' => Song::MAX_SONGS_PER_ARTIST)));
                }
                foreach ($_POST['Song'] as $index => $submitted_song) {
                    // We could have empty songs which means empty submitted forms in POST. Ignore those:
                    if ($submitted_song['title'] == '') {
                        // empty one - skip it, if you please.
                        continue;
                    }
 
                    // next, validate each submitted song instance
                    if ((int)$submitted_song['id'] > 0) {
                        /* Validate that the submitted song belong to this artist */
                        $song = Song::model()->findByPk($submitted_song['id']);
                        if ($song->artist->id != $model->id) {
                            Yii::log("Attempts to update Song with an id of {$song->id} but it belongs to an Artist with an id of {$song->model->id}" .
                                    " and not 'this' artist with id = {$model->id}", CLogger::LEVEL_ERROR, __METHOD__);
                            throw new CHttpException(500, "Error occurred");
                        }
                    }
                    else {
                        // this submitted song object is a new model. instantiate one:
                        $song = new Song();
                    }
 
                    $song->attributes = $submitted_song;
                    if (!$song->validate()) {
                        // at least one invalid song:
                        $all_songs_valid = false;
                        // we do not 'break' here since we want validation on all song at the same shot
                    }
                    else {
                        // put aside the valid song to be saved
                        $songs_to_save[] = $song;
                    }
                }
 
                // while we know that songs were submitted, determine if to show 'adding songs' or no.
                // a check whether the max songs per artist was exceeded was performed already above.
                if (count($_POST['Song']) == Song::MAX_SONGS_PER_ARTIST) {
                    $enable_add_more_songs = false;
                }
            }
 
            if ($all_songs_valid && $model->validate()) {
                /* all songs (if any) were valid and artist object is valid too. Save it all in one transaction. Save first the
                 * artist as we need its id for its songs records
                 */
                $trans = Yii::app()->db->beginTransaction();
 
                try {
                    // use aux variable for manipulating the image file.
                    $image = CUploadedFile::getInstance($model, 'icon_filename');
                    // check if a new image was submitted or not:
                    if ($image) {
                        /* the only thing that might have changed in the update is the extension name of the image (if you support more than 'only jpeg').
                         * therefore, if something was submitted, and since we already know the ID of the artist (this is an update scenario), we can
                         * determine the full updated icon_filename attribute of the model prior to its save() (unlike in create action - see there...).
                         */
                        $model->icon_filename = $model->getImageFsFilename($image);
                    }
 
                    $model->save(false);
                    // save the updated image, if any
                    if ($image) {
                        $image->saveAs($model->getImageFsFilename($image));
                    }
 
                    // save songs
                    foreach ($songs_to_save as $song) {
                        $song->save(false);
                    }
                    $trans->commit();
                }
                catch (Exception $e) {
                    // oops, saving artist or its songs failed. rollback, report us, and show the user an error.
                    $trans->rollback();
                    Yii::log("Error occurred while saving (update scenario) artist or its 'songs'. Rolling back... . Failure reason as reported in exception: " . $e->getMessage(), CLogger::LEVEL_ERROR, __METHOD__);
                    Yii::app()->user->setFlash('error', Yii::t("MusicModule.forms", "Error occurred"));
                    $success_saving_all = false;
                }
 
                if ($success_saving_all) {
                    $success_msg = (count($songs_to_save) > 0) ? "Artist and song records have been updated" : "Artist record have been updated";
                    Yii::app()->user->setFlash('success', Yii::t("MusicModule.forms", $success_msg));
                    $this->redirect(array("view", "id" => $model->id));
                }
            }
        }
        else {
            // initial rendering of update form. prepare songs for printing.
            // we put it in the same variable as used for saving (that's the reason for the awkward variable naming).
            $songs_to_save = $model->songs;
        }
 
        $this->render('update', array(
            'artist' => $model,
            'songs' => (isset($songs_to_save)) ? $songs_to_save : array(new Song('insert')),
            'enable_add_more_songs' => $enable_add_more_songs,
        ));
    }
}

View files

Ok, now to the last member of the party - the view files:

views/artist/_form.php

<?php
/* @var $this ArtistController */
/* @var $artist Artist */
/* @var $songs array */
/* @var $form CActiveForm */
/* @var $enable_add_more_songs bool */
?>
 
<div class="form">
    <?php $form = $this->beginWidget('CActiveForm', array(
    'id' => 'artist-form',
    'enableAjaxValidation' => false,
    // we need the next one for transmission of files in the form.
    'htmlOptions' => array('enctype' => 'multipart/form-data'),
)); ?>
 
    <p class="note">Fields with <span class="required">*</span> are required.</p>
 
    <?php echo $form->errorSummary($artist); ?>
 
    <?php // All 'regular' artist form fields were omitted...  ?>
 
    <fieldset>
        <legend><?php echo Yii::t("MusicModule.forms", "Songs"); ?></legend>
        <div id="songs-multiple">
            <?php
            $i = 0;
            foreach ($songs as $song) {
                /* @var Song $song */
                $this->renderPartial('/songs/_form', array('model' => $song, 'counter' => $i));
                $i++;
            }
            ?>
            <?php
            // add button to 'add' a song if adding more songs is enabled
            if ($enable_add_more_songs) {
                // print the button here, which replicates the form but advances its counters
                // add the blank, extra 'song' form, with display=none... :
                echo '<div id="extra-song" style="display: none;">';
                $this->renderPartial('/song/_form', array('model' => new Song(), 'counter' => $i));
                echo "</div>";
            }
            ?>
        </div>
    </fieldset>
 
    <div class="row">
        <?php echo $form->labelEx($artist, 'icon_filename'); ?>
        <?php if (isset($update) && ($update === true)) { ?>
        <div><?php echo 'Choose new file or leave empty to keep current image.';?></div>
        <?php } ?>
        <?php echo $form->fileField($artist, 'icon_filename', array('size' => 20, 'maxlength' => 512)); ?>
        <?php echo $form->error($artist, 'icon_filename'); ?>
    </div>
 
    <div class="row buttons">
        <?php echo CHtml::submitButton($artist->isNewRecord ? 'Create' : 'Save'); ?>
    </div>
 
    <?php $this->endWidget(); ?>
</div>

views/song/_form.php

<?php
/* @var Song $song */
/* @var int $counter */
 
/*
 * Design note: in order to prevent nested forms that will possibly confuse yii we render this form using simple CHtml methods
 *  (like beginForm()) and not using CActiveForm
 */
?>
<div class="form">
    <?php
    echo CHtml::beginForm();
    ?>
    <div class="song-<?php echo $counter ?>">
        <div class="row">
            <?php
            // if this is an 'update' use case (form re-use), render also the id of the song itself (so upon submission we'll
            // know which song to update. Check if $song->id exists...
            ?>
        </div>
 
        <?php
        /*
         * Rest of the form fields should be rendered here, using CHtml::...
         */
        ?>
    </div>
 
    <?php CHtml::endForm(); ?>
</div>

Total 10 comments

#14185 report it
kernel32ddl at 2013/07/25 01:40am
Deleting Song?

Thanks for article, but what about deleting song from artist? I don't see it in code above.

Is anybody can share working code example with working frontend? Because frontend is a hardest part of this task.

#13540 report it
Priyranjan Singh at 2013/06/04 09:42pm
wonderful

Superb

#11268 report it
anilherath at 2013/01/02 01:58pm
Curious about this but cant get work yet

Hi Boaz, I am really curious about this solution but failed to get work yet after two days. I received the error; Fatal error: Call to a member function getErrors() on a non-object in C:\xampp\htdocs\app\framework\web\helpers\CHtml.php on line 1705

Can you Kindly tell me what I did wrong?

Here is the information. I have two models Children ( PK=> children_no) and Family (PK=> id, FK=> children_no)

I created module, models and controller files by Gii.

Then edit the Children Controller ActionCreate as follows.

public function actionCreate() {
        /*
         * first, do some permission's check. I use Yii's RBAC facility. YMMV, and it doesn't matter. Just do a
         * permissions check here.
         */
    //    if (!Yii::app()->user->checkAccess('create children')) {
            // not allowed... .
      //      throw new CHttpException(401);
     //   }
 
        $model = new Children('insert');
        $family = new Family('insert');
        // by default, if we already submitted the form and if we didn't, we allow adding more familys
        // this will be updated down below if needed to
        $enable_add_more_family = true;
 
        if (isset($_POST['Children'])) {
            // start optimistically by settings a few variables that we'll need
            $all_family_valid = true;
            $family_to_save = array();
            $success_saving_all = true;
 
            $model->attributes = $_POST['Children'];
 
            // use aux variable for manipulating the image file. 'icon_filename' is the attribute name in the Children model
            $image = CUploadedFile::getInstance($model, 'icon_filename');
            // we need to put something into the icon_filename of the model since otherwise validation will fail if its
            // a 'required' field (this is 'create' scenario).
            if ($image) {
                // if its not required field and image wasn't supplied this block will not be run so
                // this is safe for both 'required field' and 'non required field' use cases.
                $model->icon_filename = "(TEMP) " . $image->name;
            }
 
            // lets start handle related models that were submitted, if any
            if (isset($_POST['Family'])) {
                if (count($_POST['Family']) > Family::MAX_Family_PER_Children) {
                    /*
                     * server side protection against attempt to submit more than MAX_familyS_PER_Children
                     * this should be accompanied with a client side (JS) protection.
                     * If its accompanied with client side protection then going into this code block means our system
                     * is being abused/"tested". No need to give a polite error message.
                     */
                    throw new CHttpException(500, Yii::t("ChildrenModule.forms", "The max amount of allowed families is {max_family_num}", array('{max_family_num}' => family::MAX_Family_PER_Children)));
                }
                // now handle each submitted family:
                foreach ($_POST['Family'] as $index => $submitted_family) {
                    // We could have empty familys which means empty submitted forms in POST. Ignore those:
                    if ($submitted_family['name'] == '') {
                        // empty one - skip it, if you please.
                        continue;
                    }
 
                    // validate each submitted family instance
                    $family = new Family();
                    $family->attributes = $submitted_family;
                    if (!$family->validate()) {
                        // at least one invalid family. We'll need to remember this fact in order to render the create
                        // form back to the user, with the error message:
                        $all_family_valid = false;
                        // we do not 'break' here since we want validation on all familys at the same shot
                    }
                    else {
                        // put aside the new *and valid* family to be saved
                        $family_to_save[] = $family;
                    }
                }
 
                // while we know that familys were submitted, determine if the number of familys has exceeded its limit.
                // this will be goof when rendering back the 'create' form so no more familys-forms will be available.
                if (count($_POST['Family']) == family::MAX_Family_PER_Children) {
                    $enable_add_more_family = false;
                }
            }
 
            // Done validation. Summarize all valid/invalid information thus far and act accordingly
            if ($all_family_valid && $model->validate()) {
                /*
                 * all familys (if any) were valid and Children model is valid too. Save it all in one transaction. Save first the
                 * Children as we need its 'id' attribute for its familys records (family.Children_id is NOT NULL in the DB level
                 * and a 'required' attribute in our model).
                 */
                $trans = Yii::app()->db->beginTransaction();
 
                try {
                    // save the Children
                    $model->save(false);
                    // handle image of Children, if supplied:
                    if ($image) {
                        // family.getImageFsFilename() encapsulates the image filename setting details.
                        $image->saveAs($model->getImageFsFilename($image));
                        /**
                         * now update the model itself again with the full filename of the image file.
                         * this is the disadvantage of using filenames that include the PK of the entry - we need it first saved
                         * in the DB before we know what it is... . */
                        // icon_filename is a VARCHAR(512) in the DB (for long filenames) and 'string' in the Children model
                        $model->icon_filename = $model->getImageFsFilename($image);
                        $model->save(false);
                    }
 
                    // save the familys
                    foreach ($family_to_save as $family) {
                        $family->children_id = $model->children_no;
                        $family->save(false);
                    }
 
                    // here, it means no exception was thrown during saving of Children and its familys (from the DB, for example).
                    // good - now commit it all...:
                    $trans->commit();
                }
                catch (Exception $e) {
                    // oops, saving Children or its familys failed. rollback, report us, and show the user an error.
                    $trans->rollback();
                    Yii::log("Error occurred while saving Children or its 'family'. Rolling back... . Failure reason as reported in exception: " . $e->getMessage(), CLogger::LEVEL_ERROR, __METHOD__);
                    Yii::app()->user->setFlash('error', Yii::t("ChildrenModule.forms", "Error occurred"));
                    $success_saving_all = false;
                }
 
                if ($success_saving_all) {
                    // everything's done. Would you believe it?! Go to 'view' page :)
                    $success_msg = (count($family_to_save) > 0) ? "Children and family records has been created successfully!" : "Children record has been created successfully!";
                    Yii::app()->user->setFlash('success', Yii::t("ChildrenModule.forms", $success_msg));
                    $this->redirect(array("view", "id" => $model->children_no));
                }
            }
        }
 
        $this->render('create', array(
            'children' => $model,
            'family' => (isset($family_to_save)) ? $family_to_save : array(new Family('insert')),
            'enable_add_more_family' => $enable_add_more_family,
        ));
    }

Then I edited children/_form

<?php
/* @var $this ChildrenController */
/* @var $model Children */
/* @var $form CActiveForm */
/* @var $family array */
/* @var $enable_add_more_family bool */
?>

<div class="form">

<?php $form=$this->beginWidget('CActiveForm', array(
    'id'=>'children-form',
    'enableAjaxValidation'=>false,
    'htmlOptions' => array('enctype' => 'multipart/form-data'),
)); ?>

    <p class="note">Fields with <span class="required">*</span> are required.</p>

    <?php echo $form->errorSummary($children); ?>

         <fieldset>
        <legend><?php echo Yii::t("ChildrenModule.forms", "Family"); ?></legend>
        <div id="family-multiple">
            <?php
            $i = 0;
            foreach ($family as $family) {
                /* @var Song $song */
                $this->renderPartial('/family/_form', array('model' => $family, 'counter' => $i));
                $i++;
            }
            ?>
            <?php
            // add button to 'add' a song if adding more songs is enabled
            if ($enable_add_more_family) {
                // print the button here, which replicates the form but advances its counters
                // add the blank, extra 'song' form, with display=none... :
                echo '<div id="extra-family" style="display: none;">';
                $this->renderPartial('/family/_form', array('model' => new Family(), 'counter' => $i));
                echo "</div>";
            }
            ?>
        </div>
    </fieldset>

        <div class="row">
        <?php echo $form->labelEx($children, 'photo'); ?>
        <?php if (isset($update) && ($update === true)) { ?>
        <div><?php echo 'Choose new file or leave empty to keep current image.';?></div>
        <?php } ?>
        <?php echo $form->fileField($children, 'photo', array('size' => 20, 'maxlength' => 512)); ?>
        <?php echo $form->error($children, 'photo'); ?>
    </div>

    <div class="row buttons">
        <?php echo CHtml::submitButton($children->isNewRecord ? 'Create' : 'Save'); ?>
    </div>

    <?php $this->endWidget(); ?>
</div>

Then Family/_form

<?php
 
/* @var $model Family */
/* @var int $counter */
?>
<div class="form">
    <?php
    echo CHtml::beginForm();
    ?>
    <div class="family-<?php echo $counter ?>">
        <div class="row">
            <?php
            // if this is an 'update' use case (form re-use), render also the id of the song itself (so upon submission we'll
            // know which song to update. Check if $song->id exists...
            ?>
        </div>
 
        <?php
        /*
         * Rest of the form fields should be rendered here, using CHtml::...
         */
        ?>
    </div>
 
    <?php CHtml::endForm(); ?>
</div>

Thanks

#9921 report it
Boaz at 2012/09/21 03:32pm
@Lothaire

Thanks for sharing your feedback. Strange, I get the form values ok on submission in Yii with that code. Yet, the section which was problematic for you was lightly tested by me (since its relating little JS which the is not written yet - man power issues... :) . Soon I'll need to make sure everything is working ok and then I'll add more code here (probably the JS) and fix the things that need fixing (like what you noted).

#9920 report it
Lothaire at 2012/09/21 03:24pm
Sub-forms for 'Songs'

I have finally figured out how to have the submit button of the form send both the Artist and Songs data to the create action of the Artist controller. What I did is remove the CHtml::beginForm command from the Song create view and manually populated the name attribute of the Song form fields as Song[$counter][name] so Yii would generate the correct name and id attributes for the "sub-form" elements.

#9860 report it
Lothaire at 2012/09/16 10:35pm
Sending Song form data in POST along with artist form submit button

Hi,

Thank you for this great tutorial. I'm exactly in the same situation as the one you are describing in this tutorial and it has helped me a lot. The only place where I am stuck is that even when I use CHtml::beginForm for my "song" sub-forms, Yii is still inserting form tags within the main Artist form, and the artist form submit button only POSTs my artist fields. None of the values I enter in the song fields get passed. Any tip on how I should modify my code so that the Artist submit button sends all the fields at once? Thanks in advance.

Lothaire

#9805 report it
Boaz at 2012/09/11 02:15pm
@harilal

Hi,

I'm sorry but I cannot release the site at its entirety. The site I'm developing at the moment is a commercial project site which I cannot release as is. I've release 12 extensions from the work I've done on it thus far and the work is not over yet. I have some very exciting extensions (IMHO) to be developed and released.

The code above is pretty full and not fragmented. The only things which I didn't include are the 'create.php' and 'update.php' view files but those are really basic and don't go much beyond what Gii gives you when you use its CRUD generator.

#9804 report it
harilal at 2012/09/11 01:33pm
Full source please...

Can you please zip and upload the whole site.

#9784 report it
Boaz at 2012/09/10 07:46am
re. Factoring (@François Gannaz)

Thanks for the feedback!

I agree that if the article includes complete (or almost complete...) code then it can be even better with success messages. I've added those.

As for the other things you mention:

a parsing and validating method in the controller with a boolean parameter that toggles create or update mode

I think that such a refactor will result in a code that includes too many if-else (or switch case) constructs and I find it less readable and less intuitive. That's MHO at least. I prefer simplicity over length in this case.

a saving method in the Artist model that would save the model and its attached objects ($songs_to_save as an optional parameter).

That's do-able.

naming convention uses camelCase instead of names_with_underscores

I prefer method-internal variables to use underscores as separators and in class and inter-class to use camelCase. Also, I consider data variables passed to view as 'simple variables' and thus choose naming convention like the one used for internal variable (e.g.: $songs_to_save). I guess this is a matter of personal taste.

#9780 report it
François Gannaz at 2012/09/10 05:10am
Factorizing

I think reading code is always a good way to improve, so I like this kind of full example. IMHO, some points could be largely improved, though.

Your actions in the controller are very long and duplicate most of their code. I suggest you to factorize your code with:

  • a parsing and validating method in the controller with a boolean parameter that toggles create or update mode,
  • a saving method in the Artist model that would save the model and its attached objects ($songs_to_save as an optional parameter).

If you want to be complete, then before redirecting, you should add a flash message. As a user, I hate it when I get no message responding to my action.

A last detail: in modern PHP (e.g. PDO) and all the PHP frameworks that I know (including Yii, see also PSR-1), the naming convention uses camelCase instead of names_with_underscores.

Leave a comment

Please to leave your comment.

Write new article