How do I get associated models to run __construct? - cakephp

Little history; I hate the fact that I can't use enums in CakePHP, but I get it. However, another pet peev I have is that my Booleans return 0 or 1 and there is no way to universally turn them to yes' and no's.
So I though I would create a little function in the afterFind method of the AppModel to do this for me. The first step I wanted to take was to identify which columns where boolean (since some columns will return zeros and ones that do not need to be converted). I devised this little peace of code:
function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
foreach($this->_schema as $col => $colDetails){
if($colDetails['type'] == 'boolean')
$this->_booleans[] = $col;
}
}
However a quick debug($this) in the model show that only the current model's boolean columns are captured. When I hit those columns directly the $this->_booleans show up but again, not those of associated models.
I've looked though the manual and the API..I see no mention as to how to approach a solution.
What am I doing wrong?

Enums are not supported by CakePHP in order to make an application database type independent. Enums are not supported by many database engines. The simplest solution for your task is:
echo $model['boolField'] ? 'Yes' : 'No';

The problem is that $this->_booleans in the AppModel only contains the schema details of the current model. In fact, the code is probably working. You should check $this->_booleans and $this->Related->_booleans, and I bet you'll find what you're looking for.

Related

specify return fields for all find calls on model in cakephp

I can't believe I don't remember how to do this, but how do I specify in my model the default fields that are returned with the find() methods? I can't find on google how to do this, or at least I don't know the wording to search for.
What you will most likely need to do, is to check if the fields key exists in a beforeFind() method in your model.
If the fields key is not set, you can set it to $this->fields in your native models, and create the beforeFind() in your AppModel, then you can instruct that method to use the $this->fields array from your models.
UPDATE
// AppModel.php
parent function beforeFind($queryData = array()) {
if (empty($queryData['fields']) && !empty($this->fields)) {
$queryData['fields'] = $this->fields;
}
return $queryData;
}
// And in your Model:
public $fields = array(
'Alert.id'
);
This will check for existence of a fields array, and will then check for existence of a $this->fields property. If it does exist, it will apply it to the query data and return that modified query data to the beforeFind() - this will change your find.
Adjust it to fit your needs, and good luck!

CakePHP's Translate Behavior not working with Containable

I've implemented CakePHP's Translate Behavior, and all went fairly smooth, but I've now noticed that my translated data from the i18n table doesn't exist when I contain() a model that is supposed to be translated.
Does the Translate Behavior not work for contained models? If so, doesn't that near-completely remove any usefulness of this behavior? (Or maybe it's just me - but I use Containable for just about everything).
Is there a different "CakePHP-way" to do translations fairly easily if I plan to use Containable a lot?
I've encountered similar problem, read dozens of pages from Google, but couldn't find a simple solution to my problem. After some debugging I've created this workaround snippet. Please take in consideration that this is jus a hack. It was mostly written for Croogo, so related models will appear translated on site. But i've browsed Translate behavior and it should work for it as well. Basically paste it in your AppModel Class. It's for Cake 2.x
// DIRTY LITTLE HACKS, FORCING TRANSLATE BEHAVIOR AFTERFIND CALLBACK
/**
* Hacking the afterFind so it will call the afterFind() from
* behavior
* Pase this in your AppModel Class
*
* #param array $results
* #param bool $primary
* #return array
*/
public function afterFind(array $results, $primary = false) {
parent::afterFind($results, $primary);
# calling only if not primary model, as they get translated pretty well
if (!$primary) {
# iterating behaviors to look for one that has something to do
# with translations ( Translate for cake general behavior, CroogoTranslate for Croogo based apps )
foreach ($this->Behaviors->enabled() as $behavior) {
if (preg_match('/(.*)[T|t]ranslate(.*)/', $behavior)) {
# setting locale, not sure if it gets set on secondary models
$this->locale = Configure::read('Config.language');
# hacking the result set to match behaviours requirments
# so basically creating the result set to look like called from originated model
# $k => array('ModelAlias' => array $results)
$results_tmp = array(
0 => array(
$this->alias => $results,
)
);
# if we find such behavior we force it's afterFind with prepared data
$results = $this->Behaviors->{$behavior}->afterFind($this, $results_tmp, true); # forcing true on primary - CroogoTranslate requires that
# restoring orginal structure like nothing ever happened
$results = $results[0][$this->alias];
# not sure if should break or not ?
# on one hand what's the point of having multiple translate behaviors in one app ?
# on the other i've seen more weird stuff that multiple translate behaviors
break;
}
}
}
return $results;
}
Apparently this is a common issue. The CakePHP Cookbook has some hints about how to handle it:
http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html

