Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Filtering & Searching

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We're doing awesome! We understand how to expose a class as an API resource, we can choose which operations we want, and have full control over the input and output fields, including some "fake" fields like textDescription. There's a lot more to know, but we're doing great!

So what else does every API need? I can think of a few things, like pagination and validation. We'll talk about both of those soon. But what about filtering? Your API client - which might just be your JavaScript code, isn't going to always want to fetch every single CheeseListing in the system. What if you need the ability to only see published listings? Or what if you have a search on the front-end and need to find by title? These are called "filters": ways to see a "subset" of a collection based on some criteria. And API Platform comes with a bunch of them built-in!

Filtering by Published/Unpublished

Let's start by making it possible to only return published cheese listings. Well, in a future tutorial, we're going to make it possible to automatically hide unpublished listings from the collection. But, for now, our cheese listing collection returns everything. So let's at least make it possible for an API client to ask for only the published ones.

At the top of CheeseListing, activate our first filter with @ApiFilter(). Then, choose the specific filter by its class name: BooleanFilter::class... because we're filtering on a boolean property. Finish by passing the properties={} option set to "isPublished".

... lines 1 - 11
/**
... lines 13 - 22
* @ApiFilter(BooleanFilter::class, properties={"isPublished"})
... line 24
*/
class CheeseListing
... lines 27 - 145

Cool! Let's see what this did! Refresh! Oh... what it did was break our app!

The filter class BooleanFilter does not implement FilterInterface.

It's not super clear, but that error means that we forgot a use statement. This BooleanFilter::class is referencing a specific class and we need a use statement for it. It's... kind of a strange way to use class names, which is why PhpStorm didn't autocomplete it for us.

No problem, at the top of your class, add use BooleanFilter. But... careful... most filters support Doctrine ORM and Doctrine with MongoDB. Make sure to choose the class for the ORM.

... lines 1 - 6
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
... lines 8 - 146

Ok, now move over and refresh again.

We're back to life! Click "Try it out". Hey! We have a little isPublished filter input box! If we leave that blank and execute... looks like 4 results.

Choose true for isPublished and try it again. We're down to two results! And check out how this works with the URL: it's still /api/cheeses, but with a gorgeous ?isPublished=true or ?isPublished=false. So just like that, our API users can filter a collection on a boolean field.

Oh! Also, down in the response, there's a new hydra:search property. OoooOOO. It's a bit techy, but this is explaining that you can now search using an isPublished query parameter. It also gives information about which property this relates to on the resource.

Text Searching: SearchFilter

How else can we filter? What about searching by text? On top of the class, add another filter: @ApiFilter(). This one is called SearchFilter::class and has the same properties option... but with a bit more config. Say title set to partial. There are also settings to match on an exact string, the start of a string, end of a string or on word_start.

Anyways, this time, I remember that we need to add the use statement manually. Say use SearchFilter and auto-complete the one for the ORM.

... lines 1 - 7
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
... lines 9 - 13
/**
... lines 15 - 25
* @ApiFilter(SearchFilter::class, properties={"title": "partial"})
... line 27
*/
class CheeseListing
... lines 30 - 148

Oh, and before we check this out, I'll click to open SearchFilter. This lives in a directory called Filter and... if I double click it... hey! We can see a bunch of other ones: ExistsFilter, DateFilter, RangeFilter, OrderFilter and more. These are all documented - but you can also jump right in and see how they work.

Anyways, go refresh the docs, open the GET collection operation and click to try it. Now we have a title filter box. Try... um... cheese and... Execute.

Oh, magnificent! It adds ?title=cheese to the URL... and matched three of our four listings. The hydra:search property now contains a second entry advertising this new way to filter.

If we want to be able to search by another property, we can add that too: description set to partial.

This is easy to set up, but this type of database search is still pretty basic. Fortunately, while we won't cover it in this tutorial, if you need a truly robust search, API Platform can integrate with Elasticsearch: exposing your Elasticsearch data as readable API resources. That's pretty freaking cool!

Let's check out two more filters: a "range" filter, which will be super useful for our price property and another one that's... a bit special. Instead of filtering the number of results, it allows an API client to choose a subset of properties to return in the result. That's next.

Leave a comment!

45
Login or Register to join the conversation
Dima Avatar
Dima Avatar Dima | posted 1 year ago | edited

there

Hey! Thanks for the great job as always!

I still have an error while trying to add use statement for Boolean Type. It says:

