saveAll() with multiple records AND for multiple models

Update (05/23/2011): Ceeram proposed an even easier workaround. And hopefully this will be addressed in an upcoming build of cake:
http://cakephp.lighthouseapp.com/projects/42648/tickets/1704-form-helper-keying-not-working-as-expected-in-some-situations#ticket-1704-3
————
Update (05/12/2011): It appears that in 1.3.x versions of CakePHP form helper doesn’t generate the input name correctly, the workaround is to supply a name key, to override the default. Or to to use Set::extract() to remove the extra key.
————
This question kept coming up on IRC over the last few days, so I’ve decided to give this a shot as well…

Since I didn’t find anything specific in the test cases, I had to rely on some trickery (so, if someone has a more elegant approach, please share).

Let’s say we have User hasMany Comment, we’d like to store a couple of new users and for each one add a comment.

First let’s build our form (view):

<?php
    echo $form->create();

    echo $form->input('1.User.name');
    echo $form->input('1.Comment.0.comment');

    echo $form->input('2.User.name');
    echo $form->input('2.Comment.0.comment');

    echo $form->end('Save');

?>

Notice a slightly unusual naming of our fields.
Starting the field name with a numeric key index will help us to get the data array that will ultimately look something like below, when it arrives from our form to the controller (remember that you should not start a field name with a zero, especially when including the Model name… *see below for a quick explanation):

Array
(
    [1] => Array
        (
            [User] => Array
                (
                    [name] => bob
                )

            [Comment] => Array
                (
                    [0] => Array
                        (
                            [comment] => nice guy
                        )

                )

        )

    [2] => Array
        (
            [User] => Array
                (
                    [name] => dave
                )

            [Comment] => Array
                (
                    [0] => Array
                        (
                            [comment] => pain in the ass
                        )

                )

        )

)

I hope you see where I’m headed with this now, if not … well, here’s our add() action in the controller:

function add() {
        if(!empty($this->data)) {
            foreach($this->data as $data) {
                $this->User->saveAll($data);
            }
        }
    }

A little dirty? Yes…
But it wraps each model and associated model into a nice transaction and saves multiple records and models in a relatively simple manner.

—————
* So why can’t (or shouldn’t) we start our input with a zero?

Let’s try:

<?php echo $form->input('0.User.name'); ?>

The resu<

<input name=&quot;data[User][User][name]&quot; type=&quot;text&quot; maxlength=&quot;100&quot; value=&quot;&quot; id=&quot;UserUserName&quot; />

Definitely not what was intended and likely an undesired effect for majority of cases.

