Cakephp Model with multiple status - php

I'm asking myself something... Here is the question :
I have a model Request. This model has a status. (let's say it is an integer)
As the status will change through the time AND as I want to keep a record of each changes this was pretty clear in my mind that the status will not be a field of the Request table/model.
I thought I will add a table/model RequestStatus and say to CakePHP
Request hasMany RequestStatus
Then I thought that it will be simple to retrieve all the Request that has a valid status only.
But I was wrong.
Using containable is not good as it retrieve the Request even if they have a wrong status (or even if they don't have a status).
So, I tested 2 solutions :
Binding on the fly the RequestStatus model as a hasOne relation but
it not works as expected
Use a joins key/array when making my find call.
There is clearly something that I missed because, this is not working.
Am I missing something ? Or the best way to do it is by having a status field in the Request table ?
EDIT : What I forgot to mention is that I need to use this query with the pagination of CakePHP

I will assume a couple of things you didn't mention. First of all, I suppose you also created a RequestStatus belongsTo Request relationship. And I imagine your RequestStatus table have four columns: id, status, request_id and created. The request_id column is the foreign key and created is the magic field that CakePHP populates when a new row is created. I also assume you are using Containable behaviour.
With that, you can go three different ways:
1) You can retrieve last valid Statuses along with their Requests.
In your RequestModel:
public function getValidRequests() {
$db = $this->RequestStatus->getDataSource();
$subquery = $db->buildStatement(array(
'fields' => array('request_id', 'max(created) as lastcreated'),
'table' => $db->fullTableName($this->RequestStatus),
'alias' => 'temp',
'group' => array('request_id')
), $this->RequestStatus);
return $this->RequestStatus->find('all', array(
'contain' => array('Request'),
'joins' => array(
array(
'table' => '(' . $subquery . ')',
'alias' => 'rs2',
'type' => 'right',
'conditions' => array(
'RequestStatus.request_id = rs2.request_id',
'RequestStatus.created = rs2.lastcreated'
)
)
),
'conditions' => array('RequestStatus.status' => 1),
'order' => array('RequestStatus.request_id' => 'desc')
));
}
2) You can retrieve all Requests with their Status, then return only those with last valid status.
In your RequestModel:
public function getValidRequests() {
$requests = $this->find('all', array(
'contain' => array(
'RequestStatus' => array(
'order' => array('RequestStatus.created' => 'desc')
)
)
));
$result = array();
foreach ($requests as $request) {
if ($request['RequestStatus'][0]['status'] == 1)
$result[] = $request;
}
return $result;
}
3) Denormalization.
Add a last_status column in your Request table. So every time you update a request's status, insert that new status into your RequestStatus table and save that new status to Request.last_status. Now you can simply do:
public function getValidRequests() {
return $this->find('all', array(
'conditions' => array('Request.last_status' => 1)
));
}

Related

Limit 'contain' to the first one (CakePHP 3)

I have to Tables: Users hasmany Memberhip. I would like to build a query to get ALL Users but EACH User should only contain the FIRST Membership (after ordering).
$users = $this->Users->find()->contain([
'Membership' => function ($q) {
return $q->order([
'year' => 'ASC'
])->limit(1);
},
'Countries',
'Board',
]);
Seems good so far. The problem is, that this query only gets a single Membership alltogether. So in the end of all Users that are beeing fetched, only one User has one Membership.
How do I get CakePHP to fetch ONE Membership for EACH User?
Thanks!
Sort key for hasOne option doesn't exist (see : CakePhp3 issue 5007)
Anyway I had the same problem and thanks to ndm : here his solution
I ended up using this:
$this->hasOne('FirstPhoto', [
'className' => 'Photos',
'foreignKey' => false,
'conditions' => function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
$subquery = $query
->connection()
->newQuery()
->select(['Photos.id'])
->from(['Photos' => 'photos'])
->where(['Photos.report_id = Reports.id'])
->order(['Photos.id' => 'DESC'])
->limit(1);
return $exp->add(['FirstPhoto.id' => $subquery]);
}
]);
Reference: How to limit contained associations per record/group?
You can do this is by creating another association (Make sure you don't include a limit you don't need one as the hasOne association creates the limit):
$this->hasOne('FirstMembership', [
'className' => 'Memberships',
'foreignKey' => 'membership_id',
'strategy' => 'select',
'sort' => ['FirstMembership.created' => 'DESC'],
'conditions' => function ($e, $query) {
return [];
}]);
This works for limiting to 1 but if you want to limit to 10 there still seems to be no good solution.

