Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Filtrar en las relaciones

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

Ve 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.

Selección de propiedades de relación incrustadas

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.

Buscar en propiedades relacionadas

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.

Buscar listados de quesos por nombre de usuario del propietario

¡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.

Leave a comment!

16
Login or Register to join the conversation
Covi A. Avatar
Covi A. Avatar Covi A. | posted hace 1 año

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.

"@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/do..., or limit the maximum serialization depth using the \"enable_max_depth\" option of the Symfony serializer (https://symfony.com/doc/cur....",
"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": []
},

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.

Reply

Hey Monoranjan Das!

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!

Reply
Jean-tilapin Avatar
Jean-tilapin Avatar Jean-tilapin | posted hace 1 año

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*

Reply

Hey Xav!

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!

Reply
Ian Avatar

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!

Reply

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/sc... - 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/sc... - 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!

Reply
Stefan L. Avatar
Stefan L. Avatar Stefan L. | posted hace 2 años

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}
]
}
Reply

Hey Stefan

Have you tried configuring the filter like this?


@ApiFilter(SearchFilter::class, properties={"brand.colors": "exact"})

Cheers!

Reply
Stefan L. Avatar

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?

Reply

Hey Stefan!

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/sc... - 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!

Reply
Stefan L. Avatar

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 :)

Reply
Daniel K. Avatar
Daniel K. Avatar Daniel K. | posted hace 2 años

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

Reply

Hi Daniel Klimowicz!

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!

Reply

Hey Jérôme 

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/do...

I hope this helps. Cheers!

Reply
Ajie62 Avatar

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. ;)

1 Reply

NP Jerome! and thanks for sharing your solution :)

Reply
Cat in space

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

Este tutorial funciona muy bien para Symfony 5 y la Plataforma API 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
    }
}