m4s0n501
  • http://www.twitter.com/tmaiaroto Tom

    Don’t forget that the models have callbacks too. Sometimes you want to do more complex things (saving files, etc.) or other sorts of logic where a saveAll() isn’t enough alone. Rather than code all that in the controller, it’s neater to put it into the model. Even better as a behavior if you can and need to re-use it.

    Though this is probably only under unusual cases…I personally prefer to work in models and leave my controllers completely empty (minus the app_controller).

  • http://teknoid.wordpress.com teknoid

    @Tom

    Sure, but that would completely defeat the point of this article. Best practice implementation is up to the user ;)

  • http://www.twitter.com/tmaiaroto Tom

    Oh, I agree. I just thought it was worth mentioning (and related to this good info) to not forget the wonderful callbacks in CakePHP that can be used in combination with saveAll(). Also thinking about complex associations and habtm.

  • http://teknoid.wordpress.com teknoid

    @Tom

    Sounds good, extra food for thought is always a good thing.

  • Pingback: CakePHP : signets remarquables du 25/03/2009 au 01/04/2009 | Cherry on the...()

  • Pingback: saveAll() with multiple records AND for multiple models | Dailytuts.net - Daily tutorial for peoples()

  • Pat

    Question for you:

    Suppose your “Comments” table had a date field, and you wanted to have the form helper build a date picker using the same naming convention you used in your example. How would you express that in echoing the date input element?

    When I try using something like ‘1.Comment.0.date_implemented’ as the field name in the $form->input call, the rendered selects for the date picker end up as “data[User]”.

    Note: I’ve converted model/table names to match your example. In my case the models are called “Builderform” and “BuilderformField”: where one builderform record has many related builderformfield records.

    • http://teknoid.wordpress.com teknoid

      @Pat

      Not sure, I haven’t tried it.

      Personally, I think it’s evil to make the user pick a date using selects… a simple jQuery widget will make your app much more user friendly and solve the problem you describe in one shot ;)

  • http://www.prleap.com/pr/29189/ Robert Shumake

    Your blog is so informative … ..I just bookmarked you….keep up the good work!!!!

    Hey, I found your blog in a new directory of blogs. I dont know how your blog came up, must have been a typo, anyway cool blog, I bookmarked you. :)

    -Robert Shumake Paul Nicoletti

  • http://www.jeremy-burns.co.uk JB

    This is clearly what I need, but I am struggling to get it to work. My problem is getting the data from $this->data (which is created in the controller) into the inputs on the form. My data array looks a little like this:

    Array
    (
    [1] => Array
    (
    [Order] => Array
    (
    [id] => 555

    )

    [BillingAddress] => Array
    (
    [id] =>

    )

    [OrderItem] => Array
    (
    [0] => Array
    (
    [id] => 101755

    [ShippingAddress] => Array
    (
    )

    [Product] => Array
    (
    [id] => 1

    )

    [Customer] => Array
    (
    )
    )

    [1] => Array
    (
    [id] => 101759

    [ShippingAddress] => Array

    So I have an order with many order items, each of which has a shipping address. Using your nomenclature I’d have though the input name for a field from ShippingAddress would be
    1.OrderItem.n.ShippingAddress.fieldname

    I’ve tried all sorts of permutations, but can’t get them to connect up. I appreciate some guidance!

    Thanks.

    • Paul

      Hi JB!
      Try this:

      Controller:
      $this->set(‘phones’, $this->Phone->find(‘list’));

      View:
      echo $form->input(‘1.Phone.0.’, array (‘type’ => ‘radio’));

  • http://revathskumar.com RSK

    it will be good if you specify to which version this tut is applicable.

    is there any change came in cakePHP 1.3. “for saveAll() with multiple records AND for multiple models”

    i need to save something like

    Array
    (
    [0] => Array
    (
    [Alert] => Array
    (
    [alert_title] => Tour Plan
    [alert_description] => chk
    [created_ID] => 1
    [created_Date] => 2011-02-04 12:23:08
    )

    [AlertView] => Array
    (
    [0] => Array
    (
    [grade_id] => 1
    [employ_id] => 1
    )

    [1] => Array
    (
    [grade_id] => 2
    [employ_id] => 2
    )

    )

    )

    [1] => Array
    (
    [Alert] => Array
    (
    [alert_title] => Tour Plan
    [alert_description] => chk
    [created_ID] => 1
    [created_Date] => 2011-02-04 12:23:08
    )

    [AlertView] => Array
    (
    [0] => Array
    (
    [grade_id] => 3
    [employ_id] => 3
    )

    [1] => Array
    (
    [grade_id] => 4
    [employ_id] => 4
    )

    )

    )

    )

    still i need to use loop for this??

  • teknoid

    @RSK

    I don’t think there was any significant change to the saveAll() logic in 1.3.
    When in doubt always look to the test cases.

    • http://revathskumar.com RSK

      thankz alot for quick reply.
      will look at the test cases. :)

  • Ceeram

    http://cakephp.lighthouseapp.com/projects/42648/tickets/1704-form-helper-keying-not-working-as-expected-in-some-situations#ticket-1704-3

    put an extra dot in front of the numeric key, and it will work as this blog post expects. Note the addition of type => text for a 2nd and more comments for a single user.

  • Bubba

    I figured out the saveAll part before reading your blog. That part I am having trouble is with catching all the validation errors, while using the for loop. Is there away to do that?

  • teknoid

    @Ceeram

    Awesome, thanks for the tip ;)

    @Bubba

    Sorry don’t know off top of my head… and unfortunately I have little time to do some testing now.

  • http://baffleinc.com/ Harley

    Hey guys.

    So this problem has been the bane of my existence for the past two weeks. for anybody who is interested, this is my solution:

    saving multiple widgets AND widgetItems in one go.

    //widget controller
    $saveSuccess = false;

    if(!empty($this->data[‘Widget’])) {
    $widget_count = 0;
    $menuIdToPass = $menu_id;
    foreach($this->data[‘Widget’] as $widgetKey => $widget) :
    $widgetData = array(
    ‘title’ => $widget[‘title’],
    ‘menu_id’ => $widget[‘menu_id’]
    );
    $saveableWidget = Set::insert($widget, ‘Widget’, $widgetData);
    if($this->Widget->saveAll($saveableWidget)) : $saveSuccess = true; endif;
    $widget_count++;
    endforeach;

    if ($saveSuccess) {

    $this->Session->setFlash(__($widget_count.’ sections have been added to the Menu. Now please order them.’, true));
    $this->redirect(array(‘controller’ => ‘menus’, ‘action’ => ‘index’));

    }

    for this to work, your foreached data array MUST be as so:

    [n or $widget] => array(
    [widget] => array
    (
    [title] => cocktails
    [menu_id] => 3
    )
    [widgetItem] => array
    (
    [0] => Array
    (
    [item] => Mojhito
    [price] => 45
    )
    [1] => Array
    (
    [item] => Mojhito
    [price] => 45
    )
    )
    )

    Finally, you need to set up your add with inputs that end up as Widget.0.title and Widget.0.WidgetItem.1.item.

    hope that helps anybody. it sure killed me.

  • teknoid

    @Harley

    Thanks for sharing. But what’s the problem with the solution posted?

    Seems like unnecessary logic in the controller using the latter approach.

  • http://sharedcorner.com/ ianemv

    very helpful. thanks for the heads-up. now, i can play around with multiple rows :)

  • Ian

    The lighthouseapp link doesn’t work any more (at least its not public) – I think they’ve shut it down in favour of github. Might be useful to put Ceeram’s suggestion into the top of the page instead of just referring to his comment.