CakePHP 3 : display data from other model and pass parameter in url from action - cakephp

I'm working on a project using CakePHP 3.x.
I have UserAddress, ServiceRequests, Service models.
There is a button on service/view/$id which when clicked will ask user to select address from service-requests/serviceArea which has a list of addresses added by user. service-requests/serviceArea view will contain a select button which when clicked will call add action in ServiceRequests controller with passing two parameters serviceId and userAddressId
This is the serviceArea function created by me.
public function serviceArea($id = null)
{
public $uses = array('UserAddress');
$service = $id;
$query = $userAddresses->find('all')
->where(['UserAddresses.user_id =' => $this->Auth->user('id')]);
$this->set(compact('userAddresses'));
$this->set('_serialize', ['userAddresses']);
}
How to display the address and also pass the $service parameter to the serviceArea view.
I am new to CakePHP, so if you think question is incomplete any edit to it will be appreciated instead of down-voting.
Thank You.
Edit 2
Thank for your answer #jazzcat
After changing my code according to yours and visiting http://domain.com/service-requests/service-area/$id. It is showing error as
Record not found in table "service_requests"
and pointing to the ServiceRequestsController on line no 33
The ServiceRequestController as containing line no 33 is
<?php
namespace App\Controller;
use App\Controller\AppController;
/**
* ServiceRequests Controller
*
* #property \App\Model\Table\ServiceRequestsTable $ServiceRequests
*/
class ServiceRequestsController extends AppController
{
/**
* isAuthorized method
*
*/
public function isAuthorized($user)
{
$action = $this->request->params['action'];
// The add and index actions are always allowed.
if(in_array($action, ['index', 'add', 'serviceRequests'])) {
return true;
}
// All other actions require an id.
if (empty($this->request->params['pass'][0])) {
return false;
}
// Check that the service request belongs to the current user.
$id = $this->request->params['pass'][0];
$serviceRequest = $this->ServiceRequests->get($id); // line : 33
if($serviceRequest->user_id == $user['id']) {
return true;
}
return parent::isAuthorized($user);
}
/* Other actions */
}
?>

This worked for me.
Just added the serviceArea action name in the isAuthorized method
if(in_array($action, ['index', 'add', 'serviceArea'])) {
return true;
}
and it's working fine as expected.

There is alot wrong with your code. Please read the docs
Is the table named user_addresses or user_address ?
You seem to mix the both.
The following would be the correct way to do it assuming your table is named user_addresses
public function serviceArea($id = null)
{
$this->loadModel('UserAddresses');
$userAddresses = $this->UserAddresses->find('all')
->where(['UserAddresses.user_id =' => $this->Auth->user('id')]);
// If you want to filter on the serviceArea ID aswell
if($id)
$userAddresses->andWhere(['id' => $id]);
// Setting SerivceArea ID to compact makes it available in view.
$serviceAreaId = $id;
$this->set(compact('userAddresses', 'serviceAreaId'));
$this->set('_serialize', ['userAddresses']);
}
This snippet:
$id = $this->request->params['pass'][0];
$serviceRequest = $this->ServiceRequests->get($id); // line : 33
Just checks if the first parameter passed to the method exists in ServiceRequests.
(That parameter could be anything, you have to keep that in mind when creating all your methods in that controller, that is to say the least.. bad)
I'm assuming that the service_requests table is associated with the users table and an user_id column exists in the service_requests table.
If that is the case this should work:
public function isAuthorized($user)
{
$action = $this->request->params['action'];
// The add and index actions are always allowed.
if(in_array($action, ['index', 'add'])) {
return true;
}
// Is not authorized if an argument is not passed to the method.
// Don't know why you'd want this , but sure.
if (empty($this->request->params['pass'][0])) {
return false;
}
// Check that the service request belongs to the current user.
$user_id = $this->Auth->user('id');
$serviceRequest = $this->ServiceRequests->find()->where(['ServiceRequests.user_id' => $user_id])->first();
if(!empty($serviceRequest)) {
return true;
}
return parent::isAuthorized($user);
}

Related

Sorty by calculated field with active record yii2

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

Fatal error: Declaration of CRMCoreContactController::save($contact) must be compatible with EntityAPIController::save

