1

I've been reading the cookbook for a while now and still don't get how I'm supposed to do this:

My original problem was this: A related Model isn't being validated

From RabidFire's commment:

If you want to count the number of Category models that a new Post is associated with (on save), then you need to do this in the beforeSave function as I've mentioned. As you've currently set up your models, you don't need to use the multiple rule anywhere. If you really, really want to validate against a list of Category IDs for some reason, then create a join model, and validate category_id with the multiple rule there.

Now, I have these models and are now validating. The problem now is that data isn't being saved in the Join Table:

class Post extends AppModel {
    var $name = 'Post';
    var $hasMany = array(
        'CategoryPost' => array(
            'className' => 'CategoryPost'
        )
    );
    var $belongsTo = array(
        'Page' => array(
            'className' => 'Page'
        )
    );

class Category extends AppModel {
    var $name = 'Category';
    var $hasMany = array(
        'CategoryPost' => array(
            'className' => 'CategoryPost'
        )
    );

class CategoryPost extends AppModel {
    var $name = 'CategoryPost';
    var $validate = array(
        'category_id' => array(
            'rule'     => array('multiple', array('in' => array(1, 2, 3, 4))),
            'required' => FALSE,
            'message'  => 'Please select one, two or three options'
        )
    );
    var $belongsTo = array(
        'Post' => array(
            'className' => 'Post'
        ),
        'Category' => array(
            'className' => 'Category'
        )
    );

This is the new Form:

<div id="content-wrap">
    <div id="main">
            <h2>Add Post</h2>
            <?php echo $this->Session->flash();?>
            <div>
            <?php
            echo $this->Form->create('Post');
            echo $this->Form->input('Post.title');
            echo $this->Form->input('CategoryPost.category_id', array('multiple' => 'checkbox'));
            echo $this->Form->input('Post.body', array('rows' => '3'));

            echo $this->Form->input('Page.meta_keywords');
            echo $this->Form->input('Page.meta_description');

            echo $this->Form->end('Save Post');
            ?>
            </div>
    <!-- main ends -->
    </div>

The data I am producing from the form is as follows:

Array
(
    [Post] => Array
        (
            [title] => 1234
            [body] => 

1234

        )

    [CategoryPost] => Array
        (
            [category_id] => Array
                (
                    [0] => 1
                    [1] => 2
                )

        )

    [Page] => Array
        (
            [meta_keywords] => 1234
            [meta_description] => 1234
            [title] => 1234
            [layout] => index
        )

)

UPDATE: controller action //Controller action

function admin_add() {
    // pr(Debugger::trace());
    $this->set('categories', $this->Post->CategoryPost->Category->find('list'));

    if ( ! empty($this->data)) {

        $this->data['Page']['title'] = $this->data['Post']['title'];
        $this->data['Page']['layout'] = 'index';

        debug($this->data);
        if ($this->Post->saveAll($this->data)) {
            $this->Session->setFlash('Your post has been saved', 'flash_good');
            $this->redirect($this->here);
        }
    }
}

UPDATE #2: Should I just do this manually?

The problem is that the join tables doesn't have things saved in it. Is there something I'm missing?

removed update #3

Join Table schema:

CREATE TABLE IF NOT EXISTS `category_posts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) NOT NULL,
  `post_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
);

Update: #4

I will put everything about my application here in the hopes of getting to do what I want to do.

// Post Model
class Post extends AppModel {
    var $name = 'Post';
    var $hasMany = array(
        'CategoryPost' => array(
            'className' => 'CategoryPost'
        )
    );
    var $belongsTo = array(
        'Page' => array(
            'className' => 'Page'
        )
    );
    var $actsAs = array('Containable');
    var $virtualFields = array(
        'date_posted' => 'DATE_SUB(Post.created, INTERVAL 7 DAY)'
    );

    var $order = array('Post.modified' => 'desc');
    var $validate = array(
        'title' => array(
            'rule' => 'notEmpty'
        ),
        'body' => array(
            'rule' => 'notEmpty'
        )
    );

    function getFeed() {
        if ($posts = $this->find('all', array('limit' => 20, 'order' => 'Post.created DESC'))) {
            return $posts;
        }
        return FALSE;
    }

    function getRecentPosts() {
        $conditions = array(
            'Post.created < (curdate() + interval 7 day)',
        );
        return $this->find('all', array('limit' => 8, 'conditions' => $conditions));
    }
}


// CategoryPost Model
class CategoryPost extends AppModel {
    var $name = 'CategoryPost';


    var $validate = array(
    'category_id' => array(
        'rule'     => array('multiple', array('in' => array(1, 2, 3, 4))),
        'required' => FALSE,
        'message'  => 'Please select one, two or three options'
    )
    );

    var $belongsTo = array(
        'Post' => array(
            'className' => 'Post'
        ),
        'Category' => array(
            'className' => 'Category'
        )
    );

    var $actsAs = array('Containable');
}

class Page extends AppModel {
    var $name = 'Page';
    var $order = array('Page.modified' => 'desc');

    var $hasOne = array(
        'Post' => array(
            'className' => 'Post'
        ));

    var $hasMany = array(
        'Snippet' => array(
            'className' => 'Snippet'
        ));

    var $validate = array(
        'title' => array(
            'rule' => 'notEmpty'
        ),
        'uris' => array(
            'slugged' => array(
                'rule' => '/^[a-z0-9-_]+$/i',
                'message' => 'This field should only contain characters, numbers, dashes and underscores'
            ),
            'uniqueUrl' => array(
                'rule' => array('uniqueUrl'),
                'message' => 'A page has already acquired this url'
            )
        ),
        'meta_keywords' => array(
            'rule' => 'notEmpty'
        ),
        'meta_description' => array(
            'rule' => 'notEmpty'
        ),
        'layout' => array(
            'rule' => 'notEmpty'
        )
    );
}


// Form
<div id="main">
        <h2>Add Post</h2>
        <?php echo $this->Session->flash();?>
        <div>
        <?php
        echo $this->Form->create('Post');
        echo $this->Form->input('Post.title');
        echo $this->Form->input('CategoryPost.category_id', array('multiple' => 'checkbox'));
        echo $this->Form->input('Post.body', array('rows' => '3'));

        echo $this->Form->input('Page.meta_keywords');
        echo $this->Form->input('Page.meta_description');

        echo $this->Form->end('Save Post');
        ?>
        </div>
<!-- main ends -->
</div>


// Posts#admin_add
function admin_add() {
    $this->set('categories', $this->Post->CategoryPost->Category->find('list'));

    if ( ! empty($this->data)) {

        $this->data['Page']['title'] = $this->data['Post']['title'];
        $this->data['Page']['layout'] = 'index';


        if ($this->Post->saveAll($this->data, array('validate' => 'first'))) {
            $this->Session->setFlash('Your post has been saved', 'flash_good');
            $this->redirect(array('action' => 'admin_add'));
        }

    }
}


// Table structure
CREATE TABLE IF NOT EXISTS `posts` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `page_id` int(11) NOT NULL,
  `title` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  `uri` varchar(127) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `body` text COLLATE utf8_unicode_ci,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=163 ;


CREATE TABLE IF NOT EXISTS `pages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
  `uris` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
  `meta_keywords` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `meta_description` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `layout` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=164 ;


CREATE TABLE IF NOT EXISTS `category_posts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) NOT NULL,
  `post_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=36 ;
Community
  • 1
  • 1
Teej
  • 12,764
  • 9
  • 72
  • 93

2 Answers2

2

Your data is not being saved because you're trying to save an array into a field.

[CategoryPost] => Array
    (
        [category_id] => Array
            (
                [0] => 1
                [1] => 2
            )
    )

The form of the data should look like this:

[CategoryPost] => Array
    (
        [0] => Array
            (
                [category_id] => 1
            )
        [1] => Array
            (
                [category_id] => 2
            )
    )

You could use the following code to do this:

// Correcting data form of CategoryPost
$categoryPosts = array();
foreach ($this->data['CategoryPost']['category_id'] as $categoryId) {
    $categoryPost = array(
        'category_id' => $categoryId
    );
    array_push($categoryPosts, $categoryPost);
}
$this->data['CategoryPost'] = $categoryPosts;

This code can be placed in the controller before the saveAll call. If you find that you're using this code in multiple places, you can refactor it into the model's beforeSave:

function beforeSave() {
    if (isset($this->data['CategoryPost']['category_id']) && is_array($this->data['CategoryPost']['category_id'])) {
        ... // above code
    }
}

Post the schema of your join table (table name and fields) for a possibly better alternative.


Alright, it took me some brain racking but I finally figured it out. Your last comment kind of put everything in place. Here's the simple fix:

In your view:

echo $this->Form->input('CategoryPost.0.category_id', array('multiple' => 'checkbox'));

This should cause the data to be of the following form:

[CategoryPost] => Array
    (
        [0] => Array
            (
                [category_id] => Array
                    (
                        [0] => 1
                        [1] => 2
                    )
            )
    )

You need to have it in this form because of the hasMany relationship between Post and CategoryPost - or it won't even validate. After you make this change, the beforeSave function will be called. NOW make the necessary changes to beforeSave to make it work! Debugging $this->data will help. I leave this part to you, cause I have another better alternative:

1) Shift the multiple validation rule to the Post model:

class Post extends AppModel { 
    ...
    var $validate = array(
        'category_id' => array(
            'rule'     => array('multiple', array('in' => array(1, 2, 3, 4))),
            'required' => false,
            'message'  => 'Please select one, two or three options'
        )
    );
    ...
}

2) Change the view accordingly:

echo $this->Form->input('Post.category_id', array('multiple' => 'checkbox'));

3) Shift the beforeSave to the Post model:

function beforeSave() {
    if (isset($this->data['Post']['category_id']) && is_array($this->data['Post']['category_id'])) {
        $categoryPosts = array();
        foreach ($this->data['Post']['category_id'] as $categoryId) {
            $categoryPost = array(
                'category_id' => $categoryId
            );
            array_push($categoryPosts, $categoryPost);
        }
        $this->data['CategoryPost'] = $categoryPosts;
    }
    return true;
}

This should keep things nice and smooth. Test out both alternatives and let me know if it works! :D

RabidFire
  • 6,280
  • 1
  • 28
  • 24
  • Yeah, I did this already and I think this one works. I might have just coded it differently. Anyway, I will look if it worked. I was on vacation. – Teej Dec 30 '10 at 14:59
  • RabidFire, I've used your code (I tried this code before)and it seems that when it saves, it reverts back to the old array before saving. I'm confused. – Teej Dec 30 '10 at 18:52
  • I even tried hardcoding a sample and it doesn't work: *$this->data['CategoryPost'][0]['category_id'] = 1;* – Teej Dec 30 '10 at 18:53
  • Try placing it in the `beforeSave` of your `CategoryPost` model. – RabidFire Dec 31 '10 at 04:35
  • it doesn't work. in fact, the beforeSave in the CategoryPost doesn't run at all. – Teej Dec 31 '10 at 06:28
  • Hm.... Then most probably the validation is failing. You can check this by setting `$this->Post->saveAll($this->data, array('validate' => 'first'))` which will NOT save the Post data if any related model's validation fails. Try it and let us know if this happens. – RabidFire Dec 31 '10 at 07:14
  • it still saves the page and post but not the category_post when I wrote the `array('validate' => 'first')` – Teej Jan 04 '11 at 14:28
  • I can confirm that returning FALSE from CategoryPost::beforeSave doesn't stop saving the Post and Page. – Teej Jan 04 '11 at 14:35
  • @Thorpe - That helped me figure out what the problem was. Edited my answer to reflect the findings. :) – RabidFire Jan 04 '11 at 16:01
  • @Rabidfire. It now works. but the category_posts.post_id = `0`. – Teej Jan 04 '11 at 18:48
  • Rabidfire, Post.id doesn't exist yet in the beforeSave. It's one reason we might not be getting the post_id for category_posts. – Teej Jan 04 '11 at 20:33
  • @Thorpe - Sorry, there was a typo. `CategoryPost.0.category_id` -- I missed the `0`. CakePHP will automatically assign the `post_id`. Validation will only fail if you are trying to validate for a non-empty post_id (which you aren't), so you don't have to worry about that. – RabidFire Jan 05 '11 at 04:11
  • @Thorpe - Made another edit to my answer. `$this->data['CategoryPost'] = $categoryPosts;` should assign the CategoryPost correctly and save `post_id` assuming that CakePHP carries on the same data for saveAll everywhere. You need to test it out to confirm. – RabidFire Jan 05 '11 at 04:15
  • @RabidFire, I was following your solution. I haven't worked the first. I was working on your alternative. – Teej Jan 05 '11 at 04:19
  • @Thorpe - Yea, you'd posted 2 comments and I missed the first. I made an edit to the alternative solution in the `beforeSave`. Just assign $categoryPosts back into the data and let it return true. – RabidFire Jan 05 '11 at 04:31
  • RabidFire, that last one didn't save anything at all. I modified my code to fit the question and gave all information that I think would be helpful. – Teej Jan 09 '11 at 17:44
0

I think that the element for multiple choice should have name like this:

echo $this->Form->input('Category', array('multiple' => 'checkbox'));

For better results create a backup copy of your view file and recreate it using the console bake script.

Nik Chankov
  • 6,049
  • 1
  • 20
  • 30
  • If you read the question, you'd see the previous question I asked. This was the original setup. I changed to my current setup to have validation. – Teej Dec 27 '10 at 08:47
  • Anyway, I tried to bake the app, but it doesn't do the validation on the Categories. – Teej Dec 27 '10 at 09:43