CakePHP: afterFind is weird with associations

So afterFind works fine and dandy when I'm within the corresponding model/controller. However, when calling an associated model, the data sent to the afterFind callback is formatted differently. This causes afterFind to crap out because it can't find the same array indexes it did when just working within the original model/controller.
Anyone know why, or what a fix might be?
$primary may not be very helpful; I've found that it is always false when using ContainableBehaviour beyond the first depth:
$this->Model->find('first', array(
'contain' => array(
'SecondaryModel' => array(
'TertiaryModel',
),
),
));
If you're setting a value based on a related model, you can check for its presence to deal with either structure like this:
function afterFind($results, $primary) {
if (isset($results['TertiaryModel'])) {
$results['secondary_model_field'] = 'value';
}
else {
foreach ($results as &$result) {
if (is_array($result) && isset($result['TertiaryModel'])) {
$result[$this->alias]['secondary_model_field'] = 'value';
}
} unset($result);
}
return $results;
}
Alternately you may be able to just check for the location of a field on the model itself. If the field doesn't exist at the top level, you will need to iterate over the set of results.
This is what the second parameter to afterFind callback is for.
$primary tells if you if the find was called from this model directly (true), or if it was called by an associated model (false).
A note from the book:
The $primary parameter indicates whether or not the current model was
the model that the query originated on or whether or not this model
was queried as an association. If a model is queried as an
association the format of $results can differ;
Code expecting $primary to be true will probably get a "Cannot use
string offset as an array" fatal error from PHP if a recursive find
is used.
So you may need different processing logic depending on the value of $primary
It appears that Cake 2.6 includes a fix for this, ensuring that all $results arrays are consistently formatted. I've done a little testing with the RC release and it does seem to work, with arrays all being passed in the format {n}.ModelName.data.
The fix is enabled by default, but you can also revert to the legacy behaviour if need be. Just add the following to your model (or AppModel) definition:
public $useConsistentAfterFind = false;

CakePHP - select database config based on route or url?