"The filter class "phpDocumentor\Reflection\Types\Boolean" does not implement "ApiPlatform\Core\Api\FilterInterface". Did you forget a use statement?"

...
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
...

This 'use' line is grey in IDE, looks like it doesn't see it.

#[ApiFilter(Boolean::class, properties: ['isPublished'])]

I didn't find a solution in the internet. Maybe you guys know how to fix it?

Reply
MolloKhan Avatar MolloKhan | SFCASTS | Dima | posted 1 year ago | edited

Hey Dima

I think I just spotted your problem. You're using "Boolean::class" instead of "BooleanFilter::class". Change that, and give it a try :)

Cheers!

Reply
Dima Avatar

Aah, yeah, indeed! My bad! Thank you

1 Reply
Raul M. Avatar
Raul M. Avatar Raul M. | posted 1 year ago

Hi Ryan,

I have some get colletionOperations in one of my entities and I want to know if is possible configure filters for each operation instead for the whole entity.

Thanks!

RaĆŗl.

Reply

Hey Raul M.!

Hmm. I have no idea! I've never used it, but it looks like you can configure your filters inside ApiResource if you know the service id of each filter - 2nd code block - https://api-platform.com/do...

And so... you MIGHT be able to add a "filters=" option under the collectionOperation's config (instead of inside ApiResource). But, I'm doing some guessing. And for this to work, I think you need to look up the service id of the filter you want.

So... maybe? But it's definitely not a common use-case.

Cheers!

Reply
Anton B. Avatar
Anton B. Avatar Anton B. | posted 2 years ago | edited

there , please advice the best practice for the following:
I made a custom filter for the entity (e.g. Cheeses),
Cheeses has embedded property (e.g. CheeseListings (Collection))
CheeseListings has embedded property (e.g. Translations (Collection))

The question is how can I properly filter the Translations Collection if I need to search Cheeses where CheeseListings.translations.language = 'EN'?
I know about Extensions, but I need to return just EN translations in the Translations Collection, it can be only achieved by

return $queryBuilder->getQuery()->getResult(); right from the Custom Filter

another words I need to perform the next query (Pseudo Code):

select * from cheeses left join cheese_listings ON (...) left join cheese_listings_translations ON (...) where cheese_listings_translations.language = 'EN'

Example:


    {
        "@id": "\/cheeses\/f60f1768-ebad-4eac-9f54-959b2afe54cf",
        "@type": "Cheeses",
        "id": "f60f1768-ebad-4eac-9f54-959b2afe54cf",
        "title": "Yummy",
        "cheeseListings": {
              "@id": "\/cheese_listings\/4e0d4ac2-4acf-4ae3-a399-252094040822",
              "@type": "cheeseListings",
              "id": "4e0d4ac2-4acf-4ae3-a399-252094040822",
              "title": "Titlte",
              "translations": {
                   {
                       "@id": "\/cheese_listings_translations\/cheese_listing=4e0d4ac2-4acf-4ae3-a399-252094040822;language=EN",
                       "@type": "CheeseListingTranslation",
                       "language": {
                           "@id": "\/languages\/de",
                           "@type": "Language",
                           "isoCode": "EN"
                   },
              "content": "Hello"
            }
          }
        }
    }
Reply

Hey Anton B.!

Sorry for my slow reply! This is a tricky question. Hmmm. Would it be possible to implement this entirely as a query extension? Here's the idea: in the query extension, you read the query parameter directly from the request. If it's present, you perform the joins necessary and modify the query to only return the en language. You could (and probably should) still have a Filter, because that's what causes the filter to show up in your documentation... you just wouldn't actually do the filtering logic in the filter class.

Buuuut, I have a feeling that I'm not thinking about some complication. Because you mentioned:

it can be only achieved by return $queryBuilder->getQuery()->getResult(); right from the Custom Filter

I don't understand why that's the case. Why can't you modify the query in the custom filter and allow the system to actually execute like normal? You would modify the query to have the joins and the WHERE for the language. Am I missing a complication?

Cheers!

Reply
Anton B. Avatar
Anton B. Avatar Anton B. | weaverryan | posted 2 years ago | edited

weaverryan , thank you for the reply, I thought a lot about this, and my conclusion is that my case is not a proper way of using the API Platform and REST itself. I think we shouldn't modify the child collection results. But if we still need to modify(filter) the child collection, the best way is to perform the separate request to that collection with another filter.

Reply
Mark V. Avatar
Mark V. Avatar Mark V. | posted 2 years ago | edited

