MapReduce not working in CakePHP 3.x - php

I'm using CakePHP 3.x, my application has add/edit pages, in edit action I'm using this code.
$patient = $this->Patients->get($patientId);
to get record of patient.
Now I want to modify value of some field after find operation, let say I want to convert dob field (date_of_birth) into different date format, in CakePHP 2.x it's possible in afterFind callback but in CakePHP 3.x here in last paragraph it state that,
If you need to modify the results after they have been fetched you should use a Modifying Results with Map/Reduce function to modify the results. The map reduce features replace the ‘afterFind’ callback found in previous versions of CakePHP.
I had also use MapReduce but it won't work for me.

Map/reduce is kind of an overkill for such a simple task, I'd suggest to use a result formatter instead, ie Query::formatResults().
In order to use any of this, ie a mapper/reducer or a formatter, you must use Table::find() instead of Table::get(), as the latter doesn't return a query, but the result, and the options do not support mappers/reducers or formatters.
However, depending on where you need the formatted value, a helper, a virtual field, or just formatting when necessary might be the better option.
Anyways, here's a basic example:
$patient = $this->Patients
->find();
->where([
'id' => $patientId
])
->formatResults(function($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
return $results->map(function($row) {
// note that now `dob` is a string!
$row['dob'] = $row['dob']->i18nFormat('dd. MMMM yyyy');
return $row;
});
})
->firstOrFail();
See also
Cookbook > Database Access & ORM > Entities > Creating Virtual Fields
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
API > \Cake\Datasource\QueryTrait::formatResults()
API > \Cake\I18n\Time::i18nFormat

Related

CakePHP ORM : Query with a join on the same table with a non-standard association

I have a very particular use case, and i can't find a clean solution with the ORM. I've searched a lot, and maybe my database model is not correct, I'm not sure.
I use CakePHP 3.8.11.
So, I have a table "MaintenanceTypes" with 3 important fields : id, name, and periodicity. Periodicity (in days) means "this maintenance is to be done every (for instance) 30 days".
Periodicity are like 7 (week), 30 (month), 90 (trimester) and so on.
I also have a table "Operations", they are little unit tests that belongs to a "MaintenanceType" (fields are id, name, maintenance_type_id).
What is special in this case, is that, as a business rule, Operations belonging to a MaintenanceType with a periodicity of 7 days is "included" in every MaintenanceType with a greater periodicity; that means that every trimester, you should do every Operations associated directly to the trimester, but also every Operations associated with the month, and the week, etc.
In raw SQL it's trivial :slight_smile:
mt_ref is the reference MaintenanceType, mt_inc are the included MaintenanceTypes (with a lesser periodicity) and finally, every Operations belonging to any of the MaintenanceTypes found.
SELECT mt_ref.id, mt_ref.name, mt_ref.periodicity,
mt_inc.name, mt_inc.periodicity, o.name
FROM maintenance_types mt_ref
LEFT JOIN maintenance_types mt_inc
ON (mt_inc.periodicity <= mt_ref.periodicity)
LEFT JOIN operations o ON (o.maintenance_type_id = mt_inc.id)
WHERE mt_ref.id = 3
I've tried to declare the association between MaintenanceTypes, but I can't find a way to declare that the association is done on the periodicity field, and, extra points, not on a equality but on a "less or equal".
To add extra difficulties, I use this query for a (very good) JQuery Datatables CakePHP plugin (https://github.com/allanmcarvalho/cakephp-datatables), so I can't simply pass the raw SQL, and I must use the ORM...
I hope this is clear, and that someone could help me on this one !
Thanks a lot !
If you need query builder instances, then pretty much have two options here that are more or less straightforward, that is either use associations with custom conditions, or manual joins.
Custom association conditions
With associations you'd probably do something like a self-association with MaintenanceTypes with a disabled foreign key and custom conditions, like so in your MaintenanceTypesTable class:
$this
->hasMany('MaintenanceTypesInc')
->setClassName('MaintenanceTypes')
->setForeignKey(false)
->setConditions(function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
return $exp->lte(
$query->identifier('MaintenanceTypesInc.periodicity'),
$query->identifier('MaintenanceTypes.periodicity')
);
});
Disabling the foreign key will prevent the ORM from creating the default A.fk = B.fk condition when joining in the association. It should be noted that you cannot contain a hasMany association with a disabled foreign key, you can only join it in! You could use a hasOne or even belongsTo association instead, but it would kinda be a lie, as you don't have a 1:1 relationship here (at least as far as I understand it).
Also note that you don't neccesarily have to use a callback with all expressions for the conditions, you could pass the conditions as a key => value array with a manually instantiated identifier expression, or even as simple string (the latter however will not be recognized when using automatic identifier quoting):
->setConditions([
'MaintenanceTypesInc.periodicity <=' =>
new \Cake\Database\Expression\IdentifierExpression('MaintenanceTypes.periodicity');
]);
->setConditions('MaintenanceTypesInc.periodicity <= MaintenanceTypes.periodicity');
Assuming you also have an association for Operations in your MaintenanceTypesTable class, you should be able to join in both, the new association and the Operations association via the query builders *JoinWith() methods, like this:
$query = $maintenanceTypesTable
->find()
->select([
'MaintenanceTypes.id', 'MaintenanceTypes.name', 'MaintenanceTypes.periodicity',
'MaintenanceTypesInc.name', 'MaintenanceTypesInc.periodicity',
'Operations.name',
])
->leftJoinWith('MaintenanceTypesInc.Operations');
In the results, the association data will be put under the _matchingData key, ie you can obtain it like $entity->_matchingData->MaintenanceTypesInc and $entity->_matchingData->Operations. If you don't want that, then you need to use aliases for the fields of the associations, like:
->select([
'MaintenanceTypes.id', 'MaintenanceTypes.name', 'MaintenanceTypes.periodicity',
'mt_inc_name' => 'MaintenanceTypesInc.name', 'mt_inc_periodicity' => 'MaintenanceTypesInc.periodicity',
'op_name' => 'Operations.name',
])
If you don't want to select all the fields everytime, use a custom finder as in the manual joins example below.
Manual joins
Using manual joins gives you complete freedom, with the query builders *Join() methods you can create whatever joins you like, and you don't have to use possible workarounds with associations.
You can add them in a custom finder for proper reusability, it could look something like this in your MaintenanceTypesTable class:
public function findWithIncludedMaintenanceTypes(\Cake\ORM\Query $query, array $options)
{
return $query
->select(/* ... */)
->leftJoin(
['MaintenanceTypesInc' => 'maintenance_types'],
function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
return $exp->lte(
$query->identifier('MaintenanceTypesInc.periodicity'),
$query->identifier('MaintenanceTypes.periodicity')
);
}
)
->leftJoin(
['Operations' => 'operations'],
function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
return $exp->equalFields(
'Operations.maintenance_type_id ',
'MaintenanceTypesInc.id'
);
}
);
}
Then you simply use the finder wherever you need it, like this:
$query = $maintenanceTypesTable
->find('withIncludedMaintenanceTypes');
Note that just like in the associations example, you can use string or array conditions for the custom joins too.
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together
Cookbook > Database Access & ORM > Query Builder > Loading Associations
Cookbook > Database Access & ORM > Query Builder > Loading Associations > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Loading Associations > Adding Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Using Finders to Load Data
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods

Get value of Entity Object

I have picture like below.
I want to get value of project_id is 2,and number_request is 1.
But i can't get it.
I have use $data['items][0][project_id] and I print $data->items but i can't show it on screen.
Please help me.
Thank you so much.
Dumping objects doesn't necessarily give you an actual representation of the objects structure, but custom formatted debug information, defined via the magic __debugInfo() method.
Properties enclosed in brackets are special debug info, what is being shown there might have been gathered from anywhere but a property with the same name, see
https://github.com/cakephp/cakephp/blob/3.3.4/src/ORM/ResultSet.php#L593-L598
What you have there is basically a collection, so if you want the first entry of the collection, you can simply call first(), like
$entity = $data->first();
$projectId = $entity->project_id;
$numberRequest = $entity->numberRequest;
I would highly suggest that you study the docs a little more, as data access is really basic stuff.
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets
Cookbook > Database Access & ORM > Query Builder > Selecting Rows From A Table
Cookbook > Database Access & ORM > Entities > Accessing Entity Data
Cookbook > Collections
API > \Cake\ORM\ResultSet
First you have to collect first object into an variable like
$val = $data->items; //its an object
$val1 = $val[0];
$projectId = $val1->project_id;
$NumberRequest = $val1->number_request;

Laravel exclude current id from query eloquent results

I am fairly new to laravel and I built a little "similar posts" section. So every post has a tag and I query all the id's from the current tag. And then I find all the posts with thoses id's. Now my problem is that the current post is always included. Is there an easy way to exclude the current id when querying?
I can't seem to find anything in the helper function on the laravel docs site
this is my function:
public function show($id)
{
$project = Project::findOrFail($id);
foreach ($project->tags as $tag){
$theTag = $tag->name;
}
$tag_ids = DB::table('tags')
->where('name', "=", $theTag)
->value('id');
$similarProjects = Tag::find($tag_ids)->projects;
return view('projects.show', ['project' => $project, 'similarProjects' => $similarProjects]);
}
An easy way to solve your issue would be to use the Relationship method directly instead of referring to it by property, which you can add additional filters just like any eloquent transaction.
In other words, you would need to replace this:
Tag::find($tag_ids)->projects
With this:
Tag::find($tag_ids)->projects()->where('id', '!=', $id)->get()
Where $id is the current project's id. The reason behind this is that by using the method projects(), you are referring your model's defined Relationship directly (most probably a BelongsToMany, judging by your code) which can be used as a Query Builder (just as any model instance extending laravel's own Eloquent\Model).
You can find more information about laravel relationships and how the Query Builder works here:
https://laravel.com/docs/5.1/eloquent-relationships
https://laravel.com/docs/5.1/queries
However, the way you are handling it might cause some issues along the way.
From your code i can assume that the relationship between Project and Tag is a many to many relationship, which can cause duplicate results for projects sharing more than 1 tag (just as stated by user Ohgodwhy).
In this type of cases is better to use laravel's whereHas() method, which lets you filter your results based on a condition from your model's relation directly (you can find more info on how it works on the link i provided for eloquent-relationships). You would have to do the following:
// Array containing the current post tags
$tagIds = [...];
// Fetch all Projects that have tags corresponding to the defined array
Project::whereHas('tags', function($query) use ($tagIds) {
$query->whereIn('id', $tagIds);
})->where('id', !=, $postId)->get();
That way you can exclude your current Project while avoiding any duplicates in your result.
I don't think that Tag::find($tag_ids)->projects is a good way to go about this. The reason being is that multiple tags may belong to a project and you will end up getting back tons of project queries that are duplicates, resulting in poor performance.
Instead, you should be finding all projects that are not the existing project. That's easy.
$related_projects = Project::whereNotIn('id', [$project->id])->with('tags')->get();
Also you could improve your code by using Dependency Injection and Route Model Binding to ensure that the Model is provided to you automagically, instead of querying for it yourself.
public function show(Project $project)
Then change your route to something like this (replacing your controller name with whatever your controller is:
Route::get('/projects/{project}', 'ProjectController#show');
Now your $project will always be available within the show function and you only need to include tags (which was performed in the "with" statement above)

Retrieving computed subset of columns in doctrine

I want to develop a RESTful API based on Slim Framework and Doctrine 2. I have a detailed permission management. So I have permissions defined as:
role:admin|entity:person|column:name|write:1
Im considering the most effective way to implement the right management into the web service.
Therefore I need to filter a computed subset of columns when building my query. What is the best place to do that, still enabling me to use all the default methods like findAll() etc. I could of course filter my fields like below:
$all = Article::createQuery('a')->getArrayResult();
/*this is getting ALL the fields -it would be better to filter before
retrieving from the db
*/
$allFiltered = array();
foreach($all as $index=>$article){
$filteredArticle = new Article();
foreach($user->getPermission('Article','r') as $permission){
$column = $permission->column;
$filteredArticle->$column = $article->$column;
}
$allFiltered[$index]=$filteredArticle
)
$app->response->setBody(json_encode($all));
Is there a way to do this at one place for all retrieving methods ?
Doctrrine2 can select partial object doc
You can fetch all permitted columns and just concatenate with a query ...

manipulate a result set from laravel eloquent

I've just picked up laravel after deciding to move to a php framework. I'm retrieving a result set of articles from a database using an eloquent query:
$posts = Article::select(array('articles.id','articles.article_date','articles.image_link','articles.headline','articles.category', 'articles.published'));
this works fine and results come out as expected.
However I now want to change the format of the article date from the mysql date format to another for the entire collection.
I thought about iterating through the object and changing using the usual php methods but wondered if there was an easier way to manipulate the data either when initiating the query or en mass with the object itself.
I've looked at mutators? but wasnt sure if this was appropriate or how to implement
Any help, pointers appreciated
You're right, mutators are the way to go. (Laravel 3 syntax)
Getter and Setter documentation for Eloquent models
public function get_article_date()
{
return date('d-m-Y H:i:s', strtotime($this->get_attribute('article_date'));
}
Getting the attribute
$articles = Articles::get(array(
'article_date',
'image_link',
'headline',
'published'
));
foreach($articles as $article)
{
echo $article->article_date;
}
Every time you get the date from your model it will run through the mutator first returning your modified result.
Absolutely no need to run raw queries for something like this.
EDIT got set and get mixed up... more coffee needed (answer edited)
Im not sure if this is working, but give it a try
$posts = Article::select(array('articles.id',DB::raw('CAST(articles.article_date as date)'),'articles.image_link','articles.headline','articles.category', 'articles.published'));

Categories