In CakePHP 2.x we have the find('all','callbacks'=>false)
What is an equivalent alternative in CakePHP3?
I have a situation where in my beforeFind callback (in my model's behavior) I'm appending a site_id to every query (for a multi-tenant app).
90% of the time I want this query appended via beforeFind, but %10 of the time I want my finds to ignore the callbacks.
I've looked at: Cakephp 3: How to ignore beforefind for specific queries? which comes close, but applying that method won't 'chain' the ignored beforeFind() callback on associated models, which I need it to do.
Updated code:
I've got two tables 'sites' and 'details' sites hasOne details, details belongs to sites. Inner Join.
In my AppController's initialize() function I've got
$tbl = TableRegistry::get( 'Sites' );
$options = [
'conditions' =>
['Sites.domain' => 'three.dev.mac',
'Sites.is_current' => 1,
'Sites.is_archive' => 0],
'contain' => 'Details', // this contain causes the problem
'ignoreCallbacks' => true
];
$tenant_details = $tbl->find('all',$options)->first();
My models beforeFind() behavior callback
public function beforeFind( Event $event, Query $query, ArrayObject $options ) {
debug($options);
if(isset($options['ignoreCallbacks']) && $options['ignoreCallbacks'] === true) {
// don't filter where clause
}
else{
// filter where clause by default
$query->where( [$this->_table->alias().'.'.'site_id'=> 7 ]);
}
return $query;
}
If I comment out the 'contain' line of my find call, the where query get's ignored as it should and my debug call returns 'ignoreCallbacks' => true' which is great.
If I leave the 'contain' line in the find() call (which is what I want) I get 2 debug outputs from beforeFind(), the first has 'ignoreCallbacks' => true', the second is empty. Apparently the second overrides the first and the query tries to append a site_id, which I don't want.
Any thoughts?
http://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html
Any options that are not in this list will be passed to beforeFind listeners where they can be used to modify the query object. You can use the getOptions() method on a query object to retrieve the options used.
So just pass a custom option to your queries find() call as 2nd arg and read that option in the beforeFind() like described above.
if (isset($options['useSiteId']) && $options['useSiteId'] === true) { /*...*/ }
I've found a way to (although it seems ugly) to have the custom $options that are passed (as #burzum mentioned in his answer) in a find call to the associated table's beforeFind() method if using 'contain' in the find. Hope this helps someone who was experiencing the same issue.
$tbl = TableRegistry::get( 'Sites' );
$options = [
'conditions' =>
$conditions,
'contain' => [
'Details'=> function ($q) {
return $q->applyOptions(['ignoreCallbacks' => true]); // IMPORTANT this is required to send the 'ignoreCallbacks' option to the contained table.
}
],
'ignoreCallbacks' => true
];
$tenant_details_query = $tbl->find('all',$options)->first();
Related
I have a blog model, and I suspect I'm not writing my code correctly to best take advantage of CakePHP's MVC structure.
Here are some snippets from my Posts controller.
public function view() {
$posts = $this->Posts->find('all')->contain([
'Comments' => ['Users'],
'Users'
]);
$this->set(compact('posts'));
}
public function index() {
$post = $this->Posts->find('all')->contain([
'Users'
])->limit(20);
$this->set(compact('post'));
}
This is a snippet from the index.ctp template
foreach ( $posts as $post ) {
<div class="post">
<h1 class="title>
<?= h($post->title) ?>
<small><?php echo $post->getCommentCount(); ?> comments</small>
</h1>
<div class="body">
<?= h($post->body) ?>
</div>
</div>
}
In my Post Entity, I have the following function
public function getCommentCount(){
$count = 0;
foreach ($this->comments as $comment){
if ( $comment->isPublished() ) {
$count += 1;
}
}
return $count;
}
My problem is I need to call the getCommentCount function from the index.ctp where the $posts object has no comment children (which the function uses).
Am I misunderstanding how the Entity functions should be coded? Instead of accessing expected variables of this object which sometimes aren't there, should I be querying the database from the Entity? Is there another approach I should be doing?
Am I misunderstanding how the Entity functions should be coded?
Yes, you do, because...
Instead of accessing expected variables of this object which sometimes aren't there, should I be querying the database from the Entity? Is there another approach I should be doing?
...an entity should be a dumb data object. When you fetch data from there you add business logic. Any data manipulation should happen somewhere in the model layer, or a service. Most of the time people using CakePHP put code into the table object, which is somewhat OKish. I'm creating additional classes in the model layer for different things and namespace them App\Model\SomeModule or directly in my app root App\SomeModule and inject whatever else I need there (request, tables...).
This code is also absolute inefficient:
public function getCommentCount(){
$count = 0;
foreach ($this->comments as $comment){
if ( $comment->isPublished() ) {
$count += 1;
}
}
return $count;
}
It assumes all comments were loaded
It is required to get all comments to get an accurate count
it iterates over all comments to filter published comments
It does it inside the entity
What if there are 500 comments? Why aren't you simply doing a count query on the database and filter them by their published status? But this would still require you to do one additional query per record. So for counts it is a lot more efficient and easy if you use the Counter Cache Behavior.
If you really need to manipulate data after you fetched it but before rendering it use map/reduce. You can refactor your getCommentCount() and use map/reduce instead if you would like to stick to your inefficient way of doing it. The linked documentation even contains an example for counting stuff.
In your index funcition you set $post, but in the index.ctp you use $posts, and you missed out to contain Comments relationship in your query.
I changed it for you:
public function index() {
$posts = $this->Posts->find('all')->contain([
'Users',
'Comments'
])
->limit(20);
$this->set(compact('posts'));
}
And based on your comment, you could get the total comments in a query like this:
public function index() {
$posts = $this->Posts->find();
$posts->select([
'total_comments' => $posts->func()->sum('Comments.id')
])
->select($this->Posts)
->contain([
'Users',
'Comments'
])
->limit(20);
$this->set(compact('posts'));
}
See Returning the Total Count of Records for more info.
You can do it this way:
$posts = $this->Posts->find('all' , [
'fields' => array_merge(
$this->Posts->schema()->columns(), [
'countCommentsAlias' => '(
SELECT COUNT(comments.id)
FROM comments
WHERE comments.post_id = Posts.id
)',
'Users.name',
'Users.email'
]
),
'contain' => ['Users']
])
->toArray();
I have an HTML form with the GET method, and five text input field, which should help to filter the data. When users fill one or more fields, these data are shown as url query.
My question is how to safely use the this query data without the possibility of SQL injection?
EDIT
Of course, is a simple filtering of user data, by name, location, etc., not fulltext search.
'first_name LIKE' => '%'.$this->request->query('first_name').'%'
Where is in the documentation explained bind method, like ?
->bind(':name', $this->request->query('name'))
To avoid SQL injection vulnerabilities, you can use query placeholders.
Your code should look something similar to
$query = $this->Users->find()
->where([
'first_name LIKE' => '%:name%'
])
->bind(':name', $this->request->query('first_name'));
More information in:
Binding Values in Cookbook 3.x: Database Access & ORM
Query::bind()
You should consider using Search Plugin
Its just very simple, write this in controller
public $components = array(
'Search.Prg'
);
public function index() {
$this->Prg->commonProcess();
$this->set('users', $this->paginate($this->Users->find('searchable',
$this->Prg->parsedParams())));
}
And this one in Model
public $filterArgs = array(
'first_name' => array(
'type' => 'like',
'field' => 'first_name'
)
);
public function initialize(array $config = []) {
$this->addBehavior('Search.Searchable');
}
and you are done.
For more examples, visit here
I just want to show on index.ctp all results from a model wich are true as conditions. Like this select:
select * from revistas where isonline = 'true';
Ive tried this code below on controller:
public function index() {
//$iscond = $this->Revista->findAllByNome('testa');
//$this->Revista->query("SELECT id, nome, acronimo, modified, user_id from xconv.revistas where isonline = 't'");
$this->Revista->find('all', array(
'contain' => array(
'conditions' => array('Revista.isonline' => 't'))));
$this->Revista->recursive = 0;
$this->set('revistas', $this->paginate());
}
Those 2 comments above tried b4. It doesn return any errors, but dont do the condition.
Is there any other file of the MVC to write more code ? What went wrong ??
Ty
TLDR:
You need to supply your conditions to the actual pagination as opposed to doing a find() query, then doing a completely separate $this->paginate().
Explanation:
$this->paginate(); will actually run a query for you. So - basically, in your question, you're running a query for no reason, then doing paginate() which runs another query that has no conditions.
Instead, apply the condition like this:
$this->Paginator->settings = array('conditions'=>array('Revista.isonline' => 't'));
$this->paginate();
Note: As noted by your comment and another answer, there are multiple ways to apply conditions to paginate - the main point to this answer is pointing out that you need to apply the conditions to the paginate, not a previous query.
Using your clues at #Dave's answer comments, I think this is a solution with the "paginator filter",
public function index() {
$this->Revista->recursive = 0; //your old code
$this->paginate['conditions']=array('Revista.isonline' => 't'); //new code
$this->set('revistas', $this->paginate()); //your old code
}
try this:
public function index() {
$this->Revista->find('all', array(
'conditions' => array('Revista.isonline' => 't')));
$this->Revista->recursive = 0;
$this->set('revistas', $this->paginate());
}
I am setting up a user/group system that allows users to send requests to join a group.
I can't seem to load the associated model and add a row. It's really really really difficult to resist the urge to just use $this->query() and be done with it... but I'm trying to learn the Cake conventions and do things the right way.
In my group model's function for handling group join requests:
$this->loadModel('GroupRequest');
$this->data = array('GroupRequest' =>
array('user_id' => $uid, 'group_id' => $gid));
if($this->GroupRequest->save($this->data)){ echo 'save success'; }
else { echo 'save fail'; }
Here are the errors I get when I run this:
Warning (512): SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'loadModel' at line 1 [CORE/cake/libs/model/datasources/dbo_source.php, line 684]
Query: loadModel
Notice (8): Undefined property: Group::$GroupRequest [APP/models/group.php, line 124]
Fatal error: Call to a member function save() on a non-object in /home/wxworksmat/sspot3/app/models/group.php on line 124
I also tried using App::import:
App::import('Model','GroupRequest');
I don't get any SQL errors importing the Model this way, but it still doesn't work. I get the following error on the save() or create() call:
Fatal error: Call to a member function save() on a non-object in /home/wxworksmat/sspot3/app/models/group.php on line 124
You are confusing controller and model methods
$this->loadModel()
is a controller method and can only be used there.
You should always use
$this->ModelName = ClassRegistry::init('ModelName');
everywhere else
I might be wrong, and please excuse me if I'm wrong, but it looks like you don't understand the concept of the framework very well. It is difficult to answer your question without giving you a complete tutorial.
This said, everything relies on model associations. If it's done correctly, things are getting easy. You should read:
Associations: Linking Models Together
Once you have your models correctly linked, you will be able to save the primary model, as well as the related model, very easily.
Saving Related Model Data (hasOne, hasMany, belongsTo)
As I understand, you are trying to do this from inside a model?
class GroupRequest extends AppModel {
public function associate($user, $group) {
$data["GroupRequest"] = array("user_id" => $user, "group_id" => $group);
$this->save($data);
}
}
Then in your Controller (assuming group_requests_controller)
$this->GroupRequest->associate($user, $group);
If you're calling this from another controller you would loadModel first
$this->loadModel("GroupRequests");
$this->GroupRequest->associate($user, $group);
However, if you're doing all of this from within GroupRequests controller you should be able to save directly, without making a separate method for it
public function add() {
$this->GroupRequest->create();
$this->GroupRequest->save($this->data); #for < 2.0
}
Your view should be something like
<?php
echo $this->Form->create("GroupRequest");
echo $this->Form->input("user_id");
echo $this->Form->input("group_id");
echo $this->Form->end("Submit");
?>
The problem I had was that I didn't have the correct model association declarations at the top of my model.
Now I have:
group.php
var $hasMany = 'GroupRequest';
group_request.php
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Group' => array(
'className' => 'Group',
'foreignKey' => 'group_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
public function new_request($user, $group) {
$data["GroupRequest"] = array("user_id" => $user, "group_id" => $group, 'status' => 'pending');
if($this->save($data)){ return true;} else {return false;}
}
Now because everything is set up CORRECTLY... I can do this in my group.php model:
$this->GroupRequest->new_request($uid,$gid)
As an added bonus, because the assocations are populating properly, when I do $this->find in my group or user model, now all the related GroupRequest entries show up. Bonus data FTW.
I have a serious deadlock about my project. I am looking for a solution for days but there is nothing.
My index page have 4 different mysql queries. I tried to code this at first inside 1 controller and 1 view. It was not the right way. Then somebody suggested using 4 elements with requestAction() function. It was very good at first. Then I needed mysql between command. I could not found a way for it to use with requestAction(). My question on Cakephp group still unanswered. Somebody suggested using actions in controller. But I couldn't figure it out how to create that controller.
Please tell me what you know about it. Thanks in advance,
Here is my current post controller's index function:
function index() {
$posts = $this->paginate();
if (isset($this->params['requested'])) {
return $posts;
} else {
$sql = array( 'conditions' => array( 'id BETWEEN ? AND ?' => array( 286, 291 ) ) );
$this->Post->find('all', $sql);
$this->set('posts', $posts);
}
}
What should I do? Should I add a new controller for rest 3 actions? Or should I do something inside index() function?
Every time I need several queries in the single controller I do this way:
// model Foo
function edit($id){
$this->data = $this->Foo->read(null, $id);
$second = $this->Foo->Foo2->find('list');
$third = $this->Foo->Foo3->find('list');
$this->set(compact('second', 'third'));
}
I guess, you want to paginate on those 3 columns so the example above is not good for that.
I think you have an error in your method. $posts is not related to your find. There should be $posts = $this->Post->find('all', $sql);. But this would not allow you to paginate on the result. Take a look at Custom Query Pagination in the manual. Maybe this would work for you:
function index(){
if(!isset($this->params['requested'])) {
$this->paginate = array('Post' => array(
'conditions' => array('id BETWEEN ? AND ?' => array(286, 291))
));
}
$this->set('posts', $this->paginate());
}