Get the total of a Project from related project items in CakePHP - php

In CakePHP i've a model Project and a model ProjectRecord:
<?php
class Project extends AppModel {
public $useTable = 'project';
public $hasOne = array(
'Customer' => array(
'foreignKey' => false,
'conditions' => array('Project.customer_id = Customer.id')
)
);
public $hasMany = array(
'ProjectRecord' => array(
'className' => 'ProjectRecord',
)
);
}
?>
Model ProjectRecord:
<?php
App::uses('AppModel', 'Model');
class ProjectRecord extends AppModel {
public $useTable = 'project_record';
public $hasOne = 'Project';
}
?>
ProjectRecord holds the values: "amount", "costs" and "vat" (in percent)
In my projects index I want to see the total of a Project. So I've to collect all ProjectRecord that are connected with one Project, then multiply the amount with costs and vat, and then count all those numbers for one value.
I know it has something to do with virtual fields, but I can't get it right to retrieve those ProjectRecords.
I've tried to add a Helper, but no matter what I did, I got the warning that the table (Project) couldn't get loaded.

I would not calculate this value each time I access a product record. I think the better solution would be to calculate the values in the ProjectRecord::afterSave() callback and save the sum to the associated Project (add a field like total_sum or whatever you prefer) of the ProjectRecord that was just saved. This way you avoid additional complexity or additional queries.

Related

Working out associations in CakePHP