CakePHP ignores condition

I have the following problem with CakePHP:
Two tables are joined (filters and accounts). Then I am building conditions and only the second condition Account.active =>1 gets executed. If I print the result, there are still showing filters that are having another mode_id than 3.
$joins= array(
array('table' => 'filters',
'alias' => 'Filter',
'type' => 'right',
'conditions' => array(
'Filter.account_id = Account.id',
)
),
);
Then I execute the request including joins and conditions
$activeAccounts = $this->Account->find('all',array(
'conditions'=>array('AND'=>array('Filter.mode_id'=>3,'Account.active'=>1)),
'joins'=>$joins));
The models were checked and no problems identified. Filter belongs to Account. Account has many Filter.
Below the query that is generated. The results are still showing filters with Filter.mode_id other than 3
Here is the query that is generated. The results are still containing rows with Filter.mode_id other than 3 despite the fact that one condition is 'Filter.mode_id'=>3
SELECT `Account`.`id`, `Account`.`user_id`, `Account`.`name`,
`Account`.`api_key`, `Account`.`account_number`, `Account`.`remaining_balance`,
`Account`.`investment_size`, `Account`.`active`
FROM `baseline_db`.`accounts` AS `Account`
right JOIN `baseline_db`.`filters` AS `Filter`
ON (`Filter`.`account_id` = `Account`.`id`)
WHERE ((`Filter`.`mode_id` = 3) AND
(`Account`.`active` = '1'))
Like say Oldskool, use the Model associations
and for your condition, The "AND" is not necessary,
you cant put :
$activeAccounts = $this->Account->find('all',array(
'conditions' => array(
'Filter.mode_id'=>3,
'Account.active'=>1
)
));
the request you want to make with the type of relation you have, seem to me weird.
If i understand, perhaps with something like that :
$this->loadModel('Filter');
$filters =$this->Filter->find("list", array(
'conditions' => array('Filter.mode_id' => 3),
'fields' => array('Filter.account_id')
));
$activeAccounts = $this->Account->find('all',array(
'conditions' => array(
'Account.account_id'=>$filters,
'Account.active'=>1
)
));

CakeDC search plugin using complex conditions in query with HABTM