I'm working on a small CakePHP application that is subject to the following constraint (awkward but out of my control): I need it to work on either of two identical databases, with the choice being based on URL. For example:
http://www.example.com/myapp/foo/action/param
http://www.example.com/myapp/bar/action/param
The first obvious solution is to have two identical CakePHP applications at myapp/foo and myapp/bar with different database configurations. This has a kludgy feel to it, though, so I'm trying to find an elegant way of creating a single application.
The approach I'm considering is this: Define routes such that myapp/foo and myapp/bar will be associated with the same controller. Then give my DATABASE_CONFIG class a constructor:
function __construct() {
$pathParts = explode('/', $_SERVER['REQUEST_URI']);
if (array_search('foo', $pathParts)) {
$this->default = $this->fooConfig;
} else if (array_search('bar', $pathParts)) {
$this->default = $this->barConfig;
}
}
(Where of course I've defined fooConfig and barConfig for the two databases.) I do have control over the URL, so I can be confident that there won't be extraneous occurrences of foo or bar in the URL.
My question is this: Is there a simpler, more elegant way of handling this odd situation? Maybe something in AppModel and/or AppController? Although I'm getting rid of duplicated code, I can't shake the feeling that I'm replacing one code smell with another.
There are a few ways to do this, here is one.
Write a sweet custom route in which you always match:
Router::connect('/:ds/*', array(), array('routeClass' => 'SweetDbRoute'));
Then have SweetDbRoutes set a class variable you can use everywhere, including in your model constructors. Then it should fail so you don't actually adjust the request.
App::import('SweetDbClass', array('file' => '/path/to/your/sweet_db_class.php'));
class SweetDbRoute extends CakeRoute {
// put your failing route code here, but use your SweetDbClass to track datasource ...
// see http://book.cakephp.org/view/1634/Custom-Route-classes
}
Then in your AppModel:
App::import('SweetDbClass', array('file' => '/path/to/your/sweet_db_class.php'));
class AppModel extends Model {
public function __construct($id = false, $table = null, $ds = null) {
$ds = SweetDbClass::$ds;
parent::__construct($id, $table, $ds);
}
}
So for example, after you perform an insert in one database, the two won't be "identical", right? Are these 2 DB somehow synced with each other? I don't know what do you need to do on those DB, but it's probably easier just to do 2 separate apps.
Yes, you can specify the DB configuration in the model: http://book.cakephp.org/view/922/Database-Configuration but you can't change it on-the-fly though (the models are not expected to change association to another table, I suppose). What you do is probably the only way.
I do have control over the URL, so I can be confident that there won't be extraneous occurrences of foo or bar in the URL
Yes, there can be "extraneous occurrences of foo or bar in the URL" :)) But it won't break your app.

Computing table name from model name

In my CakePHP application, I have a model like this:
class Duck extends AppModel {
var $name = 'Duck';
function get_table_name() {
$tbl_name = //compute default table name for this model
}
}
I would like to write the function get_table_name() that outputs the default table name for the model. For the example above, it should output ducks.
EDIT:
Several people have pointed out the use of $this->table.
I did small testing and found out the following:
In the question as I have put above, $this->table indeed contains the table name.
However, actually, my code looked more like this:
class Duck extends Bird {
var $name = 'Duck';
function get_table_name(){
$tbl_name = //comput default table name for this model
}
}
class Bird extends AppModel {
}
In this case $this->table is empty string.
I went with this approach because I wanted to share some code between two of my models. Looks like this is not a good way to share code between models which need some common functionality.
You're looking for the Inflector class.
Inflector::tableize($this->name)
(tableize calls two Inflector methods to generate the table name: underscore() and pluralize())
Edit:
According to the source code, $this->table should contain the name of the table that CakePHP will use for the model, but in my experience this isn't always set. I'm not sure why.
To get the name of the table that the model is currently using, you can use: $this->table. If you don't manually change the model's table conventions, this may be the most useful in the case of CakePHP ever changing its conventions to use table names using something other than Inflector.
CakePHP's Inflector
function get_table_name() {
$tbl_name = Inflector::pluralize($this->name);
}
OR the tableize method
function get_table_name() {
$tbl_name = Inflector::tableize($this->name);
}
Edit
This also addresses the apparent "ghost" issue with $this->table in the Model.
Digging around in the __construct for Model I discovered two things:
Cake uses Inflector::tableize() to get the table name. This alone is enough to warrant using tableize over pluralize. You'll get consistent results.
$this->table is not set by the Model::__construct() unless $this->useTable === false AND $this->table === false.
It appears that if you know you haven't set $this->useTable to false you should be able to use this over $this->table. Admittedly though I only briefly scanned the source and I haven't really dug deep enough to say why $this->table isn't working sometimes.
To get the full table name for a model you have to take the table prefix into account.
$table = empty($this->table) ? Inflector::tableize($this->name) : $this->table;
$fullTableName = $this->tablePrefix . $table;
I used to use inflector to get the table name from model's name
$tableName = Inflector::pluralize(Inflector::underscore($model));
but this is not really universal, using useTable looks better, by default it will contain table's name by convention, and if you have a table that does not match the conventions, then you should manually specify it by useTable. So, in both cases the result will be correct
$this->User->useTable

Resources