In croogo's search-View from the Nodes-controller the results include only nodes, where the searched term was found on the Node's model fields, but will skip any translated content for the nodes.
I'm trying to override this functionality and add support for translated content as well, but having a hard time to override the search-view without having to override the whole node-controller.
Does anyone has done this before and could give me some advice, as which approach could I take?
For anyone interested, I ended up doing the following:
Add a new SearchController on my Plugin with a show-view, which is an adapted version of the search-View on the NodesController.
Override the search/* route with my new view.
In order to search in the translated fields, I came with 2 possible approaches:
1) If using only the paginate for this, then there would be necessary to join the node and the i18n tables, first And then look for the string additionally on the joined fields.
2) Or, first find all distinct nodes with matching content on the i18n table through a query. And then use this list of nodes to add condition of "OR Node.id in (list_of_nodes)"
I ended up with alternative 2, and this is how the show-view looks like:
public function show() {
if (!isset($this->request->params['named']['q'])) {
$this->redirect('/');
}
App::uses('Sanitize', 'Utility');
$q = Sanitize::clean($this->request->params['named']['q']);
$results = $this->Node->query(
"SELECT DISTINCT(foreign_key) FROM `i18n` " .
"WHERE content LIKE '%" . $q . "%';");
$node_ids = array();
foreach($results as $res) {
$node_ids[] = $res['i18n']['foreign_key'];
}
$this->paginate['Node']['order'] = 'Node.created DESC';
$this->paginate['Node']['limit'] = Configure::read('Reading.nodes_per_page');
$this->paginate['Node']['conditions'] = array(
'Node.status' => 1,
'AND' => array(
array(
'OR' => array(
'Node.title LIKE' => '%' . $q . '%',
'Node.excerpt LIKE' => '%' . $q . '%',
'Node.body LIKE' => '%' . $q . '%',
'Node.id' => $node_ids,
),
),
array(
'OR' => array(
'Node.visibility_roles' => '',
'Node.visibility_roles LIKE' => '%"' . $this->Croogo->roleId . '"%',
),
),
),
);
// some more stuff ...
}
Related
I am converting some finds from cakephp2 to cakephp3.
I need to search on a first name and surname on a tutors table that are stored on different columns. According to the docs I needed to have a LEFT join bewtween the tables. I have used an or condition with 2 fields but it works like and 'and' condition if both parameters have a value.
My issue is
q1) I cant get the data with just the first name only , and the surname is null.
q2) I need to pass both first name and surname to get just those data with that name.
Not sure how to do this in cakephp3.
eg $a5='fred'; //just want all first names like fred
$a6=null; //sometimes this will be filled
$a3='2015-05-30';
$a4='2016-06-01';
$query3 = $this->Lessons->find()
->contain(['Tutors'])
->select(['lessons.id','lessons.lesson_date','tutors.id','tutors.first_name','tutors.last_name' ])
->where(['Lessons.lesson_date >' => $a3,'Lessons.lesson_date <' => $a4,
'OR' => [['tutors.first_name like' => '%'.$a5.'%'], ['tutors.last_name like' => '%'.$a6.'%']],
]);
foreach ( $query3 as $row) {
debug($row->toArray());
}
I didnt understand the docs on this point.
http://book.cakephp.org/3.0/en/orm/query-builder.html#advanced-conditions
UPDATE- tried this and this also just gives all the data with either 'at' or 'to' but it should be any names with both 'at' and 'to' in them.
$query3 = $this->Lessons->find()
->contain(['Tutors','Subjects', 'TutoringTypes','Terms','Students'])
->select(['lessons.id','lessons.lesson_date','tutors.id','tutors.first_name','tutors.last_name',
'subjects.id','subjects.name','terms.id','terms.title'])
->where(['Lessons.lesson_date >' => $a3,'Lessons.lesson_date <' => $a4])
->orWhere(function ($exp) {
return $exp->and_([
'tutors.first_name like' => '%an%',
'tutors.last_name like' => '%to%',
]);
});
Pay attention to the generated SQL. You either see it in DebugKit or debug it by calling $query->sql(). You're building a wrong query:
You generate OR ((... AND ...)) because you're using an and_. You probably want ... OR ....
->orWhere(function ($exp) {
return $exp->and_([
'tutors.first_name like' => '%an%',
'tutors.last_name like' => '%to%',
]);
});
You probably want OR. But passing the array directly to orWhere() would work as well.
For CakePhp 2.5
I have the following search function in my Controller.
public function search($query = null, $lim = null)
{
$tokens = explode(" ", $query);
$this->Paginator->settings = array(
'conditions' => array(
'Customer.site_id LIKE' => '%' . $this->viewVars['shopId'],
'CONCAT(Customer.first_name," ",Customer.last_name," ",Customer.organisation) LIKE' => '%' . implode(' ', $tokens) . '%'
),
'limit' => $lim
);
$this->set('customers', $this->Paginator->paginate());
}
This works fine, and gets the results i want.
However, its been suggested to me that I should put these search functions in my model. I can easily do this, and have done so similar as follows in my model:
public function getActiveTasks($id){
return $this->Task->find('all', array(
'conditions' => array(
'Task.customer_id' => $id,
'Task.status' => 0
),
'order' => array('Task.due_date ASC'),
'recursive' => -1,
));
}
The issue I'm having is that I cannot (or don't know how to) use Paginator with custom searches. Is there a way to paginate results from custom functions?
ie, can I do the following:
public function CustomModelSearch($query = null, $lim = null)
{
return $this->Task->find('all', array(
'conditions' => array(
'Customer.site_id LIKE' => '%' . $this->viewVars['shopId'],
'CONCAT(Customer.first_name," ",Customer.last_name," ",Customer.organisation) LIKE' => '%' . implode(' ', $tokens) . '%'
),
'limit' => $lim
));
}
and then in the controller call
$results = $this->Model->CustomModelSearch($query, $lim);
$this->set('results',$this->Paginate($results));
I cannot find a way to pass results into the paginator to render paged results to the view, unless I use it via the controller as per the first piece of code, which i've been told isn't good MVC principles.
To create custom pagination you have two options:
Create a Custom Query Pagination with a paginate and paginateCount funtions
// paginate and paginateCount implemented on a behavior.
public function paginate(Model $model, $conditions, $fields, $order, $limit,
$page = 1, $recursive = null, $extra = array()) {
// method content
}
public function paginateCount(Model $model, $conditions = null, $recursive = 0,
$extra = array()) {
// method body
}
or create a custom find and set Paginator to use that find.
good to understand but the truth should tell me that bug hits you in what you're doing.
The function in the model of "Task" you should not put $this->task->findjust$this->find that if you are in the model not need specify which model you are using, ou only need to do this in the controller.
model/Task.php:
public function CustomModelSearch($query = null, $lim = null)
{
return $this->find('all', array(
'conditions' => array(
'Customer.site_id LIKE' => '%' . $this->viewVars['shopId'],
'CONCAT(Customer.first_name," ",Customer.last_name," ",Customer.organisation) LIKE' => '%' . implode(' ', $tokens) . '%'
),
'limit' => $lim
));
}
I am working on cakephp now.Please explain the process of custom mysql query in cake php.
we have to write query by using
$this->Model->query();
and I return this to controller.In controller,i loaded the model in particular function and i called the function and set that function to view like this
$this->set('post',$this->User->get());
is it correct process?please explain the code ...
What query do you want to write this way? It is possible to write nearly all queries using the CakePHP ORM. Using the query building functions of CakePHP is the prefered way of doing it.
All data fetching an manipulation should be done in a model as well. See Separation of Concerns.
Here is a complete model method to fetch an user record based on its id OR slug.
public function view($userId, $options = []) {
$defaults = array(
'contain' => array(),
'conditions' => array(
'OR' => array(
$this->alias . '.' . $this->primaryKey => $userId,
$this->alias . '.slug' => $userId,
),
$this->alias . '.email_verified' => 1
),
);
$result = $this->find('first', Hash::merge($defaults, $options));
if (empty($result)) {
throw new NotFoundException(__d('user_tools', 'User not found!'));
}
return $result;
}
Controller:
public function view($userId = null) {
$this->set('user', $this->User->view($userId);
}
Alternative but NOT preferred mode method to fetch te data
public function view($userId, $options = []) {
$result = $this->query(/* Your query here */);
if (empty($result)) {
throw new NotFoundException(__d('user_tools', 'User not found!'));
}
return $result;
}
Your solution is correct, let me eleborate it in more detail
1st Solution
In your controller
$arrayTemp =array();
$arrayTemp = $this->ModelName->query(' Your Query String ');
$this->set('post',$arrayTemp);
2nd Solution
In your model class
function returnDate(){
return $this->query('Your Query String')
}
In Controller class
$arrayTemp = $this->ModelName->returnDate();
$this->set('post',$arrayTemp);
}
I am using the tree behavior and when I query via ->find('threaded', ...) - as expected - I get the tree back.
But I want additional joins happen, so something like:
$data = $this->Category->find('threaded', array(
'joins' => array(
array('table' => 'videos',
'alias' => 'Video',
'type' => 'LEFT',
'conditions' => array(
'Category.id = Video.category_id',
)
)
)
));
Category hasMany Video, but Video is not a tree, just related.
Can I use the threaded query for that?
To produce the "threaded" output Cake 1) calls a find('all), then 2) puts the resulting array through Set::nest() function.
So just get your output using a standard find + your custom joins, then just use the Set::nest
(NB: Hash has replaced Set in Cake 2, but Cake still uses Set internally. Both will work for now. Hash::nest )
So if you take a look at Cake's model.php, the nest function is called like so:
return Set::nest($results, array(
'idPath' => '/' . $this->alias . '/' . $this->primaryKey,
'parentPath' => '/' . $this->alias . '/' . $parent
));
Use that as a template for your call. For your data it would look something like:
return Set::nest($results, array(
'idPath' => '/Category/id',
'parentPath' => '/Category/parent_id' ));
For example, I have this relationship:
UserContact hasMany Contact
Contact hasOne Info
Contact hasMany Response
And I need to paginate Contact, so I use Containable:
$this->paginate = array(
'limit'=>50,
'page'=>$page,
'conditions' =>array('Contact.id'=>$id),
'contain'=>array(
'Response',
'Info'
)
);
I want to add search by Info.name, and by Response.description. It works perfect for Info.name, but it throws an error if I try using Response.description, saying that the column doesn't exist.
Additionally, I tried changing the relationship to Contact hasOne Response, and then it filters correctly, but it only returns the first response and this is not the correct relationship.
So, for example, if I have a search key $filter I'd like to only return those Contacts that have a matching Info.name or at least one matching Response.description.
If you look at how CakePHP constructs SQL queries you'll see that it generates contained "single" relationships (hasOne and belongsTo) as join clauses in the main query, and then it adds separate queries for contained "multiple" relationships.
This makes filtering by a single relationship a breeze, as the related model's table is already joined in the main query.
In order to filter by a multiple relationship you'll have to create a subquery:
// in contacts_controller.php:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$dbo = $this->Contact->getDataSource();
$subQuery = $dbo->buildStatement(array(
'fields' => array('Response.id'),
'table' => $dbo->fullTableName($this->Contact->Response),
'alias' => 'Response',
'conditions' => $conditionsSubQuery
), $this->Contact->Response);
$subQuery = ' EXISTS (' . $subQuery . ') ';
$records = $this->paginate(array(
'Contact.id' => $id,
$dbo->expression($subQuery)
));
But you should only generate the subquery if you need to filter by a Response field, otherwise you'll filter out contacts that have no responses.
PS. This code is too big and ugly to appear in the controller. For my projects I refactored it into app_model.php, so that each model can generate its own subqueries:
function makeSubQuery($wrap, $options) {
if (!is_array($options))
return trigger_error('$options is expected to be an array, instead it is:'.print_r($options, true), E_USER_WARNING);
if (!is_string($wrap) || strstr($wrap, '%s') === FALSE)
return trigger_error('$wrap is expected to be a string with a placeholder (%s) for the subquery. instead it is:'.print_r($wrap, true), E_USER_WARNING);
$ds = $this->getDataSource();
$subQuery_opts = array_merge(array(
'fields' => array($this->alias.'.'.$this->primaryKey),
'table' => $ds->fullTableName($this),
'alias' => $this->alias,
'conditions' => array(),
'order' => null,
'limit' => null,
'index' => null,
'group' => null
), $options);
$subQuery_stm = $ds->buildStatement($subQuery_opts, $this);
$subQuery = sprintf($wrap, $subQuery_stm);
$subQuery_expr = $ds->expression($subQuery);
return $subQuery_expr;
}
Then the code in your controller becomes:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$records = $this->paginate(array(
'Contact.id' => $id,
$this->Contact->Response->makeSubQuery('EXISTS (%s)', array('conditions' => $conditionsSubQuery))
));
I can not try it now, but should work if you paginate the Response model instead of the Contact model.