Hi I've done a find() and added a new field to some of the results:
$approved = $this->ExpenseClaim->find('all', array('conditions'=> array('ExpenseClaim.claim_status_id' => '3')));
$i = 0;
foreach ($approved as $ap) {
$approved[$i]['ExpenseClaim']['claimTotal'] = $this->ExpenseClaim->expenseClaimTotal($approved[$i]['ExpenseClaim']['id']);
$i++;
}
I now need to pass this to paginate, however I read here that you cannot do this and that I must create another model to use the afterFind() method only on this one particular find.
So I've created the new Model called ExpenseClaimTotal and set the UseTable to
public $useTable = 'expense_claims';
Then in the new models afterFind() method I did a simple debug:
public function afterFind($results, $primary = false) {
debug($results);
//return $results;
}
But when I now try and do a find against this new model in pagesController it fails:
$this->loadModel('ExpenseClaimTotal');
$approved = $this->ExpenseClaimTotal->find('all', array('conditions'=> array('ExpenseClaim.claim_status_id' => '3')));
This is the error I get:
Database Error
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'ExpenseClaim.claim_status_id' in 'where clause'
SQL Query: SELECT `ExpenseClaimTotal`.`id`, `ExpenseClaimTotal`.`user_id`, `ExpenseClaimTotal`.`claim_status_id`, `ExpenseClaimTotal`.`created`, `ExpenseClaimTotal`.`modified`, `ExpenseClaimTotal`.`approved`, `ExpenseClaimTotal`.`approved_by`, `ExpenseClaimTotal`.`declined_by`, `ExpenseClaimTotal`.`date_submitted` FROM `expenses`.`expense_claims` AS `ExpenseClaimTotal` WHERE `ExpenseClaim`.`claim_status_id` = 3
There doesnt seem to be much in the docs about using 2 models for one table
You don't want to paginate an array
You're already performing a find, it's not sensible to perform a find and then paginate the resultant array.
Simply paginate your model data directly and inject your total values in the process. As such - if you put your original "added a new field to some of the results" logic in the model:
class ExpenseClaim extends AppModel {
public function afterFind($results, $primary = false) {
foreach ($results as &$ap) {
if (isset($ap['ExpenseClaim']['id'])) {
$ap['ExpenseClaim']['claimTotal'] = $this->expenseClaimTotal($ap['ExpenseClaim']['id']);
}
}
return $results;
}
}
Your controller code becomes simply:
public function index() {
$conditions = array('ExpenseClaim.claim_status_id' => '3');
$data = $this->paginate($conditions);
$this->set('data', $data);
}
And the code is simple and "just works".
Enhancements
The above is the simplest way to achieve the desired results, but has some disadvantages - namely it will call the total method on pretty much all finds.
Depending on exactly what you're doing you may wish to for example:
Cache your totals
If appropriate, you can remove problems by simply adding the field "claim_total" to the database, and recalculate whenever it changes. That would mean there is absolutely no extra logic when reading from the expense claim model.
Use a custom find type
If you don't want to recaculate the total on all finds - you can create a custom find type
class ExpenseClaim extends AppModel {
public $findMethods = array('allWithTotals' => true);
protected function _findAllWithTotals($state, $query, $results = array()) {
if ($state === 'before') {
return $query;
}
foreach ($results as &$ap) {
$ap['ExpenseClaim']['claimTotal'] = $this->expenseClaimTotal($ap['ExpenseClaim']['id']);
}
return $results;
}
And then use it in your paginate call:
public function index() {
$this->paginate['findType'] = 'allWithTotals'; # <-
$conditions = array('ExpenseClaim.claim_status_id' => '3');
$data = $this->paginate($conditions);
$this->set('data', $data);
}
In this way, only the index method will trigger the call to add the totals.
Related
I have threads and messages on thread
I want to return all threads with the last message time, so I added a new field like this on thread model
public function fields()
{
$fields= ['idThread', 'idUser', 'title', 'unread', 'username','lastMesageTime'];
return $fields;
}
now with this method I get the calculated value lastMessageTime
public function getLastMessageTime()
{
return $this->hasMany(Messages::className(), ['idThread' => 'idThread'])
->select('time')->orderBy('time DESC')->limit(1)->scalar();
}
on my index method using active record like this
return Thread::find()->select('idThread, title, idUser')->all();
this works and I get lastMessageTime with the right value, but I want to order by so I can get the thread with the most recent lastMessageTime the first one, I tried with the following code
public function scopes() {
return array(
'byOrden' => array('order' => 'lastTimeMessage DESC'),
);
}
any idea?
Edit:
this workaround works, but I think this is not a good way because I'm not using active record so fields like username that I had defined on Thread model I had to fetch it again
$query = (new \yii\db\Query());
$query->select('*, (SELECT max(time) as lastMessageTime from messages where messages.idThread = thread.idThread ) lastMessageTime,
(SELECT name from users where users.idUser = thread.idUser) as name ')
->from('threads')
->where(['idUser'=>$idUser])
->orderBy('lastMessageTime DESC');
$rows = $query->all();
return $rows;
You can define extra fields as model properties, then override find method to load data for them.
class Thread extends \yii\db\ActiveRecord
{
public $lastMessageTime;
public static function find()
{
$q = parent::find()
->select('*')
->addSelect(
new \yii\db\Expression(
'(SELECT max(time) FROM messages WHERE messages.idThread = thread.idThread) AS lastMessageTime'
);
return $q;
}
}
Then you can load and order models like this:
$rows = Thread::find()->orderBy(['lastMessageTime' => SORT_DESC])->all();
I have to help me convert an entity to array but I have issues resolving associated records, which I need.
However, this gives me an error
The class 'Doctrine\ORM\PersistentCollection' was not found in the
chain configured namespaces ...
The code follows:
public function serialize($entityObject)
{
$data = array();
$className = get_class($entityObject);
$metaData = $this->entityManager->getClassMetadata($className);
foreach ($metaData->fieldMappings as $field => $mapping)
{
$method = "get" . ucfirst($field);
$data[$field] = call_user_func(array($entityObject, $method));
}
foreach ($metaData->associationMappings as $field => $mapping)
{
// Sort of entity object
$object = $metaData->reflFields[$field]->getValue($entityObject);
if ($object instanceof ArrayCollection) {
$object = $object->toArray();
}
else {
$data[$field] = $this->serialize($object);
}
}
return $data;
}
How can I resolve the associated fields into their respective arrays.
I have tried using the built-in, and JMS serialiser, but this gives me issues of nestedness limits, so this is not an option for me.
UPDATE:
I have updated the code to handle instance of ArrayCollection as per #ScayTrase's suggestion. However, the error above is still reported with a one-to-many field map. In debug, the variable $object is of type "Doctrine\ORM\PersistentCollection"
For *toMany association properties implemented with ArrayCollection you should call ArrayCollection::toArray() first. Just check it with instanceof before, like this
if ($object instanceof ArrayCollection) {
$object = $object->toArray();
}
I am having one table
menu with field
id and name
1 suriya
2 kamal
3 ram
I want to retrive these value inside foreach loop in below order
1) kamal
2) suriya
3) ram
I am beginner in cake php.
Pretty vague question, but this should get you started. Put this code in your menu's controller.
$menus = $this->Menu->find('all');
foreach ($menus as $menu) {
echo $menu['Menu']['name'];
}
Regarding the order, if you're not ordering alphabetically, then you'll need to add a new column to your table, such as sort_order, and order that when you call $this->Menu->find('all')
create a model named Menu.php in app/models folder -
class Menu extends AppModel {
public $primaryKey = 'id';
public $useTable = 'menu';
}
And create a controller named MenuController.php in app/controller folder -
class MenuController extends AppController {
public $use = false;
public function index () {
$this->loadModel ('Menu');
$data = $this->Menu->find ('all', array ('order' => 'name ASC'));
foreach ($data as $value) {
echo $value['Menu']['name'] . "<br/>";
}
exit;
}
}
Then try it, could work. :)
When using $this->Form->input('fieldname'), CakePHP creates a label element that, by convention, uses the fieldname. I know I can specify another string to use for the label as an argument to the input method, but is there any way I can specify the label name in the model, so I don't have to repeat the label name in multiple ctp files (e.g. so I only have to change it in one place)?
So I created this and made a quick test which worked, however, can't guarantee it's bug free:
<?php
App::uses('FormHelper', 'View/Helper');
class MyFormHelper extends FormHelper {
public function label($fieldName = null, $text = null, $options = array()) {
if ($text === null) {
$entity = $this->entity();
$field = array_pop($entity);
$model = $this->model();
$object = $this->_models[$model];
if (isset($object->labels[$field])) {
$text = $object->labels[$field];
}
}
return parent::label($fieldName, $text, $options);
}
}
Drop that in app/View/Helper/MyFormHelper.php
Add it to the helper array and alias it if you want to.
Add a public $labels array to the model with field => label-text structure.
Hope it works.
Quoting from the cakephp Book (ver 1.3):
Note that only fields of the model you are directly doing find on will be translated. Models attached via associations won't be translated because triggering callbacks on associated models is currently not supported.
Has anyone come up with a solution for this?
If not could you give me some pointers concerning the following simple scenario.
I have 2 models:
Project, Category.
Project HABTM Category
I have properly set up i18n table and I have a few entries in the db, all translated. When I retrieve a project it does retrieve the translation but not the translated category because as it says in the cakephp book models attached via associations won't be translated.
I have another workaround; I don't know if it is any better or worse performance- or style-wise, only that it suits the "fat models, skinny controllers" goal:
AppModel.php
public function getTranslatedModelField($id = 0, $field) {
$res = false;
$db = $this->getDataSource();
$tmp = $db->fetchAll('SELECT content from s2h_i18n WHERE model = ? AND locale = ? AND foreign_key = ? AND field = ? LIMIT 1',
array($this->alias, Configure::read('Config.language'), $id, $field)
);
if (!empty($tmp)) {
$res = $tmp[0]['s2h_i18n']['content'];
}
return $res;
}
SomeModel.php
public function afterFind($results, $primary = false) {
foreach ($results as $key => $val) {
if (isset($val['SomeOtherModel']) && isset($val['SomeOtherModel']['id'])) {
$results[$key]['SomeOtherModel']['name'] =
$this->SomeOtherModel->getTranslatedModelField($val['SomeOtherModel']['id'], 'name');
}
// other possible queries for other models and/or fields
}
return $results;
}
OK I found a solution. Which is mostly a workaround. I should have thought of that earlier.
What I'm doing is the following. I'm finding all projects and recursively all categories associated with projects. Now since cakephp does not translate categories I am using the results from the initial query and I am performing a second one only for categories but using the category id values that I found on the first query. Now cakephp translates categories since I'm only searching for them and I can have their data translated.
At the moment I'm OK with this solution but it would be nice if first cakephp makes the translate behavior out of the box ready or secondly if someone had a behavior that could support retrieval of translation on associated models.
I generalized the afterFind part a bit, so that it automatically grabs the fields to translate from the associated models' actsAs["Translate"] array, and uses an array of associated models to (potentially) translate:
public function afterFind($results, $primary = false){
$modelsToTranslate = array("SomeModel", "AnotherModel");
foreach ($results as $key => $val){
foreach($modelsToTranslate as $mtt){
if (isset($val[$mtt])){
foreach($val[$mtt] as $fieldname => $fieldval){
foreach ($this->$mtt->actsAs["Translate"] as $fieldToTranslate){
$results[$key][$mtt][$fieldname][$fieldToTranslate] = $this->$mtt->getTranslatedModelField($val[$mtt][$fieldname]['id'], $fieldToTranslate);
}
}
}
}
}
return $results;
}
I took above solution and generalized both functions a bit, now it needs to be used together with the translate behaviour and both functions need to go into the model.php - everything else should work by itself:
public function getTranslatedModelField($id = 0, $field) {
$res = false;
$translateTable = (isset($this->translateTable))?$this->translateTable:"i18n";
$db = $this->getDataSource();
$tmp = $db->fetchAll(
"SELECT content from {$translateTable} WHERE model = ? AND locale = ? AND foreign_key = ? AND field = ? LIMIT 1",
array($this->alias, Configure::read('Config.language'), $id, $field)
);
if (!empty($tmp)) {
$res = $tmp[0][$translateTable]['content'];
}
return $res;
}
public function afterFind($results, $primary = false) {
if($primary == false && array_key_exists('Translate', $this->actsAs)) {
foreach ($results as $key => $val) {
if (isset($val[$this->name]) && isset($val[$this->name]['id'])) {
foreach($this->actsAs['Translate'] as $translationfield) {
$results[$key][$this->name][$translationfield] =
$this->getTranslatedModelField($val[$this->name]['id'], $translationfield);
}
} else if($key == 'id' && is_numeric($val)) {
foreach($this->actsAs['Translate'] as $translationfield) {
$results[$translationfield] =
$this->getTranslatedModelField($val, $translationfield);
}
}
}
}
return $results;
}