Polymorphic associations in CakePHP2

I have 3 models, Page , Course and Content

Page and Course contain metadata, and Content contains HTML content.

Page and Course both hasMany Content

Content belongs to Page and Course

To avoid the course_id and course_id in Content (because I want it to scale more than just 2 models), I consider using polymorphic associations. I started by using Polymorphic Behavior in Bakery, but it generates waaay too many SQL queries to my liking, as well as throwing an โ€œIllegal Offsetโ€ error that I donโ€™t know how to fix (it was written in 2008, and nobody seems to , did not mention it recently, so maybe the error is due to the fact that it was not intended for Cake 2?)

In any case, I found that I can almost do everything I need by hard-coding the associations in the models as such:

Page Model

 CREATE TABLE `pages` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `slug` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) ) <?php class Page extends AppModel { var $name = 'Page'; var $hasMany = array( 'Content' => array( 'className' => 'Content', 'foreignKey' => 'foreign_id', 'conditions' => array('Content.class' => 'Page'), ) ); } ?> 

Course Model

 CREATE TABLE `courses` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `slug` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) ) <?php class Course extends AppModel { var $name = 'Course'; var $hasMany = array( 'Content' => array( 'className' => 'Content', 'foreignKey' => 'foreign_id', 'conditions' => array('Content.class' => 'Course'), ) ); } ?> 

Content Model

 CREATE TABLE IF NOT EXISTS `contents` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `class` varchar(30) COLLATE utf8_unicode_ci NOT NULL, `foreign_id` int(11) unsigned NOT NULL, `title` varchar(100) COLLATE utf8_unicode_ci NOT NULL, `content` text COLLATE utf8_unicode_ci NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) <?php class Content extends AppModel { var $name = 'Content'; var $belongsTo = array( 'Page' => array( 'foreignKey' => 'foreign_id', 'conditions' => array('Content.class' => 'Page') ), 'Course' => array( 'foreignKey' => 'foreign_id', 'conditions' => array('Content.class' => 'Course') ) ); } ?> 

It's good that $this->Content->find('first') generates only one SQL query instead of 3 (as was the case with Polymorphic Behavior), but the problem is that the returned dataset includes both belongsTo models whereas it should be truly return that which exists. Here's what the returned data looks like:

 array( 'Content' => array( 'id' => '1', 'class' => 'Course', 'foreign_id' => '1', 'title' => 'something about this course', 'content' => 'The content here', 'created' => null, 'modified' => null ), 'Page' => array( 'id' => null, 'title' => null, 'slug' => null, 'created' => null, 'updated' => null ), 'Course' => array( 'id' => '1', 'title' => 'Course name', 'slug' => 'name-of-the-course', 'created' => '2012-10-11 00:00:00', 'updated' => '2012-10-11 00:00:00' ) ) 

I want it to return one of Page or Course , depending on which one is listed in Content.class

UPDATE: Combining Page and Course models will look like the obvious solution to this problem, but the above diagrams are just shown for this question. Actual schemes are actually very different in their fields, and each of them has a different number of associations with other models.

UPDATE 2

Here is the query that appears when $this->Content->find('first'); run $this->Content->find('first'); :

 SELECT `Content`.`id`, `Content`.`class`, `Content`.`foreign_id`, `Content`.`title`, `Content`.`slug`, `Content`.`content`, `Content`.`created`, `Content`.`modified`, `Page`.`id`, `Page`.`title`, `Page`.`slug`, `Page`.`created`, `Page`.`updated`, `Course`.`id`, `Course`.`title`, `Course`.`slug`, `Course`.`created`, `Course`.`updated` FROM `cakedb`.`contents` AS `Content` LEFT JOIN `cakedb`.`pages` AS `Page` ON (`Content`.`foreign_id` = `Page`.`id` AND `Content`.`class` = 'Page') LEFT JOIN `cakedb`.`courses` AS `Course` ON (`Content`.`foreign_id` = `Course`.`id` AND `Content`.`class` = 'Course') WHERE 1 = 1 LIMIT 1 
+7
source share
1 answer

Your request is in order. Just filter the empty values โ€‹โ€‹after the search:

 public function afterFind($results, $primary = false) { return Set::filter($results, true); } 

Take a look at this question.

You can also query conditions to find if Page.id is not null or Course.id not null.

+7
source

All Articles