I am trying to learn CakePHP, and attempting to replicate what I can do with standard PHP and MySQL with Cake.
I have an event table with event information, and a list_items table that holds entries for a bullet pointed list to be displayed on each event page. I also have a venue table that lists the venues (id, event_id and name) and finally an instance table which joins the event and venue together - this consists of id, event_id, venue_id, and date.
I have associated the tables how I think they should be:
class Event extends AppModel{
public $hasMany = array(
'ListItem',
'Instance'
);
}
bullet list items:
class ListItem extends AppModel{
public $belongsTo = array(
'Event'
);
}
instances:
class Instance extends AppModel{
public $belongsTo = array(
'Event'
);
public $hasOne = array(
'Venue'
);
}
and venue:
class Venue extends AppModel{
public $belongsTo = array(
'Instance'
);
and the Event controller has the query to find an event based on the ID:
public function view($id = null){
if (!$id){
throw new NotFoundException(__('Invalid Course'));
}
$event = $this->Event->findById($id);
if (!$event){
throw new NotFoundException(__('Invalid event'));
}
$this->set('event',$event);
}
Are these associations correct? I want to be able to display the instance date alongside the venue - at the moment I can output the date no problem, but the venue eludes me.
Probably it's because the recursiveness is not deep enough. To change that, you can do the following:
The easiest but poorest way, is to change the recursiveness before the find:
$this->Event->recursive = 2; //change it according to the deepness you want
This only will retrieve all the relationships according to the deepness assigned, but you can't exclude relations you will not need.
Or, the more advanced but richer way, add the Containable behavior and create the recursiveness by yourself. At the beginning of your model add the following behavior:
public $actsAs = array('Containable');
and in your find function:
$event = $this->Event->find('first', array(
'conditions' => array('id' => $id),
'contain' => array(
'ListItem',
'Instance' => array(
'Venue'
)
)
));
The advantage of Contain over recursive is that you can control every aspect of the relationships on find queries, so you can retrieve only the data you need. So it's highly recommended that you use Containable behavior
Please see the documentation for further info: http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html

Order data based on count of related table data

I have Two diff tables as given below:
users and posts
Need data from user's table order by count of posts table
Relationship is defined as:
User Model:
public $hasMany = array('Post');
Post Model
Public $belongsTo = array('User');
counterCache - Cache your count()
This function helps you cache the count of related data. Instead of counting the records manually via find('count'), the model itself tracks any addition/deleting towards the associated $hasMany model and increases/decreases a dedicated integer field within the parent model table.
The name of the field consists of the singular model name followed by a underscore and the word “count”:
my_model_count
Let’s say you have a model called ImageComment and a model called Image, you would add a new INT-field to the image table and name it image_comment_count.
Once you have added the counter field you are good to go. Activate counter-cache in your association by adding a counterCache key and set the value to true:
<?php
class Image extends AppModel {
public $belongsTo = array(
'ImageAlbum' => array('counterCache' => true)
);
}
From now on, every time you add or remove a Image associated to ImageAlbum, the number within image_count is adjusted automatically.
You can also specify counterScope. It allows you to specify a simple condition which tells the model when to update (or when not to, depending on how you look at it) the counter value.
Using our Image model example, we can specify it like so:
<?php
class Image extends AppModel {
public $belongsTo = array(
'ImageAlbum' => array(
'counterCache' => true,
'counterScope' => array('Image.active' => 1) // only count if "Image" is active = 1
));
}

How to display differnt field other than id when using Associations and baking cake

I'm using Associations to Link Models together in CakePHP.
I have a Player model that is linked to a Statistic model using a $hasMany relationship. So basically one player can have many statistics.
After I set up these relationships, I use cake bake to generate the controllers and views for both the Player and Statistic models.
Everything works fine and works as expected, but when I go to add a statistic using the add template, the player_id is used as the association.
My question is how do I set up the model to use the player_id as the association but use something like the player's first and last name as the drop down menu in the add template.
Currently if I go to add a new statistic, I get a drop down box that called "Player" that lists all of the player id's but what I want is for the player's first_name and last_name to be in that drop down box instead of the id. I realize that I can modify the controller or template to accomplish this, but I Want to know if I can do this while setting up the model so that cake bake can take care of it.
Player Model:
<?php
class Player extends AppModel {
public $name = 'Player';
public $belongsTo = array(
'School' => array(
'className' => 'School',
'foreignKey' => 'school_id'
)
);
public $hasMany = 'Statistic';
}
Statistic Model:
<?php
class Statistic extends AppModel {
public $name = 'Statistic';
public $belongsTo = array(
'Player' => array(
'className' => 'Player',
'foreignKey' => 'player_id'
)
);
}
Players Table
`id|school_id|first_name|last_name|number|position
Cake uses the model's displayField attribute when choosing the default columns for a list. If none is defined for the model it will look for name or title. So in your model you can use:
public $displayField = 'first_name';
This will display the player's first name in the list.
If you want the display field to be the concatenation of two fields, you can use a virtual field in the model like so:
public $virtualFields = array(
'name' => "TRIM(CONCAT(Player.first_name, ' ', Player.last_name))"
);
Note the above works for Mysql. For another kind of database you will need to adjust the syntax. For example, for Sqlite it would be:
TRIM(Player.first_name || ' ' || Player.last_name)
Then add in your model:
public $displayField = 'name';

Show a list of records to have a join to another table using CakePHP

I have an application that stores Posts and Topics and joins them using a Topic_Posts table.
The associations for the application are as follows:
Post.php
class Post extends AppModel
{
public $name = 'Post';
public $belongsTo = 'User';
public $hasMany = array('Answer');
// Has many topics that belong to topic post join table... jazz
public $hasAndBelongsToMany = array(
'Topic' => array('with' => 'TopicPost')
);
}
Topic.php
class Topic extends AppModel
{
public $hasMany = array(
'TopicPost'
);
}
TopicPost.php
class TopicPost extends AppModel {
public $belongsTo = array(
'Topic', 'Post'
);
}
When a user views a topic e.g. /topics/view/topicname I want to show all the posts that contain that Topic.
So far I have the following method in my TopicsController for the view:
public function view ( $slug )
{
$topic = $this->Topic->find('first', array('conditions'=>array('Topic.slug'=>$slug)));
$this->set('topic', $topic);
$this->set('title_for_layout', $topic['Topic']['title'] . ' – Topics');
$this->paginate = array
(
'Post' => array
(
'limit'=>15,
'conditions'=>array
(
'Post.status'=>array(1,2),
'TopicPost.topic_id' => $topic['Topic']['id'],
),
'order'=>array('Post.datetime'=>'desc'),
'contain'=>array('User'=>'Profile', 'TopicPost')
)
);
$posts = $this->paginate('Post'); // this one
$this->set('posts', $posts);
}
And so that I can use Posts and TopicPosts I have added: public $uses = array('Topic','TopicPost','Post'); to the top of the controller and made all models act as containable.
So basically I need to find Posts that have a match in the database model TopicPosts for the id of the topic I'm viewing.
I just couldn't get it to work the "proper" way. I'm not sure if this is a bug in cake or something, but the paginate function simply refuses to budge.. The proper way to do this would probably be to write your own paginate function in your Post model, there is some info on how to do that in the cookbook.
Meanwhile, I offer you the workaround below. It's not optimal (at least not without caching) but it works. You can do it the proper way when/if you run into performance problem, but until then, this code below should do it.
public function view ( $slug )
{
$topic = $this->Topic->find('first', array('conditions'=>array('Topic.slug'=>$slug)));
$this->set('topic', $topic);
$this->set('title_for_layout', $topic['Topic']['title'] . ' – Topics');
// step 1: get post IDs related to your topic
$postIDs = $this->Topic->TopicPost->find
(
'list',
array
(
'fields' => array('TopicPost.post_id'),
'conditions' => array('TopicPost.topic_id' => $topic['Topic']['id'])
)
);
$this->paginate = array
(
'Post' => array
(
'limit'=>15,
'conditions'=>array
(
'Post.status' => array(1,2),
// step 2: include them in your paginate conditions
'Post.id' => $postIDs,
),
'order' => array('Post.datetime'=>'desc'),
)
);
$posts = $this->paginate('Post');
$this->set('posts', $posts);
}
(Please note that I've stripped some of the stuff in my tests as I didn't have some of the stuff in your app, so don't forget to put it back)
I think you have a problem with your model relationships, I don't understand why you have a HABTM relationship on the posts model, when you are actually emulating this (using the Has-many-through method) on the TopicPost model itself. If you don't want to use the HABTM behaviour built into cake (and I don't blame you), you should setup relationships like this:
class PostTopic extends AppModel { // note PostTopic, name should be alphabetical
public $belongsTo = array('Post', 'Topic');
}
class Post extends AppModel {
public $hasMany = array('PostTopic');
}
class Topic extends AppModel {
public $hasMany = array('PostTopic');
}
Then to fetch the IDs of posts or topics that relate, you simply load the PostTopic class and do a search:
// ie in Post Controller
$this->paginate($this->Post->PostTopic, array('PostTopic.topic_id' => $post['PostTopic']['topic_id']));
I have a similar setup on my site, where users can add a product to their inventory, It's kind of a HABTM relatipnship but with more data attached to it. See the Product, Inventory and Users models here for a more complicated example.
From question comment:
Also just to note the DB structure and relationships work fine as I
can pull the topics for a post fine using TopicPost so I KNOW that
works, it's just getting Posts for a topic that seems to be not
working...
Right, because you setup the HABTM for Post->Topic, but not for Topic->Post. The 2.0 book defines an example just like yours (but using recipes and ingredients) to describe the use case.
The main difference between hasMany and HABTM is that a link between
models in HABTM is not exclusive. For example, we’re about to join up
our Recipe model with an Ingredient model using HABTM. Using tomatoes
as an Ingredient for my grandma’s spaghetti recipe doesn’t “use up”
the ingredient. I can also use it for a salad Recipe.
The same can be said for a Post using a Topic (your name for a tag/category). And just like their example:
Remember to define a HABTM association in the Ingredient model if
you’d like to fetch Recipe data when using the Ingredient model.
Or, in your case, the Topic model.
So here's the models that you should use:
class Post extends AppModel
{
public $name = 'Post';
public $belongsTo = 'User';
public $hasMany = array('Answer');
// Has many topics that belong to topic post join table... jazz
public $hasAndBelongsToMany = array(
'Topic' => array('with' => 'TopicPost')
);
}
class Topic extends AppModel
{
// Has many posts that belong to topic post join table... jazz
public $hasAndBelongsToMany = array(
'Post' => array('with' => 'TopicPost')
);
}
Now, you don't really need TopicPost unless you are doing something special, but it is fine as it is. And, though you can use that model for adding Topics to to a post (or vice-versa), I'd suggest you use the build in method of handling HABTM saves. Review this page for details on saving HABTM.
If you decide to remove it, make sure you define the join tables and conditions for the HABTM options in each model per the docs

CakePHP HATBM not working

I'm starting a cakephp app, I've never used it in real world so I'm a bit confused how HABTM works, even though I read the documentation I couldn't get even the $this->User->Subscription and didn't see any extra object dumped
What I want is to create a HATBM between users and subscriptions
so I created three tables (users,subscriptions,users_subscribers)
Then in my User.php model I did this
var $hasAndBelongsToMany =
array(
'Subscription' =>
array('className'=>'Subscription',
'joinTable' => 'users_subscribers',
'foreignKey' => 'user_id',
'associationForeignKey' => 'subscription_id',
'unique' => true,
)
);
SUbscription.php
var $hasAndBelongsToMany = array(
'User'=>array('className'=>'User',
'joinTable' => 'users_subscribers',
'foreignKey' => 'subscription_id',
'associationForeignKey' => 'user_id',
'unique' => true));
Even with the tags example and following it, I cannot get the relation set, I also added the line <?php echo $this->element('sql_dump'); ?> to see if its running which it isn't...
Could anyone guide me how exactly you get HATBM to work, what else do I need to verify?
Full code:
pages_controller.php
http://sao.pastebin.com/PWQMhE2z
User model:
http://sao.pastebin.com/PWqwAj1v
Subscription model:
http://sao.pastebin.com/MfVFR4Kw
subscriptions SCHEMA:
http://sao.pastebin.com/mLRcEp1c
User SCHEMA:
http://sao.pastebin.com/UeTRHh3u
users_subscriptions SCHEMA:
http://sao.pastebin.com/4UeSDZte
The simplest and fastest way to get this working is by following CakePHP's rule of configuration over customization.
This means following the CakePHP conventions unless you have a very good reason not to.
I'd strongly recommend starting with a basic setup that you know works, and then modifying that if you need to. Here's a quick and easy way to get up and running.
The Database Tables
Start with three database tables: users, subscriptions and subscriptions_users. The schemas you already have are ok, but I'd make a couple modifications to make sure things go smoothly:
Add a name or title column to the users table. Either that, or you'll have to add the $displayField property to your User model. If you don't do this you'll miss out on some of the "automagic" that CakePHP provides. More info on $displayField
Change the join table's name to subscriptions_users. This is the CakePHP convention and there's no reason not to save yourself the time and worry of following it. :-)
Use the following schema for the join table:
CREATE TABLE subscriptions_users (
subscription_id int(11) NOT NULL,
user_id int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Note that there aren't any keys defined. From the CakePHP manual: "To avoid any issues - don't define a combined primary key for these two fields, if your application requires it you can define a unique index."
The Models
Try to keep your code clean. There are a lot of sensible defaults implemented in CakePHP and there's no point in defining them when they're already defined.
The following models should work for you:
user.php
<?php
class User extends AppModel {
var $hasAndBelongsToMany = array('Subscription');
}
?>
subscription.php
<?php
class User extends AppModel {
var $hasAndBelongsToMany = array('User');
}
?>
Pretty simple. Just be sure your model files are named correctly: user.php and subscription.php, all lowercase.
Also, note that you don't have to set any of the relationship options (className, joinTable, etc.) unless they need to be something besides the default. Ninety percent of the time the defaults should serve you just fine.
You should be up and running now. You can make sure the model objects are being loaded and are accessible in your controllers like this:
users_controller.php
<?php
class UsersController extends AppController {
var $name = 'Users';
function index() {
$this->autoRender = false;
var_dump(is_object($this->User));
var_dump(is_object($this->User->Subscription));
}
}
?>
subscriptions_controller.php
<?php
class SubscriptionsController extends AppController {
var $name = 'Subscriptions';
function index() {
$this->autoRender = false;
var_dump(is_object($this->Subscription));
var_dump(is_object($this->Subscription->User));
}
}
?>
The output of /users and /subscriptions should both be bool(true) bool(true).
You can see the full models by doing pr($this->User);.
Deleting records
If you delete a single record using, for example, $this->User->delete($user_id), all the records in the join table with that user ID will automatically be deleted as well.
If you want to delete a single record from a HABTM join table, without deleting the records that it links to, in effect, "unjoining" the two records, you can do it through the SubscriptionsUser model. This is a model that is created on the fly whenever there's a HABTM relationship.
See here for an example: CakePHP hasAndBelongsToMany (HABTM) Delete Joining Record
I did a test app with a basic schema and I get all the relations right. I suspect your woes have to do with the fact that you did $uses = array('User', 'Subscription');Why don't you try with $uses = $uses = array('User'); and then try
$this->User->find('all');
$this->User->Subscription->find('all');
You also need to define the same HABTM relation in your Subscription.php model. If I recall correctly, CakePHP internally fetches some of the required information from the other side's HABTM configuration.
I always use two "hasMany" relations and one "belongsTo" relation to get the HABTM effect in CakePHP with more control.
Try this:
User model (user.php)
class User extends AppModel {
public $name = 'User';
public $actsAs = array('Containable');
public $hasMany = array('UserSubscription');
}
Subscription model (subscription.php)
class Subscription extends AppModel {
public $name = 'Subscription';
public $actsAs = array('Containable');
public $hasMany = array('UserSubscription');
}
UserSubscription model (user_subscription.php)
class UserSubscription extends AppModel {
public $name = 'UserSubscription';
public $belongsTo = array('User','Subscription');
}
pages/home action (pages_controller.php)
public function home() {
$data = array(
'User' => array(
'id' => 1, 'user_email' => 'allenskd#gmail.com', 'user_password' => 'fdfdkert', 'user_salt' => 'haha', 'user_displayname' => 'David'
),
'UserSubscription' => array(
'user_id' => '1', 'subscription_id' => '1'
),
'Subscription' => array(
'id' => 1, 'title' => 'My first plan', 'price' => '30.00', 'subscriber_count' => '1'
),
);
$this->Subscription->save($data);
$this->User->saveAll($data);
$test = $this->User->find('all', array('contain' => array('UserSubscription' => array('Subscription'))));
pr($test);
}

Categories