I recently installed CRM Core and all of its missing modules needed to run it. Sadly I need this module for the project that I am working on but the second I installed them I got this error.
Fatal error: Declaration of CRMCoreContactController::save($contact) must be compatible with EntityAPIController::save($entity, ?DatabaseTransaction $transaction = NULL) in /opt/lampp/htdocs/drupal/modules/crm_core/modules/crm_core_contact/includes/crm_core_contact.controller.inc on line 111
I went back in the code and I couldn't see what to change. Line 111 is the ver last line of the code. Ill paste the code as well maybe someone out there knows how to solve this, please.
<?php
/**
* CRM Contact Entity Class.
*/
class CRMCoreContactEntity extends Entity {
protected function defaultLabel() {
return crm_core_contact_label($this);
}
protected function defaultUri() {
return array(
'path' => 'crm-core/contact/' . $this->identifier(),
'options' => array(
'absolute' => TRUE,
),
);
}
/**
* Method for de-duplicating contacts.
*
* Allows various modules to identify duplicate contact records through
* hook_crm_core_contact_match. This function should implement it's
* own contact matching scheme.
*
* #return array
* Array of matched contact IDs.
*/
public function match() {
$checks = & drupal_static(__FUNCTION__);
$matches = array();
if (!isset($checks->processed)) {
$checks = new stdClass();
$checks->engines = module_implements('crm_core_contact_match');
$checks->processed = 1;
}
// Pass in the contact and the matches array as references.
// This will allow various matching tools to modify the contact
// and the list of matches.
$values = array(
'contact' => &$this,
'matches' => &$matches,
);
foreach ($checks->engines as $module) {
module_invoke($module, 'crm_core_contact_match', $values);
}
// It's up to implementing modules to handle the matching logic.
// Most often, the match to be used should be the one
// at the top of the stack.
return $matches;
}
}
/**
* #file
* Controller class for contacts.
*
* This extends the DrupalDefaultEntityController class, adding required
* special handling for contact objects.
*/
class CRMCoreContactController extends EntityAPIController {
public $revisionKey = 'vid';
public $revisionTable = 'crm_core_contact_revision';
/**
* Create a basic contact object.
*/
public function create(array $values = array()) {
global $user;
$values += array(
'contact_id' => '',
'vid' => '',
'uid' => $user->uid,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
return parent::create($values);
}
/**
* Update contact object before saving revision.
*/
protected function saveRevision($entity) {
if (!isset($entity->log)) {
$entity->log = '';
}
$entity->is_new_revision = TRUE;
$entity->uid = $GLOBALS['user']->uid;
return parent::saveRevision($entity);
}
/**
* Updates 'changed' property on save.
*/
public function save($contact) {
$contact->changed = REQUEST_TIME;
// Storing formatted contact label for autocomplete lookups.
$contact->name = crm_core_contact_label($contact);
return parent::save($contact);
}
}
Changing
public function save($contact)
to
public function save($contact, DatabaseTransaction $transaction = NULL)
should work.
You need to switch from PHP 7.x+ to PHP 5.6. This will resolve this error.
Can't give you more advice on how to downgrade without more details on what system you're running but there are many guides out there on this topic.

Yii record is not inserting into DB

Below is my controller & model logic - I just started a barebones Yii installation to play around with it more.
I get no errors but don't see the new entry in the database - my db has been configured in the main.php (this works as Gii runs).
// controllers/PageController.php
class PageController extends Controller
{
public function actionSave($value='')
{
$pageObj = new Page;
$pageObj->savePage();
}
}
// models/Page.php
class Page extends CActiveRecord
{
/**
* #return string the associated database table name
*/
public function tableName()
{
return 'page';
}
/**
* #return array validation rules for model attributes.
*/
public function rules()
{
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('title, date_updated', 'required'),
array('live', 'numerical', 'integerOnly'=>true),
array('user_id', 'length', 'max'=>10),
array('title', 'length', 'max'=>100),
array('content, date_published', 'safe'),
// The following rule is used by search().
// #todo Please remove those attributes that should not be searched.
array('id, user_id, live, title, content, date_updated, date_published', 'safe', 'on'=>'search'),
);
}
/**
* #return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'comments' => array(self::HAS_MANY, 'Comment', 'page_id'),
'user' => array(self::BELONGS_TO, 'User', 'user_id'),
'files' => array(self::MANY_MANY, 'File', 'page_has_file(page_id, file_id)'),
);
}
/**
* #return array customized attribute labels (name=>label)
*/
public function attributeLabels()
{
return array(
'id' => 'ID',
'user_id' => 'User',
'live' => 'Live',
'title' => 'Title',
'content' => 'Content',
'date_updated' => 'Date Updated',
'date_published' => 'Date Published',
);
}
/**
* Retrieves a list of models based on the current search/filter conditions.
*
* Typical usecase:
* - Initialize the model fields with values from filter form.
* - Execute this method to get CActiveDataProvider instance which will filter
* models according to data in model fields.
* - Pass data provider to CGridView, CListView or any similar widget.
*
* #return CActiveDataProvider the data provider that can return the models
* based on the search/filter conditions.
*/
public function search()
{
// #todo Please modify the following code to remove attributes that should not be searched.
$criteria=new CDbCriteria;
$criteria->compare('id',$this->id,true);
$criteria->compare('user_id',$this->user_id,true);
$criteria->compare('live',$this->live);
$criteria->compare('title',$this->title,true);
$criteria->compare('content',$this->content,true);
$criteria->compare('date_updated',$this->date_updated,true);
$criteria->compare('date_published',$this->date_published,true);
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
/**
* Returns the static model of the specified AR class.
* Please note that you should have this exact method in all your CActiveRecord descendants!
* #param string $className active record class name.
* #return Page the static model class
*/
public static function model($className=__CLASS__)
{
return parent::model($className);
}
public function savePage($value='')
{
$page = new page;
$model->isNewRecord = true;
$model->primaryKey = NULL;
$page->title='sample page';
$page->content='content for the sample page';
$page->save(false);
}
}
In Yii, when you want to insert into a table which has some null columns, you must put null columns in your rules as SAFE like below:
array('primaryKey','safe'),
Now, Yii knows that primaryKey is a null column. So, there would be no problem via inserting into the current model.
As a note, when you call save() method with FALSE, you are telling to your model to do not the validation on insert.
Also, the correct way to skip possible errors is to validate your model before inserting like below:
if($model->validate()){
// VALIDATE, YOU CAN CALL SAVE FUNCTION
}else{
//here you can send an error message via FLASH or you can debug what the exact error is like below:
CVarDumper::dump($model->getErrors(),5678,true);
Yii::app()->end();
}
I hope, it help
So simple... I hate Yii sometimes :-)
Had to set the save() to save(false)
$page = new page;
$page->isNewRecord = true;
$page->primaryKey = NULL;
$page->title='sample page';
$page->content='content for the sample page';
$page->save(false);
Thanks for that - I had missed out some columns.. (silly me)
Improve the function even further with the help above..
public function savePage()
{
$page = new page;
$page->isNewRecord = true;
$page->primaryKey = NULL;
$page->user_id = 1;
$page->live = 0;
$page->content='content for the sample page';
$page->date_updated = date('Y-m-d H:i:s');
$page->date_published = date('Y-m-d H:i:s');
$page->title='sample page';
if ($page->validate()) {
$page->save();
} else {
CVarDumper::dump($page->getErrors(),5678,true);
Yii::app()->end();
}
}