I wrote 2 days ago to ask about andConditions and it appeared that I didn't understand the idea but the fact is that for two days now I am stuck with the next step using CakeDC:
How do I implement complex HABTM conditions in "query" methods for CakeDC search plugin?
I have Offer HABTM Feature (tables: offers, features, features_offers) and the below works just fine when used in controller:
debug($this->Offer->find('all', array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id in (8, 10)',
)
)
)
)
)
);
The problem comes when I want to use the same conditions in the search:
public $filterArgs = array(
array('name' => 'feature_id', 'type' => 'query', 'method' => 'findByFeatures'),
);
........
public function findByFeatures($data = array()) {
$conditions = '';
$featureID = $data['feature_id'];
if (isset($data['feature_id'])) {
$conditions = array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id' => $data['feature_id'],
)
)
)
);
}
return $conditions;
}
I get an error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'contain' in 'where clause'
which makes me think that I cannot perform this search and/or use containable behavior in searches at all.
Can someone with more experience in the field please let me know if I am missing something or point me to where exactly to find a solution for that - perhaps a section in the cookbook?
EDIT: Also tried the joins. This works perfectly fine in the controller, returning all the data I need:
$options['joins'] = array(
array('table' => 'features_offers',
'alias' => 'FeaturesOffers',
'type' => 'inner',
'conditions' => array(
'Offer.id = FeaturesOffers.offer_id'
),
array('table' => 'features',
'alias' => 'F',
'type' => 'inner',
'conditions' => array(
'F.id = FeaturesOffers.feature_id'
),
)
),
);
$options['conditions'] = array(
'feature_id in (13)' //. $data['feature_id']
);
debug($this->Offer->find('all', $options));
... and when I try to put in the search method I get the returned conditions only in the where clause of the SQL
WHERE ((joins = (Array)) AND (conditions = ('feature_id in Array')))
...resulting in error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'joins' in 'where clause'
EDIT: Maybe I am stupid and sorry to say that but the documentation of the plugin sucks a ton.
I double, triple and quadruple checked (btw, have lost already 30 hours at least on 1 filed of the search form facepalm) and the stupid findByTags from the documentation still doesn't make any sense to me.
public function findByTags($data = array()) {
$this->Tagged->Behaviors->attach('Containable', array('autoFields' => false));
$this->Tagged->Behaviors->attach('Search.Searchable');
$query = $this->Tagged->getQuery('all', array(
'conditions' => array('Tag.name' => $data['tags']),
'fields' => array('foreign_key'),
'contain' => array('Tag')
));
return $query;
}
As I understand it
$this->Tagged
is supposed to be the name of the model of the HABTM association.
This is quite far from the standards of cakePHP though: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
The way it is described here, says that you don't need another model but rather you associate Recipe with Ingredient as shown below:
class Recipe extends AppModel {
public $hasAndBelongsToMany = array(
'Ingredient' =>
array(
'className' => 'Ingredient',
'joinTable' => 'ingredients_recipes',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'ingredient_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
meaning that you can access the HABTM assoc table data from Recipe without needing to define model "IngredientRecipe".
And according to cakeDC documentation the model you need is IngredientRecipe and that is not indicated as something obligatory in the cakePHP documentation. Even if this model is created the HABTM assoc doesn't work properly with it - I tried this as well.
And now I need to re-write the search functionality in my way, using only cakePHP even though I spent already 30 hours on it... so unhappy. :(
Every time I come to do this in a project I always spend hours figuring out how to do it using CakeDC search behavior so I wrote this to try and remind myself with simple language what I need to do. I've also noticed that although Using the CakeDC search plugin with associated models this is generally correct there is no explanation which makes it more difficult to modify it to one's own project.
When you have a "has and belongs to many" relationship and you are wanting to search the joining table i.e. the table that has the two fields in it that joins the tables on either side of it together in a many-to-many relationship you want to create a subquery with a list of IDs from one of the tables in the relationship. The IDs from the table on the other side of the relationship are going to be checked to see if they are in that record and if they are then the record in the main table is going to be selected.
In this following example
SELECT Handover.id, Handover.title, Handover.description
FROM handovers AS Handover
WHERE Handover.id in
(SELECT ArosHandover.handover_id
FROM aros_handovers AS ArosHandover
WHERE ArosHandover.aro_id IN (3) AND ArosHandover.deleted != '1')
LIMIT 20
all the records from ArosHandover will be selected if they have an aro_id of 3 then the Handover.id is used to decide which Handover records to select.
On to how to do this with the CakeDC search behaviour.
Firstly, place the field into the search form:
echo $this->Form->create('Handover', array('class' => 'form-horizontal'));?>
echo $this->Form->input('aro_id', array('options' => $roles, 'multiple' => true, 'label' => __('For', true), 'div' => false, true));
etc...
notice that I have not placed the form element in the ArosHandover data space; another way of saying this is that when the form request is sent the field aro_id will be placed under the array called Handover.
In the model under the variable $filterArgs:
'aro_id' => array('name' => 'aro_id', 'type' => 'subquery', 'method' => 'findByAros', 'field' => 'Handover.id')
notice that the type is 'subquery' as I mentioned above you need to create a subquery in order to be able to find the appropriate records and by setting the type to subquery you are telling CakeDC to create a subquery snippet of SQL. The method is the function name that are going to write the code under. The field element is the name of the field which is going to appear in this part of the example query above
WHERE Handover.id in
Then you write the function that will return the subquery:
function findByAros($data = array())
{
$ids = ''; //you need to make a comma separated list of the aro_ids that are going to be checked
foreach($data['aro_id'] as $k => $v)
{
$ids .= $v . ', ';
}
if($ids != '')
{
$ids = rtrim($ids, ', ');
}
//you only need to have these two lines in if you have not already attached the behaviours in the ArosHandover model file
$this->ArosHandover->Behaviors->attach('Containable', array('autoFields' => false));
$this->ArosHandover->Behaviors->attach('Search.Searchable');
$query = $this->ArosHandover->getQuery('all',
array(
'conditions' => array('ArosHandover.aro_id IN (' . $ids . ')'),
'fields' => array('handover_id'), //the other field that you need to check against, it's the other side of the many-to-many relationship
'contain' => false //place this in if you just want to have the ArosHandover table data included
)
);
return $query;
}
In the Handovers controller:
public $components = array('Search.Prg', 'Paginator'); //you can also place this into AppController
public $presetVars = true; //using $filterArgs in the model configuration
public $paginate = array(); //declare this so that you can change it
// this is the snippet of the search form processing
public function admin_find()
{
$this->set('title_for_layout','Find handovers');
$this->Prg->commonProcess();
if(isset($this->passedArgs) && !empty($this->passedArgs))
{//the following line passes the conditions into the Paginator component
$this->Paginator->settings = array('conditions' => $this->Handover->parseCriteria($this->passedArgs));
$handovers = $this->Paginator->paginate(); // this gets the data
$this->set('handovers', $handovers); // this passes it to the template
If you want any further explanation as to why I have done something, ask and if I get an email to tell me that you have asked I will give an answer if I am able to.
This is not an issue of the plugin but how you build the associations. You need to properly join them for a search across these three tables. Check how CakePHP is fetching the data from HABTM assocs by default.
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#joining-tables
Suppose a Book hasAndBelongsToMany Tag association. This relation uses
a books_tags table as join table, so you need to join the books table
to the books_tags table, and this with the tags table:
$options['joins'] = array(
array('table' => 'books_tags',
'alias' => 'BooksTag',
'type' => 'inner',
'conditions' => array(
'Books.id = BooksTag.books_id'
)
),
array('table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'conditions' => array(
'BooksTag.tag_id = Tag.id'
)
)
);
$options['conditions'] = array(
'Tag.tag' => 'Novel'
);
$books = $Book->find('all', $options); Using joins allows you to have
a maximum flexibility in how CakePHP handles associations and fetch
the data, however in most cases you can use other tools to achieve the
same results such as correctly defining associations, binding models
on the fly and using the Containable behavior. This feature should be
used with care because it could lead, in a few cases, into bad formed
SQL queries if combined with any of the former techniques described
for associating models.
Also your code is wrong somewhere.
Column not found: 1054 Unknown column 'contain' in 'where clause'
This means that $Model->contain() is somehow called. I don't see such a call in your code pasted here so it must be somewhere else. If a model method can not be found this error usually happens with the field name as column.
I want to share with everyone that the solution to working with HABTM searches with the plugin lies here: Using the CakeDC search plugin with associated models
#burzum, the documentation is far from ok man. Do you notice the use of 'type' => 'checkbox' and that it is not mentioned anywhere that it is a type?
Not to mention the total lack of grammar and the lots of typos and missing prepositions. I lost 2 days only to get a grasp of what the author had in mind and bind the words in there. No comment on that.
I am glad that after 5 days on the uphill work I made it. Thanks anyway for being helpful.

How can I find all records for a model without doing a long list of "OR" conditions?

I'm having trouble composing a CakePHP find() which returns the records I'm looking for.
My associations go like this:
User ->(has many)-> Friends ,
User ->(has many)-> Posts
I'm trying to display a list of all a user's friends recent posts, in other words, list every post that was created by a friend of the current user logged in.
The only way I can think of doing this is by putting all the user's friends' user_ids in a big array, and then looping through each one, so that the find() call would look something like:
$posts = $this->Post->find('all',array(
'conditions' => array(
'Post.user_id' => array(
'OR' => array(
$user_id_array[0],$user_id_array[1],$user_id_array[2] # .. etc
)
)
)
));
I get the impression this isn't the best way of doing things as if that user is popular that's a lot of OR conditions. Can anyone suggest a better alternative?
To clarify, here is a simplified version of my database:
"Users" table
id
username
etc
"Friends" table
id
user_id
friend_id
etc
"Posts" table
id
user_id
etc
After reviewing what you have rewritten, I think I understand what you are doing. Your current structure will not work. There is no reference in POSTS to friends. So based on the schema you have posted, friends CANNOT add any POSTS. I think what you are trying to do is reference a friend as one of the other users. Meaning, A users FRIEND is actually just another USER in the USERS table. This is a self referential HABTM relationship. So here is what I would propose:
1- First, make sure you have the HABTM table created in the DB:
-- MySQL CREATE TABLE users_users ( user_id char(36) NOT NULL,
friend_id char(36) NOT NULL );
2- Establish the relationships in the User model.
var $hasAndBelongsToMany = array(
'friend' => array('className' => 'User',
'joinTable' => 'users_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'friend_id',
'unique' => true,
),
);
var $hasMany = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'user_id'
),
);
3- use the scaffolding to insert a few records, linking friends and adding posts.
4- Add the view record function to the Users controller:
function get_user($id)
{
$posts = $this->User->find('first', array(
'conditions' => array('User.id' => $id),
'recursive' => '2'
));
pr($posts);
}
5- Now you can query the User table using recursive to pull the records using the following command:
http://test/users/get_user/USER_ID
6- Your output will show all of the records (recursively) including the friends and their posts in the returned data tree when you pr($posts)
I know this is a long post, but I think it will provide the best solution for what you are trying to do. The power of CakePHP is incredible. It's the learning curve that kills us.
Happy Coding!
If Post.user_id points to Friend.id (which wouldn't follow the convention btw) then it would be
$posts = $this->Post->find('all',array(
'conditions' => array(
'Post.user_id' => $user_id_array
)
);
which would result in .. WHERE Post.user_id IN (1, 2, 3) ..
Depending on your setup, it might be quicker to run two queries rather than trying to chain them together via the Cake stuff. I'd recommend adding something like getFriendsPosts() in the Users model.
<?php
class UserModel extends AppModel {
// ... stuff
function getFriendsPosts( $user_id )
{
$friends = $this->find( ... parameters to get user IDs of all friends );
// flatten the array or tweak your params so they fit the conditions parameter. Check out the Set class in CakePHP
$posts = $this->find( 'all', array( 'conditions' => array( 'User.id' => $friends ) ) );
return $posts;
}
}
?>
Then to call it, in the controller just do
$friends = $this->User->getFriendsPosts( $this->Auth->User('id') );
HTH,
Travis
Isn't CakePHP already generating the efficient code of:
SELECT * from Posts WHERE user_id IN (id1, id2 ...)
if not, you can do
$conditions='NULL';
foreach($user_id_array as $id) $conditions.=", $id";
$posts = $this->Posts->find('all', array(
'conditions' => "Post.user_id IN ($conditions)",
));
If your models are properly associated, Cake will automatically retrieve related model records. So, when you search for a specific user, Cake will automatically retrieve related friends, and related posts of these friends. All you need to do is set the recursion level high enough.
$user = $this->User->find('first', array('conditions' => array('User.id' => $id), 'recursive' => 2));
debug($user);
// gives something like:
array(
User => array()
Friend => array(
0 => array(
...
Post => array()
),
1 => array(
...
Post => array()
)
)
)
All you need to do is extract the posts from the user's friends, which is as easy as:
$postsOfFriends = Set::extract('/Friend/Post/.', $user);

CakePHP order query results on more than 1 level

I'm using the Containable behavior to get a list of Comments (belongsTo Post, which belongs to Question; Question hasMany Post, and Post hasMany Comments; all of these belong to Users).
$data = $this->Question->find ( 'first',
array ('contain' =>
array ('User',
'Post' => array ('User', /* 'order' => 'User.created DESC'*/ )
)
)
);
It works, when I comment out the section in comments above. I suppose this is to be expected, but what I want is all of the Posts that are found, should be sorted in order of the 'created' field of the 'User' they belong to. How do I accomplish this deeper level sorting in CakePHP? I always get, "Warning (512): SQL Error: 1054: Unknown column 'User.created' in 'order clause'"
Thanks for your help!
Also, you might be trying to group on a related table from a find call that doesn't use joins.
Set your debug level to something greater than 1 so you can see the query log and make sure that Cake isn't doing two queries to fetch your data. If that is the case then the first query is not actually referencing the second table.
If you want to manually force a join in these situations you can use the Ad-Hoc joins method outlined by Nate at the following link.
http://bakery.cakephp.org/articles/view/quick-tip-doing-ad-hoc-joins-in-model-find
I have found two ways to get around this.
The first is to define the second level associacion directly in the model.
Now you will have access to this data everywhere.
It should look something like this.....
var $belongsTo = array(
'Foo' => array(
'className' => 'Foo', //unique name of 1st level join ( Model Name )
'foreignKey' => 'foo_id', //key to use for join
'conditions' => '',
'fields' => '',
'order' => ''
),
'Bar' => array(
'className' => 'Bar', //name of 2nd level join ( Model Name )
'foreignKey' => false,
'conditions' => array(
'Bar.id = Foo.bar_id' //id of 2nd lvl table = associated column in 1st level join
),
'fields' => '',
'order' => ''
)
);
The problem with this method is that it could make general queries more complex than they need be.
You can thus also add the second level queries directly into te find or paginate statement as follows: (Note: I found that for some reason you can't use the $belongsTo associations in the second level joins and will need to redefine them if they are already defined. eg if 'Foo' is already defined in $belongsTo, you need to create a duplicate 'Foo1' to make the association work, like the example below.)
$options['joins'] = array(
array('table' => 'foos',
'alias' => 'Foo1',
'type' => 'inner',
'conditions' => array(
'CurrentModel.foo_id = Foo1.id'
)
),
array('table' => 'bars',
'alias' => 'Bar',
'type' => 'inner',
'foreignKey' => false,
'conditions' => array(
'Bar.id = Foo1.bar_id'
)
)
);
$options['conditions'] = array('Bar.column' => "value");
$this->paginate = $options;
$[modelname] = $this->paginate();
$this->set(compact('[modelname]'));
I hope this is clear enough to understand and that it helps someone.
Check your recursive value. If it's too limiting, it will ignore the containable links, IIRC. I remember bumping into this a few times. I'd try containing multiple models, but my recursive option was set to 0 and nothing would get pulled. For your example, I'd think that a value of 1 (the default) would suffice, but maybe you've explicitly set it to 0 somewhere?
You can add before your call to find() the following:
$this->Question->order = 'Question.created DESC';
Yeah, I couldn't work out how to sort based on the related/associated model, so ended up using the Set::sort() method. Checkout this article for a good explanation.
// This finds all FAQ articles sorted by:
// Category.sortorder, then Category.id, then Faq.displaying_order
$faqs = $this->Faq->find('all', array('order' => 'displaying_order'));
$faqs = Set::sort($faqs, '{n}.Category.id', 'ASC');
$faqs = Set::sort($faqs, '{n}.Category.sortorder', 'ASC');
...And yes, it should probably be a Category->find() but unfortunately the original developer didn't code it that way, and I didn't wanna rework the views.

Categories