What are your thoughts on adding a search filter on the id property to the collection endpoint of a resource?

I want to be able to retrieve multiple, specific items.

Feel like the GET collection endpoint is the right place for this. Annotation is not the problem and it does work.

` * @ApiFilter(SearchFilter::class, properties={

  • "id": "exact",
  • ...
  • })
    `

But is this ReSTful? Is there a better approach?

The only downside I can see is hitting the query string length limit when retrieving many items.

Reply

Hey Mark V.!

This looks perfectly fine to me - I would do it the exact same way. You are hitting the collection endpoint, then filtering the results - I don't see any problems with that :).

> The only downside I can see is hitting the query string length limit when retrieving many items

That's true... but there is really no other way to send data up on a GET request - this IS the proper way. Hopefully, if you hit the situation, there would be some other way to filter... or... I'm not sure - I'm having a hard time imagining how you could send any GET request up there with SO much data (maybe as header?). Hopefully you don't have this problem ;).

Cheers!

Reply
Daniel K. Avatar
Daniel K. Avatar Daniel K. | posted 2 years ago

Hi, can i use SearchFilter with 2 config on the same field something like "blabla": "exact", "blabla": "partial"?
Maybe i need to create second entity?

Reply

Hey Daniel K.

I believe you can't but give it a try. What are you trying to achieve? Perhaps you need a custom filter instead

Cheers!

Reply

Hi SymfonyCasts, how are you all doing? Thank you for these great tutorials.
And of course I have a question.

While filtering / searching how can I make sure only data is shown that is authorized to be shown.
Right now I'm using voters in my symfony applications and thinking about using elasticsearch.
But I cannot find any information about using elasticsearch in combination with voter security.
Also no info on making search queries myself that take the voters into account.

Can you guys help me out?
Thank you very much in advance.

Reply

Hey truuslee

Thanks for your kind words. Related to your question, I'm not totally sure because we don't talk about integrating ES with ApiPlatform but I found this in the docs:
https://api-platform.com/do...

It says that you should create a custom extension, so here is the link to it: https://api-platform.com/do...

I hope it helps. Cheers!

Reply
Michel B. Avatar
Michel B. Avatar Michel B. | posted 3 years ago

Hi, I was wondering if It possible to apply a searchFilter on concatenated fields/properties.
For example I would like to do an ipartial on firstname + " " + lastname
How would I do that with the searchFilter option , if possible at all?

Reply

Hey Michel B.

I'm afraid there is not a built-in filter for such task. I believe you have to create a custom filter, you can read how to do so here https://api-platform.com/do...

I hope it helps. Cheers!

Reply
Default user avatar

Hi again... and sorry for so many questions... But for some reason every filter I add to any entity is being ignored with no reason (explained). I even created a StackOverflow question where I explain. Basically it is not documented at all at Swagger and also it shows "filter ignored". Any suggestions?

Reply

Hey Martin

My first guess is that you may not have a getter for the status property. Another thing to check is the imports, double-check that you imported the right ones

Cheers!

Reply
Default user avatar
Default user avatar Martin | MolloKhan | posted 3 years ago | edited

Hey MolloKhan!

Thanks for replying!

I have created the entity using <a href="https://packagist.org/packages/symfony/maker-bundle&quot;&gt;maker bundle</a>, so the property getter is there. Also, this is what my Question imports looks like:

`
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\QuestionRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; // here
use ApiPlatform\Core\Annotation\ApiFilter; // and here
use Symfony\Component\Serializer\Annotation\Groups;

/**

  • @ORM\Entity(repositoryClass=QuestionRepository::class)
  • @ApiResource(normalizationContext={"groups"={"question"}})
  • @ApiFilter(SearchFilter::class, properties={"status": "partial"})
    */
    class Question
    {
    //....
    }
    `

I have followed <a href="https://api-platform.com/docs/core/filters/&quot;&gt;this guide</a> too.

Reply

Hmm, that's very strange. The setup looks right. What happens when you play with the API? Can you do a GET and POST request successfully?
What version of Symfony and ApiPlatform are you using?
If you create a new entity does it behaves the same?

Reply
Default user avatar
Default user avatar Martƭn FernƔndez | MolloKhan | posted 3 years ago

Diego, I have tried to replicate the issue on a new project and it worked. So, I assume the problem was some alternate configuration on the project. I was able to play with the API, posting and getting entities. Not sure. But I will try to copy the project with a new configuration. Thank you anyway!

Reply

