In CakePHP 3.X using addCase how should I define a value as a fallback? [duplicate] - cakephp

I'm trying to get a query working using a case statement, and can't figure out how to get the case to return a column value instead of a constant. I have the query working perfectly, except that the column names I'm providing for the results are being quoted or otherwise mishandled by Cake or maybe PDO somewhere down in a layer that I can't dig my way through. I got as far down as bindValue, but none of the documentation I encountered along the way tells me how to do this.
I have found this example comment:
$statement->bindValue(1, 'a title');
$statement->bindValue(2, 5, PDO::INT);
$statement->bindValue('active', true, 'boolean');
$statement->bindValue(5, new \DateTime(), 'date');
but in all these cases, the value provided is a constant. I need to pass in a string that is the name of the column that I want returned.
I tried both 'string' (resulted in quoted column name) and 'integer' (resulted in 0). I tried PDO::FETCH_COLUMN (seemed highly unlikely, but looked like the next best bet from http://php.net/manual/en/pdo.constants.php, and easy to try it...). I tried 'literal', inspired by the way you can put literal strings into expressions (resulted in Error: unknown type "literal"). That error message led me to src/Database/Type.php, but nothing in there helped me either.
So, I'm pretty much stumped. Here's a simple version of the code I have (leaving out a couple of conditions and unrelated columns):
$query = $this->Games->find();
$team_id = $query->newExpr()->addCase(
[$query->newExpr()->eq('Games.status', 'home_default')],
['home_team_id', 'away_team_id'],
['string', 'string']
);
$defaulting = $query
->select([
'id' => $team_id,
'count' => 'COUNT(Games.id)',
])
->where([
'Games.status IN' => ['home_default', 'away_default'],
])
->group('id')
->toArray();
This generates this SQL:
SELECT
(CASE WHEN Games.status = 'home_default'
THEN 'home_team_id' ELSE 'away_team_id' END) AS `id`,
COUNT(Games.id) AS `count`
FROM games Games
WHERE Games.status in ('home_default','away_default')
GROUP BY id
Note that THEN 'home_team_id' ELSE 'away_team_id' END should be simply THEN home_team_id ELSE away_team_id END. This will then allow me to read the list of ids of teams that have defaulted games along with the number of games they defaulted.

By default the values passed to the second argument of QueryExpression::addCase() are being treated as to be converted to literal values, not as identifiers. If you need the latter, then you should use an expression, an IdentifierExpression.
use Cake\Database\Expression\IdentifierExpression;
// ...
$team_id = $query->newExpr()->addCase(
[
$query->newExpr()->eq('Games.status', 'home_default')
],
[
new IdentifierExpression('Games.home_team_id'),
new IdentifierExpression('Games.away_team_id')
]
);
Also ditch the third argument in this case, you don't want the values to be string literals (for expressions the types would be ignored anyways).

Related

Cakephp3: Result of CASE statement being forced to string, need integer

I am generating a case statement to return either 0 or 1:
$desc_case = $q->newExpr()
->addCase(
[$q->newExpr()->add(["description IS" => NULL])],
[0,1],
["integer","integer"]
);
$q = $q->select(["has_desc" => $desc_case]);
Which results in the following correct SQL:
SELECT [fields removed for clarity], (CASE WHEN (description) IS NULL THEN :c0 ELSE :c1 END) AS `has_desc` FROM skills Skills
I've turned hydration off, and retrieved the result with
->hydrate(false)->toArray();
The result of the CASE statement is returned as a string- either "0" or "1" - which is messing up logic downstream.
I've traced the execution code as best I can, and it looks like CakePHP is using the type names passed to correctly bind the values, but nowhere does the type make its way into the TypeMap used for mapping output.
An easy workaround is to adjust the values after the fact (which I'm doing), but I'd like to make this work as expected on principle... :)
The type information passed to addCase() is only ment to be used for input casting, ie the values passed will be bound as the given types. The return values, ie the values being selected via the compiled CASE statement, are in no way being affected.
If you want to affect the type used for casting selected values of columns that do not exist in the schema (note that changing the schema will also affect other parts of the ORM and the query builder), then you have to change the type map accordingly, for example:
$query
->getSelectTypeMap()
->addDefaults([
'has_desc' => 'integer'
]);
See also
Cakephp-3.x: How to change the data type of a selected alias?

CakePHP 3: order of translated data with missing translation strings

I use TranslationBehavior to handle translated data in my app. When retrieving data, CakePHP returns the translated string or if this string is not available, the value of the default locale. To request the data, the following query is used:
$items = $this->Model->find('all')
->all()
->extract('name');
To order the output, the query is:
$items = $this->Model->find('all')
->order([$this->Model->translationField('name') => 'ASC'])
->all()
->extract('name');
This works for all default locale items and all translated items. But when the translation for a record is missing, it will break the order. In this case the correct fallback value is returned, but the order is no longer correct. The output looks like this:
['A... (Translated)', 'B... (Translated)', 'A... (Default)', 'C... (Default)']
What I expect is the following order:
['A... (Default)', 'A... (Translated)', 'B... (Translated)', 'C... (Default)']
To achieve this, I changed the query to:
$items = $this->Model->find('all')
->order(['IF('.$this->Model->translationField('name').' != "", '.$this->Model->translationField('name').', Model.name)' => 'ASC'])
->all()
->extract('name');
Which gives the expected order:
['A... (Default)', 'A... (Translated)', 'B... (Translated)', 'C... (Default)']
The question is: is this the correct way to handle the order of mixed locales? Or did I miss something and CakePHP already provides a simpler solution?
You may ask, why should we mix the locales? In my case, it's not necessary to translate all the strings, because some of the items are identical in both languages.
Looking at how CakePHP queries the translated fields, and merges them later on at PHP level, this is the expected behavior, and you'd indeed have to use a conditional expression in the ORDER clause.
I'd suggest to use a CASE expression, as the IF() function is MySQL specific, something like:
$query = $this->Model->find();
$query
->orderAsc(
$query->newExpr()->addCase(
[
$query->newExpr()->isNotNull($this->Model->translationField('name'))
],
[
$query->identifier($this->Model->translationField('name')),
$query->identifier('Model.name')
]
)
)
// ...
Which would generate an expression similar to:
ORDER BY
CASE WHEN Model_name_translation.content IS NOT NULL
THEN Model_name_translation.content
ELSE Model.name END
ASC
And as mentioned in the comments, If you go the "do not translate identical strings" route, then you should avoid storing empty strings for such "missing" translations, instead do not store a record for them at all.
In order to avoid all this you could of course add translations for everything, even if they are equal in the different languages.

different output for model name with capital letter in cakephp3

I dont understand how I get a different output for lesson_date field if in the select I use Lessons.lesson_date' or lessons.lesson_date. I thought I am supposed to use Lessons and not lessons and that the for a single name it doesnt really matter.
with Lessons.lesson_date I get on the debug :
'lesson_date' => object(Cake\I18n\FrozenDate) {
'time' => '2015-07-09T00:00:00+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
with lessons.lesson_date I get a better output:
'lessons' => [
'id' => '5399',
'lesson_date' => '2015-07-09'
//this is the code below I am talking about. The Lessons.lesson_date gives a different output than if I change this to lessons.lesson_date
$query3 = $this->Lessons->find()
->contain(['TutoringTypes'])
->select(['lessons.id','Lessons.lesson_date','Lessons.tutoring_type_id',
'TutoringTypes.value'])
->where(['Lessons.lesson_date >' => $a3,'Lessons.lesson_date <' => $a4, .....
That is the expected/correct behavior when following the conventions, the date gets casted according to its type.
The latter output might be better suited for what you are doing with the data, but generally the former is "better" since a date object gives you more freedom of manipulating dates, handling localization, output formatting, etc...
Why?
As to the why, the output is different because the ORM will not cast the value in case of a non-conventional column alias, as it's not present in the type map that holds the information about which column is of which type.
Using lessons.lesson_date will create an alias of
lessons__lesson_date
which is not following the conventions, where as using Lessons.lesson_date will create an alias of
Lessons__lesson_date
which does follow the conventions, and will match the field in the type map, causing the ORM to cast the data.
Changing the behavior
If you need YYYY-MM-DD output, then you could simply output it formatted it in your view like
echo $lesson->lesson_date->i18nFormat('yyyy-MM-dd')
or change the default output format (which is used when the date gets casted to a string)
\Cake\I18n\FrozenDate::setToStringFormat('yyyy-MM-dd');
or maybe even change the type in the type map in order to keep the date string as is
$query = $this->Lessons
->find()
// ...
$types = ['Lessons__lesson_date' => 'string'] + $query->selectTypeMap()->defaults();
$query->selectTypeMap()->defaults($types);
See also
Cookbook > Date & Time > Formatting
Cookbook > Date & Time > Setting the Default Locale and Format String
\Cake\ORM\Query::_addDefaultSelectTypes()
\Cake\Database\Query::selectTypeMap()
\Cake\Database\Query::typeMap()

Mongoid Syntax Questions

1) Finding by instance object
Assuming I have the instance object called #topic. I want to retrieve the answers for this given topic. I was thinking I should be able to pass in :topics=>#topic, but i had to do the very ugly query below.
#answers = Answers.where(:topic_ids => {"$in" => [#topic.id]})
2) Getting the string representation of the id. I have a custom function (shown below). But shouldn't this be a very common requirement?
def sid
return id.to_s
end
If your associations are set up correctly, you should be able to do:
#topic.answers
It sounds like the above is what you are looking for. Make sure you have set up your associations correctly. Mongoid is very forgiving when defining associations, so it can seem that they are set up right when there is in fact a problem like mismatched names in references_many and referenced_in.
If there's a good reason why the above doesn't work and you have to use a query, you can use this simple query:
#answers = Answer.where(:topic_ids => #topic.id)
This will match any Answer record whose topic_ids include the supplied ID. The syntax is the same for array fields as for single-value fields like Answer.where(:title => 'Foo'). MongoDB will interpret the query differently depending on whether the field is an array (check if supplied value is in the array) or a single value (check if the supplied value is a match).
Here's a little more info on how MongoDB handles array queries:
http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ValueinanArray

CakePHP update field value based on other value in same row?

I have been trying to figure out how to do this and it seems that its not something that many people are trying to do in cakephp or I am just completely misunderstanding the documentation.. I am using the following query below to get a field value so I get a value where the "findbycreated" is 0... this part works fine
$unregisteredemail = $this->Testmodel->findBycreated('0');
$emailaddress = $unregisteredemail['Testmodel']['emailaddress'] ;
$emailpassword = $unregisteredemail['Testmodel']['password'] ;
But now, after I do some things with this data that I retrieved, I want to mark a field, in the same row, in the same model / table as a value of '1' to indicate that an action has taken place (email address has been successfully created, for example)... I just can't figure out how to do this in cakephp despite my efforts of going through the documentation and searching, this should be rather simple, I am tempted, at this point, to just use a regular mysql query as its a simple query.. basically the query is (please excuse my syntax as I haven't used direct mysql queries in a while) "update (database / table) set 'created'='1' where 'emailaddress'=$emailaddress"
Or I could use the row ID, if needed, as cakephp seems to prefer this, but still can't get how to do this.. this is my attempt below that is not working:
// update database to show that email address has been created
$this->Testmodel->read('emailaddress', $this->Testmodel->findBycreated('0'))
$this->Testmodel->id = 1;
$this->Testmodel->set(array(
'created' => '1'
));
$this->Testmodel->save();
There are, as you can see from the previous answers, several ways to achieve the same end. I'd just like to explain a little about why your way didn't work.
In the model, CakePHP has abstracted the database row(s) into an array according its implementation of ORM . This provides us with a handy way of manipulating the data and chucking it around the MVC architecture.
When you say:
$this->Testmodel->set(array(
'created' => '1'
));
You are dealing directly with the model, but the data is actually stored, as an array, in a class variable called $data. To access and manipulate this data, you should instead say:
$this->data['Testmodel']['created'] => '1';
The reason for specifying the model name as the first index is that where associated tables have been retrieved, these can be accessed in the same way, so you might have , for instance:
Array([Testmodel] => Array ([id] => 1,
[created] => [1],
...
)
[Relatedmodel] => Array ([id] => 1,
[data] => asd,
...
)
)
...and so on. Very handy.
Now, when you use $this->MyModelName->save() with no parameters, it uses $this->data by default and uses the part of the array of data appropriate to the model you are calling the save method on. You can also pass an array of data, formatted in the same way if, for some reason, you don't (or can't) use $this->data.
Your use of the method read() is incorrect. The first parameter should be null, a string or an array of strings (representing fieldname(s)). The second parameter should be the id of the record you wish to read. Instead, for param 2, you are passing the result of a find, which will be an array. The result, which you are not capturing, will be empty.
I would write your code like:
$this->data = $this->Testmodel->read('emailaddress',1);
$this->data['Testmodel']['created'] = 1;
$this->Testmodel->save();
or more succinctly:
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created', 1);
In this situation I would let Cake deal with the id's and just focus on changing the row data and resaving it to the database
$row = $this->Model->findBycreated(0);
$row['Model']['emailaddress'] = 1;
$this->Model->save($row);
This way, you don't have to worry about the id's, as the id will be in your dataset anyway, so just change what you want and then tell Cake to save it.
Ninja edit, Be sure that you are returning a full row with an id from your findBycreated() method.
There're many ways to do your work.I suggest you to read the cookbook about saving data in cakephp.And besides david's solution another simple way would be
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created' =>'1');
Ok, I think I finally found the solution, I was able to get this to work:
$this->Test->updateAll(
array(
'Test.field' => 'Test.field+100'
),
array(
'Test.id' => 1
)
);
I think you have to use updateAll as anything else will just create a new row.. basically CakePHP, for whatever reason, neglected to include a function for updating just one field so you have to put it into an array with the updateAll to make it work...
the +100 is where the updated info goes, so in this case "100" would be what the field is updated to.
In cakephp 3.x the syntax seems to be different. This is what worked for me in 3.x:
$this->Tests->updateAll(
[
'Tests.field = Tests.field+100'
],
[
'Tests.id' => 1
]
];
The difference is that the entire expression needs to be in the value of the first array.

Resources