CakePHP 3: Properly writing functions for the Entity Model - cakephp

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();

Related

cakephp limit results find('all')

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());
}

Running find('all') or find('first') on identical set of conditions in Cake 2

In a controller/index action in my CakePHP2 app, I have
$this->Model-find('all',array( ... lots conditions and things... ));
Then, in controller/view I have:
$this->Model-find('first',array( ... lots conditions and things... ));
Both of these work really well, but I'm obviously repeating a large chunk of code (namely, the find conditions, field list etc) with the only two differences being that in the first one I'm doing find('all') and in the second one I'm doing find('first') and providing the id as one of the conditions.
Is there a way that I can move the conditions into the model and then run different types of find on those conditions?
You can create a method in Youur Model:
public function getConditions($id = null) {
return array( ... lots conditions and things... );
}
somewhere in this method just test if (!empty($id)) to return correct result. (You haven't provide the 'conditions and things' so I can't write the exact code here...
and use it in both controler actions:
$this->Model->find('first', $this->Model->getConditions()));
$this->Model->find('all', $this->Model->getConditions()));
Yes you can move your query into model class as a function/method. see sample bellow for post model:
class Post extends AppModel {
public $name = 'Post';
public function getAll() {
return $this->find('all', array('limit' => 20, 'order' => 'Post.created DESC'));
}
}
and call it on your controller by $this->Model->getAll();
$this->Post->getAll();
You can also add parameter to your function.
Add the conditions as an attribute of your controller
class FoosController extends AppController{
public $conditions = array( ... lots conditions and things... )
public function index(){
$this-Foo->find('all', $this->conditions);
}
public function view(){
$this-Foo->find('first', $this->conditions);
}
}

Cakephp 2.0 + Elements + requestAction = empty result

I'm trying to use Elements in Cakephp 2.0 without luck. I have a model named Post, a controller named Posts and various views. In the layout I would like to include (for every page/view) a box with the most recent news (say 2).
So I created the element dok-posts.ctp
<?php $posts = $this->requestAction('posts/recentnews'); ?>
<?php foreach($posts as $post): ?>
<div class="Post">
....
In my PostsController I added the function recentnews()
public function recentnews(){
$posts = $this->Post->find('all',array('order' => 'Post.created DESC','limit' => 2));
if ($this->request->is('requested')) {
return $posts;
} else {
$this->set('posts', $posts);
}
}
In my layout, default.ctp I call my element
<?php echo $this->element('dok-posts'); ?>
The problem is that I get this message
Invalid argument supplied for foreach() [APP\View\Elements\dok-posts.ctp, line 9]
Debugging in dok-posts.php, right after the $this->requestAction, gives me an empty line. It seems that the recentnews function is not returning anything (debugging in the function returns an array with the posts found). Can anyone please tell me what am I doing wrong?
Since you found out that the action is actually called,
$posts = $this->requestAction('posts/recentnews');
is working correctly. Here, for clarity and extended configuration options (for later changes to the code), I suggest you to use a Router array instead of an URL
$posts = $this -> requestAction(array(
'controller' => 'posts',
'action' => 'recentnews'
));
Now to your actual problem...
Since you say, it always goes into the else branch,
$this->request->is('requested')
might not work as expected. Try this (it works perfect for me):
if (!empty($this -> request -> params['requested'])) {
return $posts;
}
In your app controller create beforefilter and getNewsElement function.
public function beforeFilter() {
parent::beforeFilter();
$data = $this->getNewsElement();
$this->set('posts',$data);
}
function getNewsElement(){
# Put Post model in uses
$posts = $this->Post->find('all',array('order' => 'Post.created DESC','limit' => 2));
return $posts;
}
dok-posts.ctp
#Remove <?php $posts = $this->requestAction('posts/recentnews'); ?>
<?php foreach($posts as $post): ?>
<div class="Post">
....
In PostsController
public function recentnews(){
$posts = $this->getNewsElement();
$this->set('posts', $posts);
}
This will solve your problem!
Try
<?php $posts = $this->requestAction('/posts/recentnews'); ?>
(note the leading forward slash)
I have followed the example in the Cakephp's guide. From the moment it does not passes the if statement it seems that there is a problem with the control.
$this->request->is('requested')
So I removed the
is->('requested')
adding
->params['requested']
and it works.
Thank you all for your help (especially wnstnsmth for the solution).
Try this
<?php $this->requestAction('/controllerName/functionName');?>

CakePHP - access to associated model in beforeSave

In my app Quotes belongTo a product, which in turn belongs to a material. As I can't get the product model afterFind array to include the material when it is accessed from the Quote model I have associated the Quote directly with a material.
The problem I'm having now is that the material_id for the quote needs to be automatically saved based on the product which is selected for the quote
i.e. pulling the value of Product.material_id from the selected product and saving it to the Quote.material_id field automatically before the Quote has been saved to the database.
I'm quite new to cakePHP. Does anyone know how this can be done?
EDIT:
Here is an example to help explain. In my Quote model i can have:
public function beforeSave($options) {
$this->data['Quote']['material_id'] = 4;
return true;
}
but i need to do something more like this which doesn't work:
public function beforeSave($options) {
$this->data['Quote']['material_id'] = $this->Product['material_id'];
return true;
}
I'm shocked this hasn't been properly answered yet....
Oldskool's response is semi-correct, but not entirely right. The use of "$this->Quote" is incorrect, as the beforeSave function itself resides in the Quote class. I'll explain using an example.
-> We have a model Subscription which belongsTo a SubscriptionsPlan
-> Model SubscriptionsPlan hasMany Suscriptions
To access the SubscriptionsPlan data in a beforeSave function in the Subscription model, you would do the following:
public function beforeSave($options = array()){
$options = array(
'conditions' => array(
'SubscriptionsPlan.subscriptions_plan_id' => $this->data[$this->alias]['subscriptions_plan_id']
)
);
$plan = $this->SubscriptionsPlan->find('first', $options);
//REST OF BEFORE SAVE CODE GOES HERE
return true;
}
It should probably work by using a find instead.
public function beforeSave($options) {
// Assuming your Product model is associated with your Quote model
$product = $this->Quote->Product->find('first', array(
'conditions' => array(
'Product.material_id' => $this->data['Quote']['material_id']
)
));
$this->data['Quote']['material_id'] = $product['material_id'];
return true;
}

How to use one controller for more than one sql query in Cakephp?

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());
}

Resources