Should I use more find in this case? - cakephp

i have two controllers
Sections_controller.php
Articles_controller.php
Section model hasmany Article...
i want to fetch articles In the form of blocks like all news sites..every block have section name with links to the articles within this section..so i use this code......
The First block
$block1=$this->Article->find('all',
array(
'limit' => 4, // just fetch 4 articles
'order' => array('Article.created'=>'DESC'),
'conditions' => array('Section_id' => 87)
)
);
// set the section for the view
$this->set(compact('block1'));
The second block
$block2=$this->Article->find('all',
array(
'limit' => 4, // just fetch 4 articles
'order' => array('Article.created'=>'DESC'),
'conditions' => array('Section_id' => 88)
)
);
// set the section for the view
$this->set(compact('block2'));
and etc....
anyone have the best method in this task without Repetition find code..
notice..i cant pass $id in the function because articles must display when request site index example( www.newssite.com)

Any find(s) should be done in the Model, not the controller - this follows the MVC structure as well as the "fat models, skinny controllers" mantra, which helps keep with the MVC idea.
This is not only the way it "should" be done, but it will also allow you to have the code in just one place:
//in the Article model
function getArticlesBySection($id) {
$articles = $this->find('all', array(
'limit' => 4,
'order' => array('Article.created'=>'DESC'),
'conditions' => array('Section_id' => $id)
));
return $articles;
}
//in the Articles controller
$block1 = $this->Article->getArticlesBySection('87');
$block2 = $this->Article->getArticlesBySection('88');
$this->set(compact('block1', 'block2'));
The above should work just fine for what you want to do, but there is always a lot you can do to improve upon it - like setting it up to be a lot more flexible by accepting an array of options:
//in the Article model
function getArticles($id, $opts = null) {
$params = array();
//limit
$params['limit'] = 100; //catchall if no limit is passed
if(!empty($opts['limit'])) $params['limit'] = $opts['limit'];
//order
$params['order'] = array('Article.created'=>'DESC');
if(!empty($opts['order'])) $params['order'] = $opts['order'];
//conditions
$params['conditions'] = array();
if(!empty($opts['sections'])) array_push($params['conditions'], array('Section_id'=>$opts['sections']));
$articles = $this->find('all', $params);
return $articles;
}
//in the Articles controller
$opts = array('limit'=>4, 'sections'=>array('87'));
$block1 = $this->Article->getArticles($opts);
$opts['sections'] = array('88');
$block2 = $this->Article->getArticles($opts);
I'm sure there are things that can be done to make this more lean...etc, but it's how I like to write it for ease of use and readability, and at least gives you a start on how to think of model methods, and the ability to use and reuse them for different purposes.

You can accomplish this with a straight mysql query, but I'm not sure how you would fit it into a cake model->find function. You can do something like this:
$articles = $this->Article->query("SELECT * FROM articles a WHERE (SELECT COUNT(*) FROM articles b WHERE a.Section_id = b.Section_id AND a.created < b.created) < 4 ORDER BY a.Section_id, a.created DESC");
/* if you look at the results, you should have the 4 latest articles per section.
Now you can loop through and set up an array to filter them by section. Modify to fit your needs */
foreach($articles as $article) {
$results[$article['Article']['Section_id']][] = $article;
}
$this->set('results',$results);

Related

Counting and categorising instances of certain fields

I'm currently writing a survey style app that has some models:
Assessments
Recommendations
An assessment has many recommendations and is joined by the assessments_recommendations table, and I store data in the join table which are effectively answers.
One such answer is user impact. This field is an integer value between 0 and 10 and is different for each assessment->recommendation.
What I want to be able to do is send a count and categorisation of that field into a view so that I can chart it using chart.js. For example, I would like:
0-3 = Low
4-7 = Moderate
8-10 = High
As far as I can tell, I need to pass something along the lines of:
$counts [4, 6, 8]
$legend ['Low', 'Moderate', 'High']
So in the above example, 4 assessment->recommendations are classed as low, 6 are moderate, 8 are high.
Does anyone have any advice on how to do this in my controller? I've been reading up on collections but I'm not sure that is the correct way of doing this. I had issues as my data is stored in the _joinData part of the array and I couldn't figure out how to access it.
Any advice would be much appreciated.
Thanks!
Edit 1
This is the controller code I have been playing around with to get an initial collection working. The col variable passes through nicely and I can see this in the view. I get 'unknown getter' error on the join data line though. It isn't entirely clear to me from the docs what variables are what in the example in the book, so I've been very much trying every combo I can think of to try and get it to work before even trying to move on with the complete scenario.
public function view($id = null)
{
$this->loadModel ('Assessments');
$this->loadModel ('Recommendations');
$query = $this->Assessments->find('all', [
'conditions' => ['Assessments.id =' => $id],
'contain' => ['Recommendations']
]);
$assessment = $query->first();
$collection = new Collection($assessment->ToArray());
$countHigh = $collection->countBy(function ($assessment) {
return $assessment->_joinData->_user_impact > 7 ? 'High' : 'Error';
});
$this->set('assessments', $assessment);
$this->set('col', $collection);
$this->set('user_impact_high', $countHigh);
}
Edit 2
So I'm testing this some more. Trying to simplify it. Even using the groupBy function in it's simple form is failing. The code below generates the following error.
public function view($id = null)
{
$this->loadModel ('Assessments');
$this->loadModel ('Recommendations');
$query = $this->Assessments->find('all', [
'conditions' => ['Assessments.id =' => $id],
'contain' => ['Recommendations']
]);
$assessment = $query->first();
$collection = new Collection($assessment->ToArray());
$test = $collection->groupBy('client_id');
$this->set('assessments', $assessment);
$this->set('col', $collection);
$this->set('test', $test);
}
Error:
Cannot use object of type Cake\I18n\FrozenTime as array

How to specify a condition to retrieve data by radius (lat, lon)?

In cakephp 2, I was able to use a virtualField for this, but seems impossible in 3. I have been struggling with this for two days without luck from the internet or the cakephp manual. I get no errors, but I do get a blank return.
My code in my controller looks like this:
if (isset($this->request->data['location']) && (isset($this->request->data['radius']))){
$radius = $this->request->data['radius'];
$location = $this->request->data['location'];
$address = $location; // Google HQ
$HttpSocket = new Client();
$geocode = $HttpSocket->get('http://maps.google.com/maps/api/geocode/json?address='.$address.'&sensor=false');
$geocode = $geocode->json;
if ($geocode['status'] == "OK"){
$lat = $geocode['results'][0]['geometry']['location']['lat'];
$lon = $geocode['results'][0]['geometry']['location']['lng'];
$R = 6371; // earth's mean radius, km
// first-cut bounding box (in degrees)
$maxLat = $lat + rad2deg($radius/$R);
$minLat = $lat - rad2deg($radius/$R);
// compensate for degrees longitude getting smaller with increasing latitude
$maxLon = $lon + rad2deg($radius/$R/cos(deg2rad($lat)));
$minLon = $lon - rad2deg($radius/$R/cos(deg2rad($lat)));
$conditions[] = ['Venues.lat' => "'BETWEEN '".$minLat."' AND '".$maxLat."'"];
$conditions[] = ['Venues.lon' => "'BETWEEN :'".$minLon."' AND :'".$maxLon."'"];
}
$this->paginate =[
'limit' => 10,
'order' => ['Quads.date' => 'asc'],
'conditions' => $conditions,
'contain' => [
'Performers' => ['Genres'],
'Users' => ['Profiles'],
'Venues' => ['fields' => [
'name',
'id',
'verified',
'address1',
'city',
'zip_code'], 'States'=>['Countries']],
'Categories',
'Likes' => ['Users' => ['Profiles']]]];
$quads = $this->paginate();
Impossible is (nearly) nothing. The old virtual fields concept is gone, right, the new ORM is flexible enough so that this isn't necessary anymore.
Your problem is that you are defining the conditions the wrong way, what you are doing there by specifying key => value sets, is creating ordinary operator conditions, where the value is going to be escaped/casted according to the column type. In case you really don't receive any errors, I'd assume that the lat/lan columns are of a numeric type, so your BETWEEN ... strings do end up as numbers, and the conditions will look something like
Venus.lat = 0 AND Venus.lon = 0
Also note that you are creating a nested array, ie
[
['keyA' => 'value'],
['keyB' => 'value']
]
and while this works, you may run into further problems in case you're not aware of it, so you'd better stick with
[
'keyA' => 'value',
'keyB' => 'value'
]
unless there's actually a technical reason to use nested conditions.
tl;dr use expressions
That being said, you can use expressions to build the proper conditions, like
$conditions[] = $this->Quads->Venues
->query()->newExpr()->between('Venues.lat', $minLat, $maxLat);
$conditions[] = $this->Quads->Venues
->query()->newExpr()->between('Venues.lon', $minLon, $maxLon);
This will safely create proper conditions like
Venus.lat BETWEEN a AND b AND Venus.lon BETWEEN x AND Y
Note that it is advised to create the expressions via the table that holds the columns (VenuesTable in this case), as you'd otherwise have to manually specify the column type (see the fourth argument of QueryExpression::between()) in order for the correct casting/escaping to be applied!
See also
Cookbook > Database Access & ORM > Query Builder > Advanced Conditions
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
API > \Cake\Database\QueryExpression::between()

Custom component won't return value

OK, this is the situation. In my beforeSave function I want to manipulate some $this->request->data entries.
This is my component:
<?php
App::uses('Component', 'Controller');
class GetStationComponent extends Component {
public function getStationId ($station) {
$stationInstance = ClassRegistry::init('Station');
$conditions = array('OR' => array(
array('Station.code LIKE' => $station),
array('Station.naam LIKE' => $station),
array('Station.naam_overig LIKE' => $station)
));
$result = $stationInstance->find('list', array('conditions'=>$conditions));
$value = array_values($result);
$value = $value[0];
return $value;
}
}
?>
And this is my beforeSave function in my Controller:
public function beforeSave($options = array()) {
if (!empty($this->request->data['Experience']['vertrekstation']) && !empty($this->request->data['Experience']['aankomststation'])) {
$this->request->data['Experience']['vertrekstation'] = $this->GetStation->getStationId($this->request->data['Experience']['vertrekstation']);
$this->request->data['Experience']['aankomststation'] = $this->GetStation->getStationId($this->request->data['Experience']['aankomststation']);
}
return true;
}
It should return an ID of the stations name. But in the Database the name itself is stored (which is filled in by the user) instead of the ID. What do I need to change in my Component (I guess...) to return the right values?
(P.S. The query itself in the component returns an ID, because at first I'd put the 'beforeSave' directly into my function which saves the data, but then my validation error said that it wasn't a right value. Which is correct...)
To complement the other answers; to get just the value of a single field, use Model::field()
return $stationInstance->field('id', $conditions);
It is best to add a sort order to this statement to make sure that the results will always be returned in the same order:
return $stationInstance->field('id', $conditions, 'code ASC');
Since you only perform a single query on the Model, and don't do anything afterwards, you don't even need the intermediate $stationInstance variable. Your code can be further simplified to:
return ClassRegistry::init('Station')->field('id', $conditions, 'code ASC');
Some observations
Because of the 'fuzzy' matching on the name of the station, the first result may not always be the station intended by the user it's best to offer an 'autocomplete' functionality in your front-end and have the user pick the correct station (e.g. To prevent picking Den Haag when the user meant Den Haag HS)
If the station does not fully matches a station, you should present a warning that the station wasn't found
You didn't surround your search-terms with % for the LIKE queries. If you intend to search for 'name "contains", you should use '%' . $station . '%'. For "starts with" use $station . '%'
As #mark suggested; beforeSave() is a callback of the Model and should be located there.
Also; beforeSave() is triggered after validation has taken place, so it will probably be too late. beforeValidate() is the best callback for this
If the Experience model is already attached to the Station model, you don't need to use a component, because you can directly access the Station model. It's best to put the search-method inside the Station model;
Moving it all to the right(*) location
*) Other options are always possible, this is just a possible approach
Add the 'search' method to the Station-model;
app/Model/Station.php
public function getStationIdByName($name)
{
$name = trim($name);
if (empty($name)) {
return null;
}
$name = '%' . $name . '%';
$conditions = array(
'OR' => array(
array($this->alias . '.code LIKE' => $name),
array($this->alias . '.naam LIKE' => $name),
array($this->alias . '.naam_overig LIKE' => $name),
)
);
return $this->field('id', $conditions, 'code ASC');
}
..and use it in the Experience Model
app/Model/Experience.php
public function beforeValidate(array $options = array())
{
if (
!empty($this->data[$this->alias]['vertrekstation'])
&& !empty($this->data[$this->alias]['aankomststation'])
) {
// Directly access the Station-model from within the Experience Model
$this->data[$this->alias]['vertrekstation']
= $this->Station->getStationIdByName($this->data[$this->alias]['vertrekstation']);
$this->data[$this->alias]['aankomststation']
= $this->Station->getStationIdByName($this->data[$this->alias]['aankomststation']);
}
// Call parent-callback after setting the values
return parent::beforeValidate($options);
}
[UPDATE] Using the Conventions, prevent unwanted behavior
After writing the previous example, I noticed there are some flaws in your current setup;
If vertrekstation and aankomststation should hold the 'foreign key' of the station (the station-id) they are not named according to the CakePHP Model and Database Conventions
Because of 1) By putting this code inside the beforeValidate(), it will also be triggered when updating an existing record. Because you're using the aankomststation and vertrekstation field both to hold the name of the station (inside the Form) and the id (inside the database), the Model will attempt to look-up the station-id via the id when updating. NOTE that inside the form you'll still be using vertrekstation and aankomstation as field-name. These field names are not present in your database, and therefore will not be able to directly update data inside your database, that's where the beforeValidate() callback is used for
Because the Experience model needs two relations to the Station model (once as departure station ('vertrekstation'), once for arrival station ('aankomststation')), you will need an alias for the Station-model. See: Multiple relations to the same model
app/Model/Experience.php
class Experience extends AppModel {
/**
* Station will be associated to the 'Experience' Model TWICE
* For clarity, using an 'alias' for both associations
*
* The associated Models will be accessible via;
* $this->DepartureStation
* $this->ArrivalStation
*
* To stick to the CakePHP conventions, name the foreign keys
* accordingly
*/
public $belongsTo = array(
'DepartureStation' => array(
'className' => 'Station',
'foreignKey' => 'departure_station_id',
),
'ArrivalStation' => array(
'className' => 'Station',
'foreignKey' => 'arrival_station_id',
)
);
public function beforeValidate(array $options = array())
{
// vertrekstation and aankomststation hold the 'names' of the
// stations and will only be present if the form has been submitted
if (
!empty($this->data[$this->alias]['vertrekstation'])
&& !empty($this->data[$this->alias]['aankomststation'])
) {
// Directly access the Station-model from within the Experience Model
// using the *aliases*
$this->data[$this->alias]['departure_station_id']
= $this->DepartureStation->getStationIdByName($this->data[$this->alias]['vertrekstation']);
$this->data[$this->alias]['arrival_station_id']
= $this->ArrivalStation->getStationIdByName($this->data[$this->alias]['aankomststation']);
// Invalidate the vertrekstation and aankomststation fields if lookup failed
if (empty($this->data[$this->alias]['departure_station_id'])) {
// Unable to find a station. Mark the Form-field invalid
$this->invalidate('vertrekstation', __('A station with this name was not found'));
}
if (empty($this->data[$this->alias]['arrival_station_id'])) {
// Unable to find a station. Mark the Form-field invalid
$this->invalidate('aankomststation', __('A station with this name was not found'));
}
}
// Call parent-callback after setting the values
return parent::beforeValidate($options);
}
}
The find('list') option of Cake returns an array like
array( 1 => 'name1',
3 => 'name2',
//etc...
)
where the index is the id and the value is the display field you set on the model.
So, when you do $value = array_values($result);, you're extracting the values of the array (meaning, the display fields). I'm assuming you're not using the id as the displayField, so that's why it's returning the names and not the id.
I'm not sure why you're using find('list') instead of find('first') or other alternative, but if you don't want to modify that for whatever reason, the fix that should return the first id obtained by the search is
reset($result); //possibly not needed, but just in case
$value = key($result );
First you must understand how Cake works
There is no $this->request in your models. Its part of the controller.
In your model your passed data will be in $this->data directly.
public function beforeSave($options = array()) {
parent::beforeSave($options); // you also forgot the parent call
if (!empty($this->data[$this->alias]['vertrekstation']) && ...)) {
$this->data[$this->alias]['vertrekstation'] = ...;
}
return true;
}
Your find call also looks pretty screwed up. I dont know what you want to do.
But I strongly advice you to use debug() etc to find out what is returned and correct your code accordingly.
You probably need find(first) if you are only interesting in a single value.

Dynamically Build a SQL Where Clause with the IN Keyword

I'm using cakephp 2.3.0. Summary - In my controller, I have a query where it returns one or more id values. Then, I want to use those id values in my next query, with the SQL IN keyword. I know conceptually what I want to accomplish. I think I'm doing this the correct way in CakePHP, but I could be wrong. I'm just not sure on the coding piece to dynamically build my IN clause.
Here is my code snippet from my controller:
$this->set('userIDs', $this->Activity->ActivitiesOfInterest->find('all',
array (
'conditions' => array ('ActivitiesOfInterest.activities_id' => $id),
'fields' => array ('ActivitiesOfInterest.user_id'))));
The above code is fine as I get the value or values that I would expect. In the code below, I have hard-coded this piece, array(3, 4), which is equivalent to the SQL IN keyword. The values 3, 4 would be stored in the userIDs array. But, this is where I'm not sure how to dynamically build the values of 3 and 4, from the array in the above code. Of course, 3 and 4 may not always be the values because it just depends on what is in the database. I think I need to loop through the array and build up my list of id's. I'm still somewhat new to CakePHP and I'm guessing this is an easy answer for the experienced folks.
And, yes I actually do want to perform a deleteAll operation, based on my use case.
$this->Activity->ActivitiesOfInterest->deleteAll(
array('ActivitiesOfInterest.user_id' => array(3, 4)), false);
Thank you, much appreciated.
You can also do this by using sub query for deleteALL query.
$conditionsSubQuery['ActivitiesOfInterest.activities_id'] = $id;
$db = $this->Financial->getDataSource();
$subQuery = $db->buildStatement(
array(
'fields' => array('ActivitiesOfInterest.user_id'),
'table' => $db->fullTableName($this->Activity->ActivitiesOfInterest),
'alias' => 'ActivitiesOfInterest',
'offset' => null,
'joins' => array(),
'conditions' => $conditionsSubQuery,
'order' => null,
'group' => null
),$this->Activity->ActivitiesOfInterest
);
$subQuery = ' ActivitiesOfInterest.user_id IN (' . $subQuery . ')' ;
$subQueryExpression = $db->expression($subQuery);
$conditions[] = $subQueryExpression;
$result = $this->Activity->ActivitiesOfInterest->deleteAll(compact('conditions'));
You don't need to use $this->set(), unless you need those user-ids inside your view;
The Hash::extract() may be helpfull here.
// Find the user-ids
$result = $this->Activity->ActivitiesOfInterest->find('all', array (
'conditions' => array (
'ActivitiesOfInterest.activities_id' => $id
),
'fields' => array (
'ActivitiesOfInterest.user_id'
)
));
if ($result) {
// 'extract' just the user-ids from the results
$userIds = Hash::extract($result, '{n}.ActivitiesOfInterest.user_id');
$this->Activity->ActivitiesOfInterest->deleteAll(
array(
'ActivitiesOfInterest.user_id' => $userIds
),
false
);
}

cakephp sort link not working

Can't make sort links work. (concrete or virtual fields).
Vitual fields for my sum() field on this action:
$this->Qca->virtualFields['comps'] = 'Sum(CASE WHEN Qca.qca_tipcode = 1 THEN 1 END)';
$this->Qca->virtualFields['production'] = 'Sum(qca_end - qca_start)';
$this->Qca->virtualFields['idle'] = 'Sum(Qca.qca_durend)';
My find(), works fine:
$hoursvalues = $this->Qca->find('all', array('conditions' => $conditions,
'fields' => array('Qca.dir_id', 'Qca.name', 'Sum(CASE WHEN Qca.qca_tipcode = 1 THEN 1 END) AS Qca__comps', 'Sum(qca_end - qca_start) as Qca__production', 'Sum(Qca.qca_durend) as Qca__idle'),
'group' => array('Qca.dir_id')
)
);
and then:
$this->paginate('Qca' );
$this->set('hoursvalues', $hoursvalues);
What extra settings does $this->paginate('Qca' ); needs? Please note I have all data I need via find().
What is it I'm missing that sorting does not work for either concrete or virtual fields?
Thansk a lot!
Carlos
Part of your problem is the following:
$this->paginate('Qca');
$this->set('hoursvalues', $hoursvalues);
$this->paginate() returns an array with sorted values. What you need to do is specify your extra settings in $this->paginate array and then
$this->set('hoursvalues', $this->paginate('Qca'));
For your other fields, making them virtualFields will make them easier to work with.

Resources