Polymorphic associations in CakePHP2 - cakephp

I have 3 models, Page , Course and Content
Page and Course contain meta data and Content contains HTML content.
Page and Course both hasMany Content
Content belongsTo Page and Course
To avoid having page_id and course_id fields in Content (because I want this to scale to more than just 2 models) I am looking at using Polymorphic Associations. I started by using the Polymorphic Behavior in the Bakery but it is generating waaay too many SQL queries for my liking and it's also throwing an "Illegal Offset" error which I don't know how to fix (it was written in 2008 and nobody seems to have referred to it recently so perhaps the error is due to it not having been designed for Cake 2?)
Anyway, I've found that I can almost do everything I need by hardcoding 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')
)
);
}
?>
The good thing is that $this->Content->find('first') only generates a single SQL query instead of 3 (as was the case with the Polymorphic Behavior) but the problem is that the dataset returned includes both of the belongsTo models, whereas it should only really return the one that exists. Here's how the returned data looks:
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 only want it to return one of either Page or Course depending on which one is specified in Content.class
UPDATE: Combining the Page and Course models would seem like the obvious solution to this problem but the schemas I have shown above are just shown for the purpose of this question. The actual schemas are actually very different in terms of their fields and the each have a different number of associations with other models too.
UPDATE 2
Here is the query that results from running $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

Your query is okay. Just filter empty values after find:
public function afterFind($results, $primary = false) {
return Set::filter($results, true);
}
Take a look at this question.
You can also try to provide conditions to you find query that Page.id in not null or Course.id is not null.

Related

CakePHP using foreign keys with different names as the associated primary keys

