If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeVe directamente a /api/users/5.jsonld
. Este usuario posee un CheeseListing
... y hemos decidido incrustar los campos title
y price
en lugar de mostrar sólo el IRI. ¡Genial!
Antes hemos hablado de un filtro muy chulo llamado PropertyFilter
, que nos permite, por ejemplo, añadir ?properties[]=username
a la URL si sólo queremos recuperar ese campo. Lo hemos añadido a CheeseListing
, pero no a User
. ¡Arreglemos eso!
Por encima de User
, añade @ApiFilter(PropertyFilter::class)
. Y recuerda que tenemos que añadir manualmente la declaración use
para las clases de filtro: use PropertyFilter
.
... lines 1 - 6 | |
use ApiPlatform\Core\Serializer\Filter\PropertyFilter; | |
... lines 8 - 15 | |
/** | |
... lines 17 - 20 | |
* @ApiFilter(PropertyFilter::class) | |
... lines 22 - 24 | |
*/ | |
class User implements UserInterface | |
... lines 27 - 190 |
Y... ¡hemos terminado! Cuando actualizamos, ¡funciona! Aparte de las propiedades JSON-LD estándar, sólo vemos username
.
Pero espera, ¡hay más! Quita la parte de ?properties[]=
por un segundo para que podamos ver la respuesta completa. ¿Qué pasaría si quisiéramos obtener sólo la propiedad username
y la propiedad title
de la relación incrustada cheeseListings
? ¿Es posible? Totalmente, sólo tienes que conocer la sintaxis. Vuelve a poner ?properties[]=username
. Ahora añade &properties[
, pero dentro de los corchetes, pon cheeseListings
. Luego[]=
y el nombre de la propiedad: title
. ¡Dale caña! ¡Muy bien! Bueno, el title
está vacío en este CheeseListing
, pero te haces una idea. La cuestión es ésta: PropertyFilter
es una buena idea y puede utilizarse para filtrar datos incrustados sin ningún trabajo adicional.
Hablando de filtros, hemos dado a CheeseListing
un montón de ellos, incluyendo la posibilidad de buscar por title
o description
y filtrar por price
. Vamos a añadir otro.
Desplázate hasta la parte superior de CheeseListing
para encontrar SearchFilter
. Vamos a dividir esto en varias líneas
... lines 1 - 16 | |
/** | |
... lines 18 - 34 | |
* @ApiFilter(SearchFilter::class, properties={ | |
* "title": "partial", | |
* "description": "partial" | |
* }) | |
... lines 39 - 41 | |
*/ | |
class CheeseListing | |
... lines 44 - 202 |
Buscar por title
y description
está muy bien. ¿Pero qué pasa si quiero buscar por propietario: encontrar todos los CheeseListings
que pertenecen a un User
concreto? Bueno, ya podemos hacerlo de otra manera: obtener los datos de ese usuario y mirar su propiedad cheeseListings
. Pero tenerlo como filtro podría ser súper útil. Diablos, ¡entonces podríamos buscar todos los listados de quesos propiedad de un usuario concreto y que coincidan con algún título! Y... si los usuarios empiezan a tener muchos cheeseListings
, podríamos decidir no exponer esa propiedad en User
en absoluto: la lista podría ser demasiado larga. La ventaja de un filtro es que podemos obtener todos los listados de quesos de un usuario en una colección paginada.
Para ello... añade owner
ajustado a exact
.
... lines 1 - 16 | |
/** | |
... lines 18 - 34 | |
* @ApiFilter(SearchFilter::class, properties={ | |
... lines 36 - 37 | |
* "owner": "exact" | |
* }) | |
... lines 40 - 42 | |
*/ | |
class CheeseListing | |
... lines 45 - 203 |
Ve a actualizar los documentos y prueba la ruta GET. Tenemos un nuevo cuadro de filtrado, incluso podemos buscar por varios propietarios. Dentro de la caja, añade el IRI - /api/users/4
. También puedes filtrar por id
, pero se recomienda el IRI.
Ejecuta y... ¡sí! Obtenemos el CheeseListing
para ese User
. Y la sintaxis de la URL es maravillosamente sencilla: ?owner=
y el IRI... que sólo parece feo porque está codificado en la URL.
¡Pero podemos volvernos aún más locos! Añade un filtro más: owner.username
ajustado a partial
.
... lines 1 - 16 | |
/** | |
... lines 18 - 34 | |
* @ApiFilter(SearchFilter::class, properties={ | |
... lines 36 - 38 | |
* "owner.username": "partial" | |
* }) | |
... lines 41 - 43 | |
*/ | |
class CheeseListing | |
... lines 46 - 204 |
Esto es muy bonito. Vuelve a actualizar los documentos y abre la operación de recogida. Aquí está nuestro nuevo cuadro de filtro, para owner.username
. Fíjate en esto: Busca "cabeza" porque tenemos un montón de nombres de usuario con cabeza de queso. ¡Ejecuta! Esto encuentra dos listados de queso propiedad de los usuarios 4 y 5.
Busquemos a todos los usuarios... para estar seguros y... ¡sí! Los usuarios 4 y 5 coinciden con la búsqueda del nombre de usuario. Probemos a buscar exactamente este cheesehead3
. Ponlo en la casilla y... ¡Ejecuta! ¡Ya está! La búsqueda exacta también funciona. Y, aunque estamos filtrando a través de una relación, la URL está bastante limpia:owner.username=cheesehead3
.
Vale, sólo un tema más breve para esta parte de nuestro tutorial: los subrecursos.
Heyyy back Omar! :)
First, sorry for the slow reply - holidays started a bit early for me, and these harder API Platform questions usually wait for me.
Hmm, what you're describing sounds an awful lot like GraphQL. And, though I haven't used it personally, API Platform does also expose a GraphQL API - https://api-platform.com/docs/core/graphql/ - perhaps that's the direction you should check into (unless you've already checked into it).
Cheers!
Hey, many many thanks for this helpful tutorial. it's really helpful.
but now i got a error. and that is when i try to fetch user data then i saw this error. single user and all user both.
<blockquote> "@context": "/api/contexts/Error",
"@type": "hydra:Error",
"hydra:title": "An error occurred",
"hydra:description": "The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the \"api_platform.eager_loading.max_joins\" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the \"enable_max_depth\" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth).",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "/home/mono/Projects/Goodness/vendor/api-platform/core/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php",
"line": 137,
"args": []
},
</blockquote>
i try to find the solution from api platform. but still i can't solve it. could you please tell me about eager-loading. and how can i solve it.
here is my user.php code.
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* @ApiResource(
* normalizationContext={"groups"={"user:read"}},
* denormalizationContext={"groups"={"user:write"}},
* )
* @ApiFilter(PropertyFilter::class)
* @UniqueEntity(fields={"username"})
* @UniqueEntity(fields={"email"})
*/
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
* @Groups({"user:read","user:write"})
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
* @Groups({"user:write"})
*/
private $password;
/**
* @ORM\Column(type="string", length=255, unique=true)
* @Groups({"user:read","user:write","products:item:get","products:write"})
* @Assert\NotBlank()
*/
private $username;
/**
* @ORM\OneToMany(targetEntity=Product::class, mappedBy="owner", cascade={"persist"}, orphanRemoval=true)
* @Groups({"user:read","user:write"})
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* @return Collection|Product[]
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->setOwner($this);
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->removeElement($product)) {
// set the owning side to null (unless already changed)
if ($product->getOwner() === $this) {
$product->setOwner(null);
}
}
return $this;
}
and here is product.php (like CheeseListing.php)
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\ProductRepository;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use Carbon\Carbon;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* collectionOperations={ "get","post"},
* itemOperations={
"get"={
* "normalization_context"={"groups"={"products:read","products:item:get"}}
* },
* "put",
* "delete"
* },
* attributes={
"pagination_items_per_page"=10,
* "formats"={"json","jsonld","html","jsonhal","csv"={"text/csv"}},
* },
* normalizationContext={"groups"={"products:read"},"swagger_defination_name"="Read"},
* denormalizationContext={"groups"={"products:write"},"swagger_defination_name"="Write"}
* )
* @ORM\Entity(repositoryClass=ProductRepository::class)
* @ApiFilter(BooleanFilter::class,properties={"isPublished"})
* @ApiFilter(SearchFilter::class, properties={
* "title": "partial",
* "description": "partial",
* "owner": "exact"
* })
* @ApiFilter(PropertyFilter::class)
*/
class Product
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* it is title column
* @ORM\Column(type="string", length=255, unique=true)
* @Groups({"products:read","products:write","user:read","user:write"})
* @Assert\NotBlank()
* @Assert\Length(
* min=5,
* max=40,
* maxMessage="write your title in less then 20 chars"
* )
*/
private $title;
/**
* @ORM\Column(type="integer", nullable=true)
* @Groups({"products:read","products:write","user:read","user:write"})
* @Assert\NotBlank()
*/
private $price;
/**
* @ORM\Column(type="text", nullable=true)
* @Groups({"products:read","products:write","user:read","user:write"})
* @Assert\NotBlank()
*/
private $description;
/**
* @ORM\Column(type="boolean", nullable=true)
* @Groups({"products:read","products:write","user:write","user:read"})
*/
private $isPublished;
public function __construct(string $title)
{
$this->createdAt = new \DateTimeImmutable();
$this->title = $title;
}
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $createdAt;
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="products",fetch="EAGER")
* @ORM\JoinColumn(nullable=false)
* @Groups({"products:read","products:write","user:read","user:write"})
*/
private $owner;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
// public function setTitle(string $title): self
// {
// $this->title = $title;
//
// return $this;
// }
public function getPrice(): ?int
{
return $this->price;
}
public function setPrice(?int $price): self
{
$this->price = $price;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
/**
* @Groups({"products:read"})
*/
public function getShortDescription(): ?string
{
if(strlen($this->getDescription()) < 20){
return $this->description;
}
return substr($this->getDescription(),0,20).'...';
}
/**
* @SerializedName("details")
*/
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
// public function setTextDescription(?string $description): self
// {
// $this->description = nl2br($description);
//
// return $this;
// }
public function getIsPublished(): ?bool
{
return $this->isPublished;
}
public function setIsPublished(?bool $isPublished): self
{
$this->isPublished = $isPublished;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
/**
* @Groups({"products:read"})
*/
public function getCreatedAtAgo(): string
{
return Carbon::instance($this->getCreatedAt())->diffForHumans();
}
// public function setCreatedAt(?\DateTimeInterface $createdAt): self
// {
// $this->createdAt = $createdAt;
//
// return $this;
// }
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
i am using symfony version 5.3.0
please help me. sorry the comment so long.
Hey Covi A.!
I'm really sorry for my very delayed reply - you had a tough question and I've been working on a new library this past week!
So, I believe the problem is basically one of recursion. When you serialize a user, you're using the group user:read
. That means the User.product
property is serialized. But then, in Product, on the owner property, you also have user:read
. This means that it then tries to serialize the "owner" property, which is a User. So, it serializes that User.... which then serializes its User.product property... and so on.. forever. I believe the "too many joins" is basically another way of saying "too much recursion".
The easiest solution is to remove user:read
from the Product.owner
property. If you fetch a Product directly, the owner would still be included (since it's in the product:read
group), but it wouldn't try to serialize recursively anymore.
Let me know if that helps!
Cheers!
Hello there!
I'm still learning a lot of stuff lately, and that's great. Thanks SymfonyCast. But could I have some advice on a search problem ?
I have an Article entity on one hand, and a Category (=tag) entity on the other. An article can have many categories. In my search function, I can generate something like :
/api/articles?categories[]=api/categories/1&categories[]=api/categories/2&categories[]=&page=1
which returns all the articles having tag1 and all the articles having tag2. But what I really want, is the articles having both, to narrow the list.
Is there a simple way to do that ? Have I missed some documentation ? Or should I learned how to make Custom Research Filters...? finger crossed
Hey Jean-tilapin!
Really happy we've been useful - keep up the hard work!
> But what I really want, is the articles having both, to narrow the list.
Hmm. Good question! It's a little bit tough to read due to the dynamic nature of the class, but here is the logic behind the search filter: https://github.com/api-plat...
If I'm guessing and reading correctly, you are using the "exact" strategy, which means you are falling into this case - https://github.com/api-plat... - which is a "WHERE IN". That means you're getting something like "WHERE category in (api/categories/1, api/categories/2, api/categories/3)". So.. exactly what you're saying - it will return all articles that are in *any* of these categories.
Looking through the rest of this class, unless I'm missing something, you will not be able to use the SearchFilter out of the box for this. So... you'll need a custom filter. Fortunately, this isn't too hard and we did cover it in a recent tutorial :). Check out https://symfonycasts.com/sc... and the next chapter after.
Let me know if that helps!
Cheers!
Hello,
Unless I have missed something, there is a fairly basic operation that I need to do but can't see how. It would be similar to having something like this in your example.
Request a user (item get), with embedded cheese-listings filtered to isPublished=true.
In other words, get user 1 with his published cheese-listings.
Or /api/users/1?cheese-listings.isPublished=true
In the interface, filters only show up in the get collection pane... which on the face of it seems obvious, but the case I'm describing doesn't seem so far-fetched and I really want to avoid multiple requests to achieve what seems rather trivial.
Thanks!
Hey Ian!
Yea, it's a pretty good question :). So, the way that you're "supposed" to do this is probably by fetching the User and then fetching the exact cheese listings you need - like GET /api/cheeses?user=/api/users/1&published=1
(or something like that, you get the idea). I know that you've already thought about this (and are trying to avoid the extra HTTP request), but this is basically what ApiPlatform wants you to do.
Check out this video for some rationale about how the embedded resources are loaded - https://symfonycasts.com/screencast/api-platform-security/filtered-collection - I think it will help highlight what's going on.
I believe the only way for you to accomplish this would be to:
A) Add a publishedCheeseListings field (like I did) by adding a getter method
B) Use the PropertyFilter - https://symfonycasts.com/screencast/api-platform/property-filter - as a way to be able to sometimes request this field and sometimes not request it. The only thing I'm not sure about is if there is a way to NOT include the field by default, and only include it IF it's requested via the PropertyFilter (I know it's possible to return it by default and then avoid returning it via the PropertyFilter, just not sure if you can do add fields via PropertyFilter).
Anyways, let me know if that helps :).
Cheers!
hi, is there a way to filter the subresources while fetching the main resource
For example:
All Items:
{
[
brand: 'audi',
colors: [
{name: blue},
{name: green}
]
}
Is there a way to filter the results of a subresource, so the result would be following?
{
[
brand: 'audi',
colors: [
{name: blue}
]
}
Hey Stefan L.
Have you tried configuring the filter like this?
@ApiFilter(SearchFilter::class, properties={"brand.colors": "exact"})
Cheers!
Isn't this only for filtering the main Resource, so for example it will return all brands where a specific color is included, but it will filter the brands, not the subresource colors?
Hey Stefan L.!
Yea, I believe also that this would filter the main resource, not the colors property. So, the overall issue is how API Platform loads the colors property. The logic looks like this:
A) API Platform makes an initial query for the main resource. In this example, let's pretend the main resource is for "cars". So, when you GET /api/cars, it queries for all the car resources. If you have any filters applied (e.g. ?brand=audi) then those are used to modify the query.
B) To return the "colors" property for each car, API Platform simple calls $car->getColors()
on each Car object. And so, you can see why you would always get all the colors returned.
So if you want to filter that sub-collection, you probably need to do it (more or less) as a custom field. You could still expose your "colors" field as a normal field. However, you would then probably need a custom normalizer for the Car resource so that you could modify this field dynamically. The process would be similar to when we add a completely custom field in the next tutorial - https://symfonycasts.com/screencast/api-platform-security/custom-field#adding-the-custom-isme-field - the difference would be that you aren't really adding a new field. Instead, you would read the Request query parameter and, if it exists, you would change the "colors" property to a different value.
Let me know if that makes sense! Often, the more natural way to do something like this is to make a request to /api/colors?car=/api/cars/5&color=blue
. In other words, make a direct query to the resource you want to filter. However, I realize that under certain situations, this isn't ideal - so what you're trying to do isn't wrong - just showing how it might work more easily in some situations.
Cheers!
Yes, it makes sense to me, thanks :)
Unfortunately I have to use this query on a list, so /api/colors?car=/api/cars/5&color=blue would only work for 1 item and there would also be cars with no color but they have to be visible too.
I am trying to solve it with a custom field, thank you very much :)
Hi, i have this config in my Entities
* @ApiResource(
* collectionOperations={
* "get",
* },
* itemOperations={
* "get",
* "delete"={
* "controller"=NotFoundAction::class,
* "read"=false,
* "output"=false,
* },
* "enhancement"={
* "method"="GET",
* "normalization_context"={"groups"={"enhancement:read"}},
* },
* }
* )
and i have 'enhancement:read' in almost all fields and in relation also, and i thought that API platform will query DB with joins, but i see in my symfony debugger that to get 5 entities and corresponding related entities api platform is making 9 queries, maybe i'm doing something wrong
Hi Daniel K.!
Interesting. I don't know the answer to this, but I know where to look. API Platform (I'm guessing you know this part, but just in case - it's not something we talked about in the tutorial) automatically fetches relationships eagerly. The class that does that is this one: https://github.com/api-plat...
I would add some debug code to this class and figure out *where* & why the eager loading is not happening. I can't see anything with your code or that class that would make me expect this behavior.
Let me know what you find out - I'd really be interested!
Cheers!
Hey there
I didn't try this but looks like you have to declare your filter service first like so:
services:
#...
someEntity.search_filter:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { someProperty: 'strategy' }, {...} ]
...
Then you have to bind that service to your ApiResource (entity), and then it should work. You can find more info about configuring filters here: https://api-platform.com/docs/core/filters/#doctrine-orm-and-mongodb-odm-filters
I hope this helps. Cheers!
Thanks Diego! Sorry, I removed by comment because I didn't see your answer! :/ I managed to find a solution before seeing your comment and it seems that you were right. I posted the whole solution here: https://stackoverflow.com/q... Maybe it will help others. ;)
// 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
}
}
Heyyy, thanks a lot for the great courses!
I have a small question regarding 2 things, "Filtering" and "Relations".
I'v been working for quite some time with a framework based on symfony (Shopware), and they have a very powerful searching system. When using the "List" api to get a list of the entitties, we can pass a "Criteria" Json object. Thanks to this "Criteria" object, I can pass any filter on any field I want. I can also inside this object pass "Associations" which basically says, 'Load the fields in this relation object' or 'Just the ID is enough'.
All of this functionality exists even when adding any new entities without any more configurations!
An example of the json object that would be sent with the api is:
Here is another example with "Associations", imagin we are fetching an order, and we want to get all transactions made for this customer, beside the current state of these transactions (Order -> Transaction -> TransactionState):
Do anyone knows how can I implement something like this!! Is there some more config in the background that I can add or some other library.
Thanks a lot in advanced