CakePHP pass a array to paginate() - new model

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.

Sorting information from related tables - Symfony2

A. I have a table with users and a table with friends and here are their connections:
This in the User class:
/**
* #ORM\OneToMany(targetEntity="Friend", mappedBy="user")
*/
protected $friends;
public function __construct()
{
$this->friends = new ArrayCollection();
}
and this is in the Friend class:
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="friends")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
I have an template, in which I render the friends of the current user just using
$friends = $user->getFriends();
But now I want to be able to sort the friends of the user. I created a repository class with this function:
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery('SELECT f FROM EMMyFriendsBundle:Friend f ORDER BY f.name ASC')
->getResult();
}
but it displays all the friends of all the users, not just the current user. Is there a way to show only the friends of the current user without using something like "WHERE friend.user_id = user.id"? If yes, please tell me how to do it.
B. Another thing I would like to ask is how to manage my controllers.
This is the action which renders the template to show all unsorted friends:
/*
* #Displays the basic information about all the friends of an user
*/
public function displayAction()
{
$user = $this->get('security.context')->getToken()->getUser();
if($user->getName() === NULL)
{
$name = $user->getUsername();
}
else
{
$name = $user->getName();
}
$this->get('session')->setFlash('hello', 'Hello, '.$name.'!');
$friends = $user->getFriends();
$cat = new Category();
$dd_form = $this->createForm(new \EM\MyFriendsBundle\Entity\ChooseCatType(), $cat);
return $this->render('EMMyFriendsBundle:Home:home.html.twig', array(
'name' => $name, 'friends' => $friends, 'user' => $user,
'dd_form' => $dd_form->createView()));
}
And I have a sortAction, but I don't know how to write it. It should be the same as the above, only the row
$friends = $user->getFriends();
should be replaced with
$friends = $em->getRepository('EMMyFriendsBundle:Friend')
->findAllOrderedByName();
but if I copy it, it's a lot code duplication and it's stupid. I thought of one controller with an if-statement, but what to have in it? Please give me some ideas! Thanks in advance!
Do this:
/**
* #ORM\OneToMany(targetEntity="Friend", mappedBy="user")
* #ORM\OrderBy({"name" = "ASC"})
*/
protected $friends;

Resources