I have two issues with my CakePHP application (its my first one in CakePHP). I am trying to convert an old php website to cake.
1.Issue
I have my controller that accepts a parameter $id, but the data is comming from joined tables so in the cookbook it had something like this
MY Controller
dish_categories_controller.php
class DishCategoriesController extends AppController {
var $uses = array("DishCategory");
var $hasOne ='';
function get_categories($id)
{
$this->set('dishes',$this->DishCategory->find());
$this->layout = 'master_layout';
}
}
model
dish_category.php
class DishCategory extends AppModel{
var $name = 'DishCategory';
var $hasOne = array(
'Dish' => array(
'className' => 'Dish',
'conditions' => array('Dish.id' => '1'),
'dependent' => true
)
);
}
As you can see the Dish.id=> '1' is hard coded, how can make it dynamic there so that I pass a value and make it something like Dish.if =>$id ?.
So that was my first issue.
The second issue is related to the view
That model returns only one record, how can I make it so that it returns all and also how would I be able to loop through that, below the code in the view currently and the array format.
This is in my view
<?php
echo $dishes['DishCategory']['category_info'];
echo $dishes['DishCategory']['category_title'];
echo $dishes['Dish']['dish_name'];
echo $dishes['Dish']['dish_image'];
echo $this->Html->image($dishes['Dish']['dish_image'], array('alt' => 'CakePHP'))
?>
Array Format
Array ( [DishCategory] => Array
( [id] => 1 [category_name] => Appetizers
[category_keywords] => appetizer, appetizers
[category_title] => Our Side Dishes
[category_info] => Test Test
[dish_id] => 1 )
[Dish] => Array ( [id] => 1
[dish_name] => Rice
[dish_disc] => The Best flavor ever
[dish_price] => 2.90 [dish_image] => /img/rice_chicken.jpeg [dish_category_id] => 1
[dish_price_label] => Delicious Arepa ) )
I would appreciate your help to help me understand how to better do this. Thank you.
Firstly, you DishCategoriesController has model properties, you can remove them. In your controller, you will set up the conditions for the find like so:
class DishCategoriesController extends AppController {
function get_categories($id)
{
// find category with a dish of $id
$this->set('dishes', $this->DishCategory->find('all', array(
'conditions' => array(
'Dish.id' => $id
)
)));
// set master layout
$this->layout = 'master_layout';
}
}
Your DishCategory model will look very basic, you don't need to hard code the relationship condition:
class DishCategory extends AppModel {
/**
* hasOne associations
*
* #var array
*/
public $hasOne = array(
'Dish' => array(
'className' => 'Dish',
'foreignKey' => 'dish_category_id'
)
)
}
At this point it is worth noting that since the DishCategory hasOne Dish, using the above find query, it will only ever return a single result. But, it you were returning multiple results, you could loop through them in your view like so:
<?php foreach ($dishes as $key => $dish): ?>
<?php var_dump($dish) ?>
<?php endforeach ?>
Related
I have the following code setup (snipped for brevity)
class BasePackage extends AppModel {
public $name = 'BasePackage';
public $hasAndBelongsToMany = array('ProductSubtype', 'ProductType');
}
class ProductType extends AppModel {
public $name = 'ProductType';
}
class ProductSubtype extends AppModel {
public $name = 'ProductSubtype';
}
Above are the simple Model classes.
/* tables in database */
base_packages
product_types
product_subtypes
base_packages_product_types
base_packages_product_subtypes
The first table is the main package that users are creating with the form, the product_* tables are pre-loaded with appropriate types and subtypes (they don't change very often), the last two are the Join tables that CakePhp wants to have
/* in BasePackage/add.ctp */
// ...
<ul class="nwblock">
<li>
<?php
echo $this->Form->input('ProductType.product_type_id', array(
'label' => 'Choose Product Type',
'type' => 'select',
'class' => 'form-control',
'style' => 'width:300px; margin-bottom:20px;',
'options' => $protypes
));
?>
</li>
</ul>
<ul class="nwblock">
<li>
<?php
echo $this->Form->input('ProductSubtype.product_subtype_id', array(
'label' => 'Choose Subtype(s)',
'multiple' => 'multiple',
'type' => 'select',
'class' => 'form-control',
'style' => 'width:300px;height:390px;margin-bottom:20px;',
'options' => $subtypes
));
?>
</li>
</ul>
// ...
Above we see the two controls that are loaded from the product_* tables. The types are a single select dropdown and the subtypes are a multiple select list.
/* in BasePackageController.php */
public function add() {
$protypes = $this->BasePackage->ProductType->find('list',
array('fields' => array('ProductType.id', 'ProductType.display')));
$subtypes = $this->BasePackage->ProductSubtype->find('list',
array('fields' => array('ProductSubtype.id', 'ProductSubtype.display')));
$this->set('protypes', $protypes);
$this->set('subtypes', $subtypes);
if ($this->request->is('post')) {
$this->BasePackage->create();
if (!empty($this->request->data)) {
$this->BasePackage->saveAll($this->request->data, array('deep' => true));
}
}
}
The process is as follows, while the user creates a new BasePackage, they select a ProductType from a dropdown box and one to many ProductSubtypes from a multiple select list. When the $this->BasePackage->saveAll() call is made, the data to be inserted into base_packages and base_packages_product_types tables is inserted correctly. However, the base_packages_product_subtypes table remains untouched.
UPDATE:
If I remove the 'multiple' => 'multiple', from the form->input options, the code saves both the producttype and the productsubtype (as expected). This is obviously not sufficient, as I need to save 1-to-many. Anyone know how to activate the 'Many' part of the HABTM?
To me BasePackage <> ProductType looks more like it should be a many-to-one relation, ie BasePackage belongsTo ProductType?
Anyways... please follow the conventions as described in the Cookbook:
http://book.cakephp.org/2.0/en/models/saving-your-data.html#saving-related-model-data-habtm
The form helper should be fed with the model name, ie ProductSubtype, and the view var should be camel backed plural, ie productSubtypes, that way CakePHP will do the rest for you automatically.
public function add() {
// ...
$this->set('productSubtypes', $subtypes);
// ...
}
echo $this->Form->input('ProductSubtype', array(
'label' => 'Choose Subtype(s)',
'class' => 'form-control',
'style' => 'width:300px;height:390px;margin-bottom:20px;'
));
Can you try with BasePackage->saveAssociated ?
http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array
Consider this code:
Controller Code
<?php
App::uses('AppController', 'Controller');
class UsersController extends AppController {
public $components = array(
'Security',
'Session'
);
public function example() {
if ($this->request->is('post')) {
$this->set('some_var', true);
}
}
}
View Code
<?php
echo $this->Form->create();
echo $this->Form->input('name');
echo $this->Form->end('Submit');
Since I have the Security component in place, tampering with the form in any way (such as adding a field to it) will cause the request to be black-holed. I'd like to test this:
Test Code
<?php
class UsersControllerTest extends ControllerTestCase {
public function testExamplePostValidData() {
$this->Controller = $this->generate('Users', array(
'components' => array(
'Security'
)
));
$data = array(
'User' => array(
'name' => 'John Doe'
)
);
$this->testAction('/users/example', array('data' => $data, 'method' => 'post'));
$this->assertTrue($this->vars['some_var']);
}
public function testExamplePostInvalidData() {
$this->Controller = $this->generate('Users', array(
'components' => array(
'Security'
)
));
$data = array(
'User' => array(
'name' => 'John Doe',
'some_field' => 'The existence of this should cause the request to be black-holed.'
)
);
$this->testAction('/users/example', array('data' => $data, 'method' => 'post'));
$this->assertTrue($this->vars['some_var']);
}
}
The second test testExamplePostInvalidData should fail because of some_field being in the $data array, but it passes! What am I doing wrong?
By adding the 'some_field' in the data of ->testAction, the security component will assume that field is part of your app (since it's coming from your code, not a POST array) so it won't be seen as a "hack attempt".
Checking for blackholes is a little more convoluted. But Cake core tests already test the blackhole functionality, so if those tests pass, you don't need to check it in your app.
If you insist though, check out the core Cake tests for guidance:
Specifically:
/**
* test that validatePost fails if any of its required fields are missing.
*
* #return void
*/
public function testValidatePostFormHacking() {
$this->Controller->Security->startup($this->Controller);
$key = $this->Controller->params['_Token']['key'];
$unlocked = '';
$this->Controller->request->data = array(
'Model' => array('username' => 'nate', 'password' => 'foo', 'valid' => '0'),
'_Token' => compact('key', 'unlocked')
);
$result = $this->Controller->Security->validatePost($this->Controller);
$this->assertFalse($result, 'validatePost passed when fields were missing. %s');
}
Lots more examples in the file:
https://github.com/cakephp/cakephp/blob/master/lib/Cake/Test/Case/Controller/Component/SecurityComponentTest.php
First of all, sorry if this sounds too simple. I'm new to cakePHP by the way.
My objective is to expand the team_id in season controller where it linked thru standings table.
My current model rule are:
Each season has many standings
Each standings denotes a team.
Basically is a many to many relation between season and team = standings.
Here's my SeasonController:
class SeasonsController extends AppController
{
public $helpers = array('Html', 'Form');
public function view($id)
{
$this->Season->id = $id;
$this->set('season', $this->Season->read());
}
}
When I browse a single season thru /cakephp/seasons/view/1, the $season array shows:
Array
(
[Season] => Array
(
[id] => 1
[name] => English Premier League 2013/2014
[start] => 1350379014
[end] => 0
)
[Standing] => Array
(
[0] => Array
(
[id] => 1
[team_id] => 1
[win] => 1
[lose] => 0
[draw] => 1
[GF] => 5
[GA] => 4
[season_id] => 1
[rank] => 1
)
[1] => Array
(
[id] => 2
[team_id] => 2
[win] => 0
[lose] => 1
[draw] => 1
[GF] => 4
[GA] => 5
[season_id] => 1
[rank] => 2
)
)
)
The result only shows team_id, but how can I get further expansion for team_id?
I have put below in my Team's model, but still doesn't auto linked. Below are all my models:
class Team extends AppModel
{
public $name = 'Team';
public $hasMany = array(
'Standing' => array(
'className' => 'Standing'
));
}
class Season extends AppModel
{
public $name = 'Season';
public $hasMany = array(
'Standing' => array(
'className' => 'Standing'
));
}
class Standing extends AppModel
{
public $name = 'Standing';
public $belongsTo = array(
'Team' => array(
'className' => 'Team',
'foreignKey' => 'team_id',
),
'Season' => array(
'className' => 'Season',
'foreignKey' => 'season_id',
),
);
}
TLDR:
Use CakePHP's [Containable] behavior.
Explanation & Code example:
"Containable" lets you "contain" any associated data you'd like. Instead of "recursive", which blindly pulls entire levels of data (and can often cause memory errors), "Containable" allows you to specify EXACTLY what data you want to retrieve even down to the field(s) if you'd like.
Basically, you set your model to public $actsAs = array('Containable'); (I suggest doing that in your AppModel so Containable is available for all models). Then, you pass a "contain" array/nested array of the associated models you want to retrieve (see below example).
After reading about it and following the instructions, your code should look similar to this:
public function view($id = null)
{
if(empty($id)) $this->redirect('/');
$season = $this->Season->find('first', array(
'conditions' => array(
'Season.id' => $id
),
'contain' => array(
'Standing' => array(
'Team'
)
)
);
$this->set(compact('season'));
}
Once you get more comfortable with CakePHP, you should really move functions like this into the model instead of having them in the controller (search for "CakePHP Fat Models Skinny Controllers"), but for now, just get it working. :)
Use :
$this->Season->id = $id;
$this->Season->recursive = 2;
$this->set('season', $this->Season->read());
Model
<?php
class Tonguetwister extends AppModel {
var $name = 'Tonguetwister';
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'language' => array(
'className' => 'language',
'foreignKey' => 'language_alias',
'dependent'=> true
)
);
}
?>
Controller
<?php
class TonguetwistersController extends AppController {
var $name = 'Tonguetwisters';
var $uses = array('Tonguetwister', 'Language');
function index() {
$this->set('languages', $this->Language->find('all'));
}
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid tonguetwister', true));
$this->redirect(array('action' => 'index'));
}
$this->set('tonguetwisters', $this->Tonguetwister->find('all', array('conditions' => array('language_alias' => $id))));
}
}
?>
I only want to see languages on index() that have tongue twisters. How can I do this?
There might be a more efficient way, but here's how to pick only unique languages from the Tonguetwister table:
function index() {
$languageList = $this->Tonguetwister->find(
'list',
array(
'fields' => array( 'language_alias', 'language_alias' ),
'group' => 'Tonguetwister.language_alias',
'recursive' => -1
)
);
// $languageList is now an array that holds the language ids
$this->set(
'languages',
$this->Tonguetwister->Language->find(
'all',
array(
'conditions' => array(
'Language.id' => $languageList
)
)
)
);
}
By the way, you don't need to put Language into $uses. Since they have a relation set you can access the Language model with $this->Tonguetwister->Language.
You don't really need to do two SQL queries for this. If the tables are joined on "language_alias" you can do something like this:
function index() {
$this->Language->recursive = 0;
$this->set('languages', $this->Language->find('all', array(
'conditions' => array($this->Language->alias.'.language_alias' => $this->Tonguetwister->alias.'.language_alias')
));
}
You should just do one query that's going to join the tables properly.
I want to model the following simple relationship:
One Passenger belongs to a Car; One Car has many Passengers.
The passenger table has an id and Car_id column, the Car table has one id column.
My models look like this:
<?php
class Passenger extends AppModel {
var $name = 'Passenger';
var $belongsTo = 'Car';
} ?>
and
<?php
class Car extends AppModel {
var $name = 'Car';
var $hasMany = array (
'Passenger' => array (
'className' => 'Passenger',
'foreignKey' => 'car_id'
)
);
}
?>
and my add Passenger .ctp looks like this:
<?php
echo $this->Form->create('Passenger');
echo $this->Form->input('car_id');
echo $this->Form->end('Save');
?>
BUt when I access the page to add a passenger, all I see is an empty drop down box. Is there an additional step I must take in order to populate the dropbox with all cars?
First off, you have forgotten to mention the belongsTo relation in your Passenger model:
<?php
class Passenger extends AppModel {
var $name = 'Passenger';
var $belongsTo = array('Car');
}
?>
Next, in the corresponding action of your controller, you will need to obtain a list of all the cars from the database, and set it to the plural form of the model's variable ($cars). You would do that like so:
$cars = $this->Passenger->Car->find('list');
$this->set(compact('cars'));
This will convert the car_id input field into a drop down list with the populated information.
HTH.
The Passenger will only know about the car with which it is associated - at this point, none.
In the add method in the passenger controller, do
$this->Car->find('list');
and pass the result into your view:
$this->set('cars',$cars);
In the view, give the $cars variable as the value for $options in the field declaration:
echo $this->Form->input('car_id', array('options' => $cars));
Alternatively, you can do something like:
echo $this->Form->input('Car.id', array('options' => $cars));
$this->CompanyCashback->bindModel(array('belongsTo' => array(
'CompanyBranch' => array('className' => 'CompanyBranch', 'foreignKey' => false, 'conditions' => array('CompanyCashback.publisher_id = CompanyBranch.publisher_id && CompanyBranch.branch_type = "online" ')),
'PersonalInformation' => array('className' => 'PersonalInformation', 'foreignKey' => false, 'conditions' => array('CompanyCashback.publisher_id = PersonalInformation.user_id')),
'Country' => array('className' => 'Country', 'foreignKey' => false, 'conditions' => array('PersonalInformation.country_id = Country.id')),
'User' => array('className' => 'User', 'foreignKey' => false, 'conditions' => array('PersonalInformation.user_id = User.id')))
));