CakePHP 3 - access params of Query object - cakephp

In CakePHP 3.x I can do this:
$Substances = TableRegistry::get('Substances');
$query = $Substances->find()->where($where_conditions)->select(['id']);
debug($query);
This will show me the Query object.
If I want to get the SQL string I can use debug($query->sql());. This will give me the SQL string with placeholders for any parameters, e.g.
SELECT ... WHERE ... in (:c0,:c1,:c2))
When using debug($query) I can see the values for :c0, :c1, etc:
'params' => [
':c0' => [
'value' => (int) 47,
'type' => 'smallinteger',
'placeholder' => 'c0'
],
':c1' => [
'value' => (int) 93,
'type' => 'smallinteger',
'placeholder' => 'c1'
],
':c2' => [
'value' => (int) 845,
'type' => 'smallinteger',
'placeholder' => 'c2'
],
':c3' => [
'value' => (int) 354,
'type' => 'smallinteger',
'placeholder' => 'c3'
]
]
However, I cannot access them outside the debug statement. For example $query->params() or $query['params'] doesn't give me the parameters - it will error. I want to be able to pass this array to a custom function, so how can I access it?
It's strange because I can use debug($query->sql()) to get the SQL string as above, and params is just another thing in that object, but doesn't seem to be accessible.
I've read How to get params from query object in CakePHP 3 but think that's a different question as it was to do with not seeing the values in the debug statement due to the default depth that debug would provide.
The reason I want to do this is because I want to be able to do a CREATE TABLE AS query that will write the values of the SELECT statement into a new table (Important: see this link for an example of how that works in vanilla MySQL). I can't figure out how to do that with the ORM in Cake, so was planning on writing a custom function. But I need to be able to access both the SQL as well as the parameters bound so that the query can be executed in my own function.
If you know of a solution where I can use the ORM to do the CREATE TABLE AS query, I'm still interested to know about this. However I would like to know if params are accessible outside debug() as well.

Premise: I did not actually understand why you need the params
anyway. The information you need is stored by the query ValueBinder object
so you could simply do
$params = $query->getValueBinder()->bindings();
debug($params);
but for some reason this will get you an empty array. My guess is that the query need some kind of initialization first.
in fact if you run
debug($query);
$params = $query->getValueBinder()->bindings();
debug($params);
you'll see your params. I think someone more expert than me will come and give a full explanation
edit: I noticed that debugging $query calls $query->sql() which in turns calls conection->compileQuery();
so you can do
$query->sql(); // you need this to compile the query
// and generate the bindings
$params = $query->getValueBinder()->bindings();

CakePHP does not provide specific methods for creating such CREATE TABLE AS statements, so you'll have to build that on your own.
Compiling a query as the one shown in your question is simple enough using the query objects sql() method, and as arilia already mentioned, you'll be able to access the parameters bound to that query after is was compiled.
Having the compiled SQL and the associated value binder, you can combine this with a custom raw query to build your CREATE TABLE AS statement. All you need to do is prepare a new statement with the compiled SQL, and attach the value binder via its own attachTo() method.
One thing you might also have to do, is to define custom aliases in your select(), as otherwise you'd end up with columns selected (and created) in the form of Substances_id.
$Substances = TableRegistry::get('Substances');
$selectQuery = $Substances
->find()
->where($where_conditions)
->select(['id' => 'id']); // < create aliases as required
// compile the ORM query, this will populate the value binder
$selectSql = $selectQuery->sql();
// combine table creation SQL and compiled ORM query in a single statement
$createStatement = $Substances
->getConnection()
->prepare('CREATE TABLE dynamic_table AS ' . $selectSql);
// attach the ORM querys value binder, binding all its values to the given statement
$selectQuery->getValueBinder()->attachTo($createStatement);
$success = $createStatement->execute();
This should create SQL similar to:
CREATE TABLE dynamic_table AS
SELECT
id AS id
FROM
substances Substances
WHERE
...
See also
Cookbook > Database Access & ORM > Database Basics > Interacting with Statements
API > \Cake\ORM\Association::attachTo()

Related

CakePHP 3 hasMany association with bitwise expression

I have two tables, AclGroups and AclPermissions, and I want to create a hasMany relationship between them, i.e AclGroups has many AclPermissions.
The condition to determine whether a group owns a given permission is done in a single bitwise check. This is what i'm trying to do:
SELECT
*
FROM
acl_groups
JOIN
acl_permissions ON acl_permissions.permission & acl_groups.permission != 0
In AclGroupsTable I have tried the following:
$this->hasMany('AclPermissions', [
'className' => 'AclPermissions',
'foreignKey' => 'permission',
'conditions' => [
'AclPermissions.permission & AclGroups.permission !=' => 0
]
]);
But that just gives me
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'aclgroups.permission' in 'where clause'
In my controller I do:
$this->AclGroups->find('all')->contain(['AclPermissions']);
I suppose the real question is: Is there a way I can change the conditions of the ON clause in the query that fetches associated records
As mentioned in the comments, records of hasMany associations (and belongsToMany for that matter) will always be retrieved in a separate query when using contain().
If you need to create joins with such an association, then you must explicitly use the corresponding functionality for joins, for example leftJoinWith():
$this->AclGroups->find('all')->leftJoinWith('AclPermissions');
This will create a query similar to the one you are showing. However it would also generate the default conditions using the configured foreign key, you'd have to disable the foreign key in order to avoid that, like:
$this->hasMany('AclPermissions', [
'foreignKey' => false, // << like this
// ...
]);
Given that the association conditions won't work with contain() (and disabling the foreign key will make it even more unusable for that purpose), you may want to create a separate association for your joining purposes, or use the "lower level" join methods, where you specify all the conditions manually (you can for example put this in a custom finder in order to keep your code DRY):
$query = $this->AclGroups
->find('all')
->join([
'table' => 'acl_permissions',
'alias' => 'AclPermissions',
'type' => 'LEFT',
'conditions' => [
'AclPermissions.permission & AclGroups.permission != :permission'
]
])
->bind(':permission', 0, 'integer');
Note that the value is being explicitly bound here to ensure that the correct type is being used, as it couldn't be determined from the non-standard left hand value (which isn't really ment to contain SQL snippets - you may want want to look into using expressions).
See also
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Adding Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods

CakePHP Query - Conditional Based on Contain Field

I am developing a system that holds reports from customer's computers and displays failures in a list. I am attempting to write a query that locates all systems that have currently failed or have failures in the past.
My model for Computers has a field that says last_report_pass that allows me to quickly find computers that failed on the current day. My Reports are associated with a computer ID and has a field called status that says whether it was a pass or fail.
I am attempting to write a query that will only show last_report_pass being 0, or failed, or show it if it has reports that were found and joined (meaning there were previous failures).
Here was my idea:
$computers = $this->Computers->find('all', [
'conditions' => [
'last_report_pass' => '0',
'COUNT(Reports) NOT' => '0'
],
'contain' => [
'Reports' => [
'conditions' => [
'status' => '0'
]
]
);
I do not know what to do from here. I could probably write this in SQL but am trying to stick with Cake's ORM Query Builder. Thanks in advance!
You will need to use matching
its similar to contain, but it will filter by associated data:
It will be something like this
$query->distinct(['Computers.id'])
->matching('Reports', function ($q) {
return $q->where(['Reports.last_report_pass' => 0]);
});
Its important to notice that you will also have to contain the Reports table if you need to display some data which is on this table.
Reference

CakePHP 2 - Deriving table name of model within Behavior

Whats the usual practice in getting a behavior (linked to multiple models) to build filters for SQL queries, and then read the table that belongs to that model?
I have a Behavior function which is meant to do a database query with certain SQL conditions. I currently pass in the $this->request->data.
I have issues building the SQL conditions because i'm not sure how to derive the name of the table (that corresponds to the model). See below for example, I want to change "BillingCenterDetail" which is the table name (and also the model name), to something generic I can use across different models. I want this table name to be derived automatically based on the model name. I'm not sure if i can use the $model reference for that.
public function saveWithTimeConstraintCheck(Model $model, $data) {
//FIND ALL RECORDS THAT OVERLAP
$overlapfilter = array(
'BillingCenterDetail.billing_center_id =' => $data['BillingCenterDetail']['billing_center_id'],
'BillingCenterDetail.startdate <=' => $data['BillingCenterDetail']['enddate'],
'BillingCenterDetail.enddate >=' => $data['BillingCenterDetail']['startdate']
);
... after building the filter, I can use $model->find to execute the query, this should be OK because its generic.
$overlapresults = $model->find('all', array('conditions' => $overlapfilter));
I've answered my own question.
And actually to build filter conditions, I needed name of the model, not the name of the table, because the name of the table is a plural name with the "S" at the end.
I used
$Model->name
From:
CakePHP: get current model name in a controller
For table names i found out u can also use
$this->Model->table
cakephp - get table names and its column details

CakePHP - Passing an array of record IDs to paginate - is it possible?

Due to having to having to import data from an old non cake app and oddly built database table I need to pass paginate an array of records it is allowed to display - is this possible?
Normally I would reorganise the data into proper relationships etc but due to time scales etc this is not possible.
To give you more info - in my users table I have a field that contains a list of ID's that relate to documents they are allowed to view. The field will contain something like
123,23,45,56,765,122,11.9,71,25
Each ID refers to a document the documents model. I know that normally you would create proper ACOs and AROs and let the ACL/Auth componant handle which users can access what but this isnt an option this time around. So I thought if I could do it via paginate/find it might be an option?
Any help would be really appreciated.
Thanks in advance
You wouldn't pass it a list of IDs, you'd use the IDs in your paginate conditions - something like below.
(Code written off top of my head, so pardon any syntax errors...etc. It should give you the right idea/path at least):
//Controller
$this->loadModel('User');
$this->loadModel('Document');
$user = $this->User->findById($userId);
$documentIds = explode(',', $user['User']['doc_ids']);
$this->paginate = array(
'conditions' => array(
'id' => $documentIds
)
));
$documents = $this->paginate('Document');
When you pass an array as a condtion (eg. 'id'=>$arrayOfIds), it uses MySQLs "IN" - something like:
... WHERE id IN (45, 92, 173)

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