I set up more than one models, which i want to associate with a master-model like this:
class CommonType extends AppModel {
public $useDbConfig = 'common';
public $hasOne = array(
'CommonTypeDescription' => array(
'className' => 'CommonTypeDescription',
'foreignKey' => 'type_description_id',
'dependent' => true
),
'CommonTypeExperience' => array(
'className' => 'CommonTypeExperience',
'foreignKey' => 'type_expirience_id',
'dependent' => true
),
'CommonTypeProperty' => array(
'className' => 'CommonTypeProperty',
'foreignKey' => 'type_property_id',
'dependent' => true
),
);
public $belongsTo = array(
'CommonTypeCategory' => array(
'className' => 'CommonTypeCategory',
'foreignKey' => 'common_type_id',
));
}
In Heidi SQL (for example) i set the foreignkeys for the tables type_experiences, type_properties and type_descriptions as above, so that foreignkeys in my code and in my tables have the same name.
But now, i don't want to name the primary key as the foreign key, the primary key of the (e.g.) type_descriptions table should named "id". This is the model of type_description:
class CommonTypeDescription extends AppModel {
public $name = 'CommonTypeDescription';
public $useDbConfig = 'common';
public $useTable = 'type_descriptions';
public $primaryKey = 'id';
}
and here is the database creation code:
CREATE TABLE `type_descriptions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`common_type_id` INT(11) NULL DEFAULT NULL,
`short_description` VARCHAR(50) NULL DEFAULT NULL,
`long_description` VARCHAR(50) NULL DEFAULT NULL,
`notes` VARCHAR(50) NULL DEFAULT NULL,
`modified` DATETIME NULL DEFAULT NULL,
`created` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `type_description_id` FOREIGN KEY (`common_type_id`) REFERENCES `common_types` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION) ...
But when i try to get the data from my model CommonType ($this->find('all')) i get an error like "Unknown column 'CommonTypeDescription.type_description_id' in 'on clause'"
First, i thought that i have to declare the primary key of the description model as "id", but it seams, that the foreign key definition overwrites the naming convention from cake of the primary key from the associated model.
Many thanks for your help & Greetings,
Guido
ps. i've changed the Model-Names of this post (if there is a type-mistake). So when i rename the primary keys of the associated tables, it all works fine, but i want them to name only to "id".
How about update your CommonType Model to this:
'CommonTypeDescription' => array(
'className' => 'CommonTypeDescription',
'foreignKey' => 'common_type_id', //or try type_description_id if there's an error
'dependent' => true
)
Then on your CommonTypeDescription model add this relationship, I think you forgot adding belongs to relationship to CommonType Model:
public $belongsTo = array(
'CommonType' => array(
'className' => 'CommonType ',
'foreignKey' => 'common_type_id',
));

Issue with setDataSource in CakePHP 2.4.8

I wrote a behavior, Cascadable, that merges records across 2 databases. It works sort of like CSS, where the more local style overrides the global. The behavior uses $this->setDataSource() to switch between the databases. This was working up until the 2.4.8 release when this commit was made to the Model.php file.
Here's sort of an extraction from the behavior:
$Model->setDataSource( 'alt' );
$altData = $Model->find($findType, array(
'contain' => false,
'conditions' => array(
$Model->alias.'.id' => $ids
),
'callbacks' => 'before'
));
If the callbacks option is set to false, I get Missing Database Table errors about tables that are only present in the default (parent) datasource.
I'm not sure what I'm doing wrong here, but after 2.4.8 upgrade it always uses the wrong (parent) database instead of the child. I can revert Model.php back to the 2.4.7 version to fix the issue. I realize this is difficult to replicate because it only breaks in my very specific database structure.
MySQL Table Schema example:
Parent Database Table
CREATE TABLE `products` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`price` float(8,2) DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `active` (`active`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 PACK_KEYS=0;
INSERT INTO `parent_db`.`products` (`id`, `name`, `price`, `active`) VALUES (NULL, 'Big Box of Goodies', '99.99', '0');
Child Database Table
CREATE TABLE `products` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`price` float(8,2) DEFAULT NULL,
`active` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `active` (`active`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 PACK_KEYS=0;
INSERT INTO `child_db`.`products` (`id`, `name`, `price`, `active`) VALUES (NULL, 'Small Box of Surprises', NULL, '1');
Query Results
From Parent DB
array(
(int) 0 => array(
'Product' => array(
'id' => '1149',
'name' => 'Big Box of Goodies',
'price' => '99.99',
'active' => false
)
)
)
From Child DB with working Cascadable behavior (child record is merged into parent record, ignoring null values)
array(
(int) 0 => array(
'Product' => array(
'id' => '1149',
'name' => 'Small Box of Surprises',
'price' => '99.99',
'active' => true
)
)
)
Workaround:
Setting the schemaName manually after using setDataSource fixes the issue for me.
$Model->setDataSource( 'alt' );
App::uses('ConnectionManager', 'Model');
$dataSources = ConnectionManager::enumConnectionObjects();
$Model->schemaName = $dataSources['alt']['database'];

MySQL Error with straightforward polymorphic association in CakePHP

I have 4 models, Application , Accommodation, Invoice and InvoiceItem
Invoice, Application and Accommodation all hasMany InvoiceItem
InvoiceItem belongsTo Application, Accommodation and Invoice
The purpose of all this is so that the line items of an Invoice (which come from the InvoiceItem model) can be associated with either an Accommodation or an Application via the InvoiceItem.foreign_id depending on whether InvoiceItem.class is set as Accommodation or Application
From what I understand, this is known as a polymorphic association.
Unfortunately, when I do a $this->Invoice->find('all'); with recursive set to 3 (or set to -1 with the appropriate fields specified in containable) I get this MySQL error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'InvoiceItem.class' in 'where clause'
SQL Query: SELECT `Application`.`id`, `Application`.`name`,
`Application`.`created`, `Application`.`updated` FROM `applications`
AS `Application` WHERE `Application`.`id` = 3786 AND
`InvoiceItem`.`class` = 'Application'
I don't understand why Cake would try to add InvoiceItem.class as a condition without creating a join for the InvoiceItem model.
Here are my models (note: I've cut down the number of fields in each for readability -- the Application and Accommodation models share very few similar fields in reality):
Accommodation Model
CREATE TABLE `accommodations` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
)
<?php
class Accommodation extends AppModel {
var $name = 'Accommodation';
var $hasMany = array(
'InvoiceItem' => array(
'className' => 'InvoiceItem',
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Accommodation'),
)
);
}
?>
Application Model
CREATE TABLE `applications` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
)
<?php
class Application extends AppModel {
var $name = 'Application';
var $hasMany = array(
'InvoiceItem' => array(
'className' => 'InvoiceItem',
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Application'),
)
);
}
?>
InvoiceItem model
CREATE TABLE IF NOT EXISTS `invoice_items` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`class` varchar(30) COLLATE utf8_unicode_ci NOT NULL,
`foreign_id` int(10) unsigned NOT NULL,
`name` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)
<?php
class InvoiceItem extends AppModel {
var $name = 'InvoiceItem';
var $belongsTo = array(
'Accommodation' => array(
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Accommodation')
),
'Application' => array(
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Application')
),
'Invoice' => array(
'className' => 'Invoice',
'foreignKey' => 'invoice_id',
),
);
}
?>
Invoice model
CREATE TABLE IF NOT EXISTS `invoices` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)
<?php
class Invoice extends AppModel {
var $name = 'Invoice';
var $hasMany = array(
'InvoiceItem' => array(
'className' => 'InvoiceItem'
'foreignKey' => 'invoice_id'
)
);
}
?>
I'm using CakePHP 2.4.0
class InvoiceItem extends AppModel,and bind no belongsTo.
class InvoiceItem extends AppModel {
var $name = 'InvoiceItem';
var $belongsTo = null
}
you can assign a suitable belongsTo to InvoiceItem at Runtime depend on content
$this->InvoiceItem->bindModel(array( 'belongsTo' => array(...) ) )

Column not found, retrieving data associated model

I've got the following find() function
$this->User->find('all',array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'fields' => array('User.id','UserRole.id')
));
And the following associations are defined
// UserRole.php
class UserRole extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
// User.php
class User extends AppModel {
public $hasMany = array(
'UserRole' => array(
'className' => 'UserRole',
'foreignKey' => 'user_id',
'dependent' => false,
)
);
}
And In App model recursive is set to -1
public $recursive = -1;
The database tables are the following:
CREATE TABLE IF NOT EXISTS `user_roles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`event_id` int(10) unsigned NOT NULL,
`role_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`first_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`last_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
But I get the vollowing error
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'UserRole.id' in 'field list'
SQL Query: SELECT `User`.`id`, `UserRole`.`id` FROM `atlanta`.`users` AS `User` WHERE `User`.`id` = 5
I get that cake can't find the UserRole.id, but what did I do wrong?
Either you use some behavior like Containable and your find() will look like this
$this->User->find('all',array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'contain' => array(
'UserRole' => array(
'fields'=>array('id')
)
),
'fields' => array('id')
));
OR you set recursive to 1
$this->User->find('all',array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'fields' => array('User.id'), // UPDATE: unfortunately you cant specify associated models fields without behavior
'recursive' => 1
));
The thing is that when recursive is set to -1 or 0 it does not fetch associated models unless some behavior takes care of that for e.g. Containable or Linkable.

cakePHP meioupload, upload image to a different folder for each model

I use meioupload to upload images in cakePHP, i use a table called 'attachment' to save the uploaded image's information, this is the structure of my attachment table:
CREATE TABLE IF NOT EXISTS `attachments` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`class` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`foreign_id` bigint(20) unsigned NOT NULL,
`filename` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`dir` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`mimetype` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
`filesize` bigint(20) DEFAULT NULL,
`height` bigint(20) DEFAULT NULL,
`width` bigint(20) DEFAULT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
And i currently have another 2 tables connected with this through the class field (table name) and foreign_id. Now my question is, how can i save the uploaded image to a different folder for each model?
For example: i would like to save my post's image into 'post' folder and to save my profile image into 'profile' folder
UPDATE: in my attachment model
public $actsAs = array(
'MeioUpload' => array(
'filename' => array(
'dir' => 'post', #i set the default folder as 'post' at the moment
'create_directory' => true,
'allowed_mime' => array(
'image/jpeg',
'image/pjpeg',
'image/png'
),
'allowed_ext' => array(
'.jpg',
'.jpeg',
'.png'
),
'thumbsizes' => array(
'large' => array(
'width' => 500,
'height' => 500
),
'small' => array(
'width' => 100,
'height' => 100
)
)
)
)
);
UPDATE #2 : let say that i currently have 3 tables, "attachment" "post" and "profile", the one that actAs meioupload is "attachment", every time i upload an image through "post" or "profile", i will save the image information into "attachment", foreign_id and class fields in "attachment" is the one connecting "attachment" to "post" and "profile".
UPDATE #3 : i followed Dunhamzzz suggestion on using the behavior on the fly, and come up with this solution, and it works.
$this->Attachment->Behaviors->attach(
'MeioUpload', array(
'filename' => array(
'dir' => 'avatars'
)
));
Thanks
The answer is in your MeioUpload, specifically the 'dir' option, you can put {ModelName} or {fieldName} in to alter where the file saves. Here is the default in the behaviour itself:
dir' => 'uploads{DS}{ModelName}{DS}{fieldName}',
Update
In order to get MeioUpload to support different settings for the same model, you could try attaching the behaviour on the fly, which allows you to change the settings as you please.
eg in your posts action
$this->Attachment->Behaviours->attach('MeioUpload', array('dir' => '/uploads/posts/');
Be sure to read the part on behaviours on the docs, it will hopefully help you work out a solution on a per-action basis as opposed to per-model which comes with behaviours.
Here is an example for the $actAs array.
'MeioUpload' => array(
'filename' => array(
'dir' => 'files/banners',
'create_directory' => false,
'allowed_mime' => array(
'image/jpeg',
'image/pjpeg',
'image/gif',
'image/png'
),
'allowed_ext' => array(
'.jpg',
'.jpeg',
'.png',
'.gif'
),
)
),
as you can see, there is a key "dir" that you can modify

Resources