Ohh, that's interesting. Perhaps if you upgrade ApiPlatform the error goes away but I might be wrong. Cheers!

Reply
Benjamin K. Avatar
Benjamin K. Avatar Benjamin K. | posted 3 years ago

Hi
Update: After testing Custom Filters its ok to use them. i didnt see yet a reason to write them because most filters are allready there. But for special purposes its ok. If i am right select - where is allready done. A custom Filter is AndWhere from queryBuilder. That makes sense we filter a resource and not build a complete query ...

https://api-platform.com/do...

Reply

Hey Benjamin K.!

> If i am right select - where is allready done. A custom Filter is AndWhere from queryBuilder. That makes sense we filter a resource and not build a complete query

That's correct :). It makes the filters fairly easy to build.

Cheers!

Reply
Default user avatar

Hi, great tutorial!!. Just a question, how you would search / filter in some JSON property (like user's roles)?

Reply

Hey Martin!

Hmm, good question! A normal SearchFilter would, in theory, work (as you would be searching through the JSON string)... but it would be fairly imprecise. To do this correctly, you would probably need to create a API Platform custom filter. Then, assuming you're using a database with a proper JSON type (like pgsql or MySQL 5.7 or higher), you would need to do a bit more work to query for the item via Doctrine. Basically, because JSON types are not supported across all database systems, as far as I know, there is no built-in way to properly query a JSON type in Doctrine. But there are resources out there - e.g. https://blog.liplex.de/sear... - about how to do this.

Let me know if that helps!

Cheers!

1 Reply
Default user avatar
Default user avatar Martin | weaverryan | posted 3 years ago | edited

Hey weaverryan.

Thanks for replying. It really helped. Thank you. I am not sure why but is not documented in Swagger UI. I guess this is because of the value returned (return $this->json($users);. But it's working!

Reply
Manuka L. Avatar
Manuka L. Avatar Manuka L. | posted 3 years ago

Hi, How do I do an OR operation on the filters? Lets say I want to search ?firstName=abc&lastName=abc. If "either" firstname OR lastname has "abc", how do I select the result?
The following seems to do and AND, where it would only return results if both firstname and lastname has the passed value.

user.filter_by_name:
parent: api_platform.doctrine.orm.search_filter
arguments: [{ firstname: ipartial, lastname: ipartial }]
tags: ['api_platform.filter']

Reply

Hi Manuka L.!

Sorry for the slow reply! I believe you will need to create a custom filter for this. The built-in filters always use AND logic... I think it's, sort of, just a missing feature when you configure the search filter (in my opinion). Here is an issue about this - https://github.com/api-plat... - and a gist that will hopefully help: https://gist.github.com/axe...

Cheers!

Reply

Hey!

small question: Is it possible to add a query param kind like a filter but for post resource ?
Then the request url will be:
POST: http://api.domain.local:8785/users?newQuery=true then the swagger will add a field under the doc ?

Reply

hi ahmedbhs!

Hmm. Maybe? :) I'm not sure I fully understand. Your Swagger docs (or more technically accurate, the OpenAPI specification) are "static" - you can view them by going to /api/docs.json. So, this is not something that can be affected by query parameters to a specific endpoint - it's a 100% static definition of your API.

So, what are you trying to accomplish exactly? Do you want to make it possible to POST a field... but ONLY when the URL has newQuery=true? If so, that might be done with a context builder - https://symfonycasts.com/screencast/api-platform-security/context-builder - if the query param exists, then add a new group that the field is in. Or do you want something different?

Cheers!

Reply
Olivier Avatar
Olivier Avatar Olivier | posted 3 years ago

Hi!

Little question, take for example that I have a "User" entity with a username property. Somewhere in my application I need to query a user from the API with the EXACT username, and at another place I want to query the samething but this time with PARTIAL. Is it possible to do that?

I know it's not the correct solution but something like that:
@ApiFilter(SearchFilter::class, properties={"username": "exact/partial"}

Then the request url will be:
http://api.domain.local:8785/users?username=tony&matchStrategy=exact

I'm not the best in english, hope you understand what I mean.
Thanks!

Reply

Hey Olivier

That's a good question and by looking at the ApiPlatform docs I couldn't find anything related to define 2 strategies on the same property. So, I think what you need to do is to create your own filter by following this steps: https://api-platform.com/do...

Cheers!

1 Reply

Hi SymfonyCasts,

I have lots of data in lots of different languages, like about 26 languages.
Should i give the client the opportunity to add a 'language' FILTER to the get request like ?language=en or should the client add an 'accept language' to the request header?
And if the 'accept language' is the answer, how do i deal with this in api platform?

Thank you for your answer .

Reply

Hey truuslee!

Sorry for my slow reply. Honestly, I'm not sure if there is a strictly-agreed-upon best practice for this or not. You've listed 2 possible options - and there is (technically) one more - having the language in the URL - e.g. /api/en/articles, though this last one doesn't feel very RESTful to me (as it would mean that, in a philosophical level, /api/en/articles/1 and /api/es/articles/1 are different rest "resources").

So, I would choose between the two options that you listed - choosing which ever one would be most convenient for whoever is using your API. Maybe the "accept language" header is a better practice, but I wouldn't stress out over it if the query parameter is more convenient. Let's look at the implementation details of each:

A) If you used, ?language=en, then probably it would mean that your API is designed so that if, for example, I make a request to /api/articles that it returns the list of ALL articles in ALL languages. Then adding the language filter is just a matter of using the built-in API Platform filters.

B) If you used the "accept language" header, you have a little bit more work to do. You would need to parse the "accept language" header to determine which language to ultimately use. I would then probably create an "API Platform Doctrine extension" - https://api-platform.com/docs/core/extensions/ - that would limit the collection or choose the item result based which language you determine is correct. Then you should also add a Content-Language response header set to which language you ultimately chose. I would do that via a listener - https://api-platform.com/docs/core/events/#custom-event-listeners - the POST_RESPOND one (which just means you add a listener to the kernel.response event - called ResponseEvent in Symfony 4.3 and higher.

Let me know if this helps! Mostly, follow what will be most useful in your API - it sounds like you are only considering solid option.

Cheers!

Reply

Thank you very much for your reply. You're a big help as always. I've chosen to do it with a filter, far less work.
I have other questions for you, I hope you don't mind?

1.
I have an entity Brand that is related to another entity called Product. (One product has one Brand, one Brand has many Products).
Now the api-client wants a list of all the brands a customer is using.
Something like this:
/api/customer/{id}/brands

But entity Brand is not related to the Customer entity, I can only get the brands via entity1 related to entity2, related to entity Product, finally related to entity Brand.
How can i do this?
I am thinking about creating a new entity with customer ids and brand ids together in a 'cross' table. But i hope there is a better way?

  1. I have created a Custom Doctrine ORM Extension.
    Because I want to add a groupby to a query.
    This works great, it makes sure the result set is grouped.
    But one thing is wrong. The hydra variable "<b>hydra:totalItems</b>" is still mentioning the original amount of records, as if there were no groupby.
    This is my code:
    `

final class FooExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{

public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass, string $operationName = null): void {
    $this->addGroupBy($queryBuilder, $resourceClass);
}

public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass, array $identifiers,string $operationName = null,array $context = []): void {
    $this->addGroupBy($queryBuilder, $resourceClass);
}

private function addGroupBy(QueryBuilder $queryBuilder, string $resourceClass): void
{
    if (Foo::class !== $resourceClass) {
        return;
    }
    $rootAlias = $queryBuilder->getRootAliases()[0];
   <b> $queryBuilder->addGroupBy($rootAlias . '.foo_field');</b>
}

}
`

How can i make hydra show me the correct number of records?

Thanks in advance and hope to hear from you soon!

Reply

Hey truuslee!

But entity Brand is not related to the Customer entity, I can only get the brands via entity1 related to entity2, related to entity Product, finally related to entity Brand.
How can i do this?
I am thinking about creating a new entity with customer ids and brand ids together in a 'cross' table. But i hope there is a better way?

Hmm. Ideally we can avoid adding duplication to our database to make API Platform happy :). My initial instinct (which is a bit of cheating) is to tell the API client to use a different endpoint. What I mean is: why do they need this specific endpoint? It's definitely possible to add it - but there could be a much simpler solution if we have some flexibility. For example, a URL like /api/brands?customer=/api/customers/1 would probably be easier, though this would still need to be a custom filter (since there is not actually a "customer" property on brands). Another option would be to simply "add" a brands field when you fetch a customer (instead of making it a different URL). You could probably do this by adding a getBrands() method to Customer that loops over all the products and then returns an array of each Brand that is related to all the Products (if I've understood the relationships correctly).

But anyways, to implement this, I would create a custom item operation on Customer for the /api/customers/{id}/brands URL. I try to avoid having custom operations because it usually means you're trying to "bend" the system a little far - but it's not a huge deal, and you need to get your job done :). In this case, you would write a custom controller and be able to do whatever you want.

But one thing is wrong. The hydra variable "hydra:totalItems" is still mentioning the original amount of records, as if there were no groupby.

Interesting! I don't know the answer to this - bit I'm a little surprised by it. The reason is that, behind the scenes, API Platform uses a "Doctrine paginator"... and since we're using Doctrine to build the query, I would expect it to be smart enough to get the proper count. Here is the code behind this: https://github.com/api-platform/core/blob/master/src/Hydra/Serializer/CollectionNormalizer.php#L96-L102

For you (and me), $object is an instance of ApiPlatform\Core\Bridge\Doctrine\Orm which is just a wrapper around Doctrine\ORM\Tools\Pagination\Paginator.

I'd be interested what the count query looks like. Typically (assuming you have a result that requires "pagination" - like more than 25 records) Doctrine will make a query specifically to get the total count (in addition to the query to get the specific results it needs). After making the API request, you can go to /_profiler to find the profiler for that API request. If you click into this and go to the Doctrine tab on the left, you'll be able to see all the queries including a query where Doctrine is trying to take your query and use it to get a count of all items. I'd be interested to know if that query looks "wrong" in some way.

I hope this helps!

Cheers!

Reply
Gustavo Avatar
Gustavo Avatar Gustavo | posted 3 years ago

Hi, thanks for that lesson.
How can i add an ApiFilter like BooleanFilter using a YAML config file?

Reply

Hi Gustavo!

Good question - I actually don't know! Here are how I think it might look (stolen from a few Stack Overflow references):


App\Entity\CheeseListing:
    attributes:
        filters:
            - 'ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter'
            # I'm not sure how you would pass a filter with options. Maybe this?
            ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter:
                properties: [{title: partial}]

Let me know if it works :).

Cheers!

Reply
Sung L. Avatar
Sung L. Avatar Sung L. | posted 4 years ago

How can I create a /search api endpoint for unrelated tables?

After watching this tutorial, adding search function for a specific entity seems simple.
I was wondering how I can create a /search endpoint to search in multiple tables. For example, I have a CheeseListing and WineListing tables and there are no relations. I want to have a search feature that returns matching records from cheese description and wine description.

Any tips how to do this?

Thanks.

Reply

Hey Sung L.!

Sorry for my slow reply - great question. There are two probable approaches to this:

1) Use the elastic search integration: https://api-platform.com/do... - it's very robust, but could be overkill if you just need to do something simple.

2) Create a custom class (not a Doctrine entity, but you can still put it in the Entity directory) that contains whatever fields you want... which is maybe just a "results" array that will contain a mixture of objects? You will only want the "read" operations to be enabled for this resource (and probably just the collection operation). Then, use a custom data provider (https://api-platform.com/do... to take control of how you fetch the "collection". I'm not exactly sure how this would all look in the end, however... as I'm just "thinking" out loud. Assuming you created some custom class called ListingSearchResult and marked this an ApiResource, I guess you might give that properties like "item", which will be a CheeseListing or WineListing embedded object... and... I'm not sure if you need any other properties. So, the endpoint may look a bit funny.

I don't have a super great answer for this - well for suggestion (2). I think it could work and be nice... but it would need some more investigation. And, we might need to take some inspiration from what the ElasticSearch implementation looks like in Api Platform. Of course, you could always create a traditional endpoint that returns the results... e.g. just query for the array of CheeseListing & WineListing objects, then serialize that collection to the "jsonld" format.

Sorry I couldn't be more clear - this is not totally clear in my head, obviously :).

Cheers!

Reply
Braunstetter Avatar

Ok, nice suggestions. I mean, thats something very common. A customer want to have a nice applications search and want me to write a search for some entities at once. I searched for a while for ElasticSearch implementation. And that reading and filtering process sounds very clear and easy but for writing and persisting thats another issue - I guess. Are there ressources how to get the ElasticSearch entites into the Database with Api-Platform?

Reply
Ouchayan H. Avatar
Ouchayan H. Avatar Ouchayan H. | posted 4 years ago

could I know when you will publish the rest of this amazing training ?

Reply

Hey Hamid,

We're trying to release one video per day! Most probably this course should be completely released in the beginning of the next week. Thank you for your patience!

Cheers!

Reply
Ouchayan H. Avatar
Ouchayan H. Avatar Ouchayan H. | Victor | posted 4 years ago

Thanks for you fast reply.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
    }
}
userVoice