Apigility Doctrine filter all fetch - php

I'm on an ZF2 Apigility application with doctrine and the QueryBuilderFilter module.
Based on this example
I am trying to add a filter to all entities fetch that contain an IdDealer property.
For that I have attached a FetchListener to all fetch events :
DoctrineResourceEvent::EVENT_FETCH_PRE
DoctrineResourceEvent::EVENT_FETCH_ALL_PRE
And then I try to add a filter to the query if the entity is compatible with this filter.
public function __invoke(DoctrineResourceEvent $event)
{
if ($event->getName() == DoctrineResourceEvent::EVENT_FETCH_ALL_PRE)
$entity_class = $event->getEntity();
else //DoctrineResourceEvent::EVENT_FETCH_PRE
$entity_class = $event->getEntityClassName();
/** Entity has idDealer so we filter for the user */
if (method_exists($entity_class, 'getIdDealer')) {
$em = $event->getObjectManager();
$filterManager = $this->getServiceManager()->get('Zf\Doctrine\QueryBuilder\Filter\ManagerOrm');
$filterManager->filter(
$em->createQueryBuilder()->select('row')->from($entity_class, 'row'),
$em->getMetadataFactory()->getMetadataFor($entity_class),
[
'filter' => [
'type' => 'eq',
'field' => 'idDealer',
'value' => (int)$this->getUser()->getIdDealer()->getId(),
]
]
);
}
}
But I cannot get the filter to work.
I have found some info on internet that says that I should be able to retrieve the QueryBuilder from the event but it doesn't seem to be the case anymore.
Anyone with an idea on how to get this working ?

I have just spent a good few hours trying to do something very similar.
The zf-doctrine-querybuilder guide suggests you can use Apigility Doctrine Events to filter querys:
https://github.com/zfcampus/zf-doctrine-querybuilder#use-with-apigility-doctrine
and links to an example:
https://github.com/zfcampus/zf-doctrine-querybuilder/blob/master/docs/apigility.example.php
However I believe this suggestion and example code is now out of date due to the change "Remove QueryBuilder from DoctrineResourceEvents; add ObjectManager #182 ":
https://github.com/zfcampus/zf-apigility-doctrine/pull/182
I believe the way they want us to do this going forward is via custom Query Providers. See the following documentation that gives an example of how to bind a custom query provider to a specific entity:
https://github.com/zfcampus/zf-apigility-doctrine#query-providers
Query Providers are available for all find operations. The find query
provider is used to fetch an entity before it is acted upon for all
DoctrineResource methods except create.
A query provider returns a QueryBuilder object. By using a custom
query provider you may inject conditions specific to the resource or
user without modifying the resource. For instance, you may add a
$queryBuilder->andWhere('user = ' . $event->getIdentity()); in your
query provider before returning the QueryBuilder created therein.
If you want to look at an example Query Provider listener see ZF\Doctrine\QueryBuilder\Query\Provider\DefaultOrm:
https://github.com/zfcampus/zf-doctrine-querybuilder/blob/master/src/Query/Provider/DefaultOrm.php
For far I've not found any other solutions to getting a handle on the QueryBuilder being used as was previously possible when you could get it from the DoctrineResourceEvent object.
Hope this helps
Alex

Related

Searchfilter only works on GetCollection, I want it on Get

I have the following annotation written in my Entity RegistrationHashes in API platform:
#[ORM\Entity()]
#[ORM\Table(name: "registration_hashes")]
#[ApiResource(
operations: [new Get(), new GetCollection()],
normalizationContext: ['groups' => ['read']],
security: 'is_granted("ROLE_USER")',
)]
#[ApiFilter(
filterClass: SearchFilter::class,
properties: [
'registrationId' => SearchFilterInterface::STRATEGY_START,
'hash' => SearchFilterInterface::STRATEGY_EXACT,
]
)]
When query /registration_hashes I can search for the two properties registrationId and hash They match and this works. Except I dont want to return a collection, I just want either the one row or not found. How can I apply my SearchFilter to GET operation only?
Get operation is not made to do that and can't be use like you want to.
Get operation always need an api identifier in order to retrieve an item based on it. registrationId in your case.
If you want to search for items you will always need to use collection operations.
In your case, just call your getCollection operation with the needed parameters.
Something like that :
/api/registration_hashes?registrationId=xxx&hash=xxx
If you combination of registration and hash is unique you will get only one result.
Also, Search Filter of api platform are made to work only on collection operations. Its intentional.
Update :
One way to do it is to do something like that :
new Get(
uriTemplate: "/users/{registrationId}/{hash}
provider: CustomProvider::class
)
And do your own logic inside CustomProvider.php to get the object using
public function provide(Operation $operation, array $uriVariables = [], array $context = [])
{
$hash = $uriVariables['hash'];
$registrationId = $uriVariables['registrationId']
//You own logic to get it from database
}
Look at https://api-platform.com/docs/main/core/state-providers/ for deeper explanation about state provider

ApiPlatform/Symfony 6 - add searchfilter on custom getter (not in database)

I'm working on a project whit api platform 2.6 and symfony 6.
I have an Entity based on a table from my database.
My API exposes the fields of my table.
but I have a specific need to add information that does not exist in the database.
I create a custom getter, I integrated it into the normalization group, and it shows fine when responding from API.
But I would like to add a filter on this field.
ApiFilter(
SearchFilter::class, properties: [
'customInformation' => 'exact',
It does not work.
How to make Api-Platform take it into account as if it were a field of my database ?
Thank !
William
Duplicate of How to apply an ApiFilter(SearchFilter: class) to a dynamic getter of an entity? [doctrine-orm, api-platform, graphql]
ApiPlatform filters only work on properties for now.
You could probably accomplish that with a custom filter but i find it hard to imagine a case where you need to filter on data that does not exist.
There is probably a change on your conception that will do the job.
Update based on your comment :
I strongly advise you to change this
function getEmergency(): string
{
if ( $this->createdby == 'admin' and createdAt > days + 3) {
return 'urgent';
}
else
{
return 'not urgent';
}
}
To something like this
function isUrgent(): bool
{
return $this->createdby === 'admin' and createdAt > days + 3;
}
Much simplier method since there is only 2 case possible.
Then for your problem with api platform why dont you simply filter like this for example
&createdBy=admin&createadAt[before]=20220105
And inside your class
#[ApiFilter(SearchFilter::class, properties: ['createdBy' => 'exact'])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
class foo {}
Look at this Syntax on the link bellow to better understand date filter with api platform: ?property[<after|before|strictly_after|strictly_before>]=value
https://api-platform.com/docs/core/filters/#date-filter

Bad Performance when using a output DTO with doctrine entities with a set of relations

API Platform version(s) affected:
/srv/api # composer show | grep api-platform
api-platform/core v2.6.8 Build a fully-featured hypermedia or GraphQL API in minutes!
Description
To define the response of our API endpoints, we have used attributes on the generated Doctrine entity such as:
/**
* #ORM\Table(name = "products")
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
#[ApiResource(
collectionOperations: [
'get' => [
'path' => '/products',
],
],
itemOperations: [
'get' => [
'path' => '/products/{id}',
],
],
normalizationContext: [
'groups' => [
'product:read',
],
],
output: ProductOutput::class,
)]
class Product {
.... // properties and getters+setters
}
The Product entity has a 1:n relation to the Variant entity which is also a ApiResource with an different endpoint /variants. The Variant entity has several relations to other entities and some values of all entities are translatable with https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/translatable.md.
The performance was as expected => good enough.
Later on, it was required to "enrich" the response of /products and /variants with some data, which was not mapped in relations between Product <> additional-data | Variant <> additional-data, so we decided to use Outputs DTO with DataTransformers, as documented in the API-Platform docs.
The DataTransformer's method transform puts the data into the DTO by using therespective getters of the entities, e. g.:
$output = new ProductOutput();
$output->id = $object->getId();
$output->category = null !== $object->getCategory() ?
$this->iriConverter->getIriFromItem($object->getCategory()) :
'';
$output->identifierValue = $object->getIdentifierValue();
$output->manufacturer = $object->getManufacturer();
$output->variants = $object->getVariants();
The $object is a Product entity, in this case.
The DTO contains only public properties, such as
/**
* #var Collection<int, Variant>
*/
#[Groups(['product:read'])]
public Collection $variants;
and the Groups attributes, which are also defined in the normalizationContext of the ApiResource attribute in the Product entity above.
After that, we found the performance had drastically deteriorated: A request to the /products endpoint which "lists" 30 products with the related variants needs around 25 seconds.
After analyzing, we determined the following:
without DTO: Doctrine runs one single query with a lot of joins to retrieve all the related data from the database.
with DTO: Doctrine runs in sum 3.155 single queries to get the data.
by default, API-Platform uses Eager-Fetching (see https://api-platform.com/docs/core/performance/#force-eager), but it seems so that will be ignored if the getters of a entity are used in the DTO.
the serialization process needs the most time. That is maybe (also) a Symfony issue.
In a try to reduce the Doctrine queries, we created a DataProvider to fetch the related data. This actually worked, as using the DataProvider reduced the number of queries to +/- 50, but the serialization process also needed around 25s. So the cause of the performance problem does not seem to be the lazy-loading of doctrine, which is now done.
The question is: Why is using a DTO so much slower how would it be it possible to get performance back to an acceptable level?
I may be wrong, but I guess you are mistaking EAGER and LAZY loading in doctrine. As far as I understand, EAGER loaded associations will be fully loaded immediately, while LAZY loaded associations will only be fully loaded when the property is accessed via the property getter (what is actually happening in your transformer).
https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#by-eager-loading
(There is also the EXTRA_LAZY option that won't even fully load related entities if you only access them with a couple of allowed methods)
From my opinion, as long as you are not serializing the related entities as nested documents (but as IRIs only), as far as I understand you shouldn't even need to fully load those entities. So LAZY (and don't accessing the related entities via getter) would be the way to go. This way we've been able to speed up entity loading a lot for entities which only needed to be serialized as an IRI (although we aren't using DTO and transformers).
It was not possible to improve performance when using a DTO for this data-structure. Instead of a DTO and data transformer we used a Doctrine Event Listener (postLoad) to set the value which is a unmapped property (Doctrine/Symfony: Entity with non-mapped property) now.

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)

MapReduce not working in CakePHP 3.x

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

Categories