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 ;
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
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.
Related
Objective: Person can follow any other Person or Organization.
/**
* PersonFollow schema:
CREATE TABLE `person_follows` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`person_id` int(11) DEFAULT NULL,
`table_id` int(11) DEFAULT NULL,
`table_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_person_id` (`person_id`),
KEY `i_table_id` (`table_id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
*/
class PersonFollow extends AppModel {
public $belongsTo = array(
'Person' => array(
'foreignKey' => 'person_id',
),
'FollowPerson' => array(
'className' => 'Person',
'foreignKey' => 'table_id',
//'conditions' => array('PersonFollow.table_name' => 'persons'), // tried this
),
'FollowOrganization' => array(
'className' => 'Organization',
'foreignKey' => 'table_id',
//'conditions' => array('PersonFollow.table_name' => 'organizations'), // tried this
),
);
}
class Person extends AppModel {
public $hasMany = array(
'Following' => array(
'className' => 'PersonFollow',
'dependent' => true,
),
'Follower' => array(
'className' =>'PersonFollow',
'foreignKey' => 'table_id',
'conditions' => array('Follower.table_name' => 'persons'),
'dependent' => true
),
);
}
class Organization extends AppModel {
public $hasMany = array(
'Follower' => array(
'className' =>'PersonFollow',
'foreignKey' => 'table_id',
'conditions' => array('Follower.table_name' => 'organizations'),
'dependent' => true
/* Tried this
'foreignKey' => false,
'finderQuery' => 'SELECT `PersonFollow`.*
FROM `person_follows` as `PersonFollow`
WHERE `PersonFollow`.`table_id` = {$__cakeForeignKey__$}
AND `PersonFollow`.`table_name` = "organizations"',
*/
)
);
}
$following = $this->Person->find('all', array(
'contain' => array('Following' => array('FollowPerson','FollowOrganization')),
'conditions' => array('Person.id' => 1234),
));
Results for $following return all Following that match on table_id, and does not use table_name condition. I'm trying to get FollowOrganization where PersonFollow.table_name = 'organizations' and same for FollowPerson where PersonFollow.table_name = 'persons'. It appears $hasMany is only using the 'foreignKey' and not the 'conditions'. What am I missing?
I have the following relations:
class Platformuser extends AppModel {
public $hasAndBelongsToMany = array(
'Service'
);
}
class Service extends AppModel {
public $hasAndBelongsToMany = array(
'Platformuser'
);
}
I am doing a action on the PlatformusersController to get the services associated with this user with the following query:
$this->Platformuser->find('all', array(
'conditions' => array('Platformuser.id' => $userId),
));
It returns everything about Platformuser/Service and I ONLY want the data of the service:
array(
(int) => array(
[Platformuser] => array(
[id] => [1]
[name] => [Domingo]
),
[Service] => array(
(int) 0 => array(
[id] => [1]
[name] => [dropbox],
[PlatformusersService] => array(
[id] => [1],
[platformuser_id] => [1],
[service_id] => [1],
[modified] => [2013-10-10 00:00:00],
[created] => [2013-10-10 00:00:00];
)
)
)
)
I want something like:
array(
[Service] => array(
(int) 0 => array(
[id] => [1]
[name] => [dropbox]
)
)
Any ideas?.
You should use the containable behavior. It's easy to use and you'll able to fetch data according to your need like.
$plateFormuser = $this->Platformuser->find('first', array(
'conditions' => array('Platformuser.id' => $userId),
'contain' => array(
'Service' => array(
'fields' => array('id', 'name')
)
)
));
$services = Set::merge(
Set::classicExtract($plateFormuser, 'Service.{n}.{id}'),
Set::classicExtract($plateFormuser, 'Semester.{n}.{name}')
);
Now you can json encode the $services to get result as you've mentioned [{"id":"1", "name":"dropbox"}].
I'm trying to get my associated models in CakePHP 2.3 to save properly, but I'm having issues. I'm storing posts, and I want to know what links are in those posts. For each of those links, I'd like to store anchor text if it is available. My database is set up as in the following diagram.
(source: derekperkins.com)
Anchor Model
class Anchor extends AppModel {
public $hasMany = array(
'PostsUrl' => array(
'className' => 'PostsUrl',
'foreignKey' => 'anchor_id',
'dependent' => false
)
);
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Anchor::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('anchor' => $data['anchor'])
));
if( $id )
$data['id'] = $id['Anchor']['id'];
return parent::save($data, $validate, $fieldList);
}
}
URL Model
class Url extends AppModel {
public $hasMany = array(
'PostsUrl' => array(
'className' => 'PostsUrl',
'foreignKey' => 'url_id',
'dependent' => false
)
);
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Url::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('url' => $data['url'])
));
if( $id )
$data['id'] = $id['Url']['id'];
return parent::save($data, $validate, $fieldList);
}
}
PostsUrl Model
class PostsUrl extends AppModel {
public $belongsTo = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'post_id'
),
'Url' => array(
'className' => 'Url',
'foreignKey' => 'url_id'
'Anchor' => array(
'className' => 'Url',
'foreignKey' => 'anchor_id'
)*/
);
}
Post Model
class Post extends AppModel {
public $hasMany = array(
'PostsUrl' => array(
'className' => 'PostsUrl',
'foreignKey' => 'post_id',
'dependent' => false
)
);
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Post::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('external_post_id' => $data['external_post_id'])
));
if( $id )
$data['id'] = $id['Post']['id'];
return parent::save($data, $validate, $fieldList);
}
}
Submitting Data
I've created a form to test my model. This is the code I'm using to save the array created by the form. I am getting a message saying that things saved successfully, but only the post saves. Nothing is entered into the other three tables. I'm also using DebugKit and no SQL calls reference any of that data.
$this->Post->saveAssociated($this->request->data, array('deep' => true))
Array
(
[Post] => Array
(
[external_post_id] => 12345
[sentiment_score] => 3.3
)
[URL] => Array
(
[url] => http://test.com
)
[Anchor] => Array
(
[anchor] => Test Anchor
)
)
I've also tried formatting my arrays to have the URL and Anchor underneath PostsUrl as a subarray, but that didn't work either.
My Model::save functions are there to keep me from duplicating data, and they work properly in other models I have used in the past (though I'm open to suggestions on a better way to do this, as this uses a database call for each check). I've also tried commenting them out, and it doesn't affect my code. How should I structure this to save properly?
First of all about your Model::save functions, for example:
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Post::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('external_post_id' => $data['external_post_id'])
));
if( $id )
$data['id'] = $id['Post']['id'];
pr($data);
return parent::save($data, $validate, $fieldList);
}
it prints $data in this way :
Array
(
[id] => 3 //for example 3
[Post] => Array
(
[external_post_id] => 12345
[sentiment_score] => 3.3
)
[URL] => Array
(
[url] => http://test.com
)
[Anchor] => Array
(
[anchor] => Test Anchor
)
)
this $data is incorrect, the correct data is:
Array
(
[Post] => Array
(
[id] => 3 //for example 3
[external_post_id] => 12345
[sentiment_score] => 3.3
)
[URL] => Array
(
[url] => http://test.com
)
[Anchor] => Array
(
[anchor] => Test Anchor
)
)
You must change your Model::save function this way:
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Post::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('external_post_id' => $data['external_post_id'])
));
if( $id )
$data[$this->name]['id'] = $id['Post']['id'];
return parent::save($data, $validate, $fieldList);
}
second , you can't save this data with single save, you should save your data this way:
$postData = array
(
'Post' => array
(
'external_post_id' => 12345
'sentiment_score' => 3.3
)
);
$this->Post->save($data);
$postUrlId = $this->PostsUrl->find('first', array(
'conditions' => array(
'post_id' => $this->Post->id
),
'fields' => array('id')
));
$urlAnchorData = array(
'URL' => array
(
'url' => 'http://test.com'
),
'Anchor' => array
(
'anchor' => 'Test Anchor'
),
'PostsUrl' => array(
'id' => $postUrlId['PostsUrl']['id']
)
);
$this->PostsUrl->saveAll('$urlAnchorData');
Hy i am trying to create a follow feature to my website so user can follow each other , I am using Cakephp what kind of relation should i use , what should i name the tables.
NB: I created a user table + follow table containing user_id and follower_id !
If you don't need to save any information about the relation, then hasAndBelongsToMany is the natural relation to use in this case.
Try this:
// User Model
var $hasAndBelonsToMany = array(
'Follower' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'associationForeignKey' => 'follower_id'
'joinTable' => 'followers_users'
)
)
then you must create the users table as normal, and a table 'followers_users' with columns: 'id', 'user_id', 'follower_id' (and 'created' and 'updated' if you need them).
EDIT :
To retrieve your data (read here) you do it as usual:
$this->User->find('all', array('conditions' => array('id' => 1)));
Then you'll get an array like:
Array(
[User] => array(
[id] => 1
[name] => xxxx
)
[Follower] => array(
[0] => array(
[id] => 2
[name] => yyyy
)
[1] => array(
[id] => 3
[name] => zzzz
)
)
)
To save your data (read here and here), you need to create an array like:
array(
[User] => array(
[id] => 1
[name] => xxx
)
[Follower] => array(
[0] => array(
[id] => 2
)
[1] => array(
[id] => 3
)
)
)
Explanation of what you are looking / example is right in the book:
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#multiple-relations-to-the-same-model
Similar example per the book:
"It is also possible to create self associations as shown below:"
<?php
class Post extends AppModel {
public $name = 'Post';
public $belongsTo = array(
'Parent' => array(
'className' => 'Post',
'foreignKey' => 'parent_id'
)
);
public $hasMany = array(
'Children' => array(
'className' => 'Post',
'foreignKey' => 'parent_id'
)
);
}
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'
)
);
Will this be the correct format of array that is needed to save this with saveAll? This doesn't save the CategoryPost model. If it isn't what should be the format of the array?
Array
(
[Post] => Array
(
[title] => query
[body] =>
query
[page_id] => 122
[modified] => 2010-12-30 23:33:47
[created] => 2010-12-30 23:33:47
[uri] => query-9
)
[CategoryPost] => Array
(
[0] => Array
(
[category_id] => 1
)
[1] => Array
(
[category_id] => 2
)
)
[Page] => Array
(
[meta_keywords] => query
[meta_description] => query
[title] => query
[layout] => index
)
)
Well, everthing seems fine, and the CategoryPost data should be saved.
Maybe comment out the validation from the CategoryPost model and try then.