-
-
Notifications
You must be signed in to change notification settings - Fork 878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(serializer): collect cache tags using a TagCollector #5758
Conversation
src/HttpCache/TagCollector.php
Outdated
* | ||
* @author Urban Suppiger <urban@suppiger.net> | ||
*/ | ||
class TagCollector implements TagCollectorInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you explain to me the difference between all these methods? I think that we should be able to simplify this and reduce the number of arguments. I'd suggest:
use ApiPlatform\Serializer; // we want to avoid creating a dependency between Serializer and HttpCache, the opposite is fine.
interface IriCollectorInterface {
public function collect(string $iri, mixed $object, array $context) {}
}
Not even sure if object
matters in the signature and it could be added to the context? It'd be interesting to add the attribute
that's being normalized if needed although that I'd start without it and if someone needs that we add it afterwards? Or do you already have a use case where that matters?
If that's the case, we can put everything we need inside our context. I'd not have a childContext
though, only give the current context it introduces less complexity and the use cases are often not good ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The various methods (extension points) are needed in order to implement the cache logic proposed under #5650 in userland.
In order to implement this logic, the tag collector service on user side would look something like below (no guarantee for correctness, typing in a text editor only).
<?php
class CustomTagCollector implements TagCollectorInterface
{
public function collectTagsFromNormalize(mixed $object, string $format = null, array $context = [], string $iri = null): void
{
$this->addCacheTagForRessource($context, $iri);
}
public function collectTagsFromNormalizeRelation(mixed $object, string $format = null, array $context = [], string $iri = null): void
{
}
public function collectTagsFromGetAttribute(mixed $object, string $format = null, array $context = [], string $iri = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null, array $childContext = []): void
{
$this->addCacheTagsForRelation(array $context, $propertyMetadata)
}
private function addCacheTagForRessource(array $context, ?string $iri): void
{
if (isset($context['resources']) && isset($iri)) {
$context['resources'][$iri] = $iri;
}
}
private function addCacheTagsForRelation(array $context, ApiProperty $propertyMetadata): void
{
// Add cache tag for related collection
if (isset($context['resources'])) {
if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) {
foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) {
$cacheTag = $context['iri'].self::IRI_RELATION_DELIMITER.$dependency;
$context['resources'][$cacheTag] = $cacheTag;
}
} else {
$cacheTag = $context['iri'].self::IRI_RELATION_DELIMITER.$context['api_attribute'];
$context['resources'][$cacheTag] = $cacheTag;
}
}
}
}
If there's only one collect
method, I'd need to figure out from which point in the normalization process the collect method was called. Might be possible based on the content of the attributes, but not sure whether this leads to robust and understandable code.
With regards to parameters, in order to implement the original behavior of api-platform as well as the logic from #5650 I'd need the following parameters as a minimum: $context
, $iri
, $propertyMetadata
All the rest ($object
, $format
, $childContext
, $attribute
, $type
) can also be removed and we could add in case there's a specific usecase coming up for it. $type
would be needed though, if anyone wants to differentiate relations to collections (*ToMany) and *ToOne relations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking at the $context['operation'] you should be able to find out where you're at. I'd really like to see what code you need to handle these. Understand that if we add an interface we really need to work on it because it'll last a while after publishing it and we did the mistake in the past, making an interface evolve with our current BC layer is quite hard afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No prob, fully understand design of the interface is crucial.
For implementation of the current api-platform logic, I could use $attribute
to ignore calls coming from getAttribute
, because this parameter is not sent from normalize
or normalizeRelation
. I cannot use $context['api_attribute']
, because this is set for nested/embedded objects also for calls from normalize
or normalizeRelation
.
class TagCollector implements TagCollectorInterface
{
public function collect(mixed $object, string $format = null, array $context = [], string $iri = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null, array $childContext = []): void
{
if(!$attribute){
$this->addResourceToContext($context, $iri);
}
}
private function addResourceToContext(array $context, ?string $iri): void
{
if (isset($context['resources']) && isset($iri)) {
$context['resources'][$iri] = $iri;
}
}
}
Implementing the logic above for #5650 is a bit more tricky. With the current code, I don't see any good means to differentiate between calls from normalize
and normalizeRelation
. I could add an additional string attribute to disclose, where the collect call was made from. But honestly, that seems a bit like a disguised event system and I'd rather have a proper event system implemented as in the initial PR #5753.
Alternatively I think I could add the normalized $data
back as an attribute and then check within collect, if data is an object (fully normalized embedded resource) or only a string (resource normalized as IRI only).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to find out whether the call is made in a normalizeRelation
as opposed to a normalize
? you have api_sub_level
for that IIRC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to find out whether the call is made in a
normalizeRelation
as opposed to anormalize
?
Yeah, correct. #5650 gives more background on the motivation. But in a nutshell, I'd want to treat fully embedded resources (resource embedded in the response as object) differently from linked resources (only IRI of resource included in response).
For the former, the cache tag is triggered from normalize
. For latter, the cache tag is triggered from normalizeRelation
(at least that's how it's implemented now).
you have
api_sub_level
for that IIRC.
Not sure, this works. When running tags.feature tests, api_sub_level
seems always to be true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively I think I could add the normalized
$data
back as an attribute and then check within collect, if data is an object (fully normalized embedded resource) or only a string (resource normalized as IRI only).
I just pushed a proposal implementing this. When passing $data
, I can check on userland if $data
is an array/object or a string (IRI) only and hence differentiate embedded objects from linked IRIs.
For JsonAPI this is a bit "trickier", because even for linked data, the return value is an array and not only a string. However, as we pass $format
as well, this could still be differentiated from a normal fully embedded object.
I had to change the tests, because now the order of tags has changed when waiting for the normalized data. The tags are still the same though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please resolve #5758 (comment) so that we don't change any tests
src/HttpCache/TagCollector.php
Outdated
*/ | ||
class TagCollector implements TagCollectorInterface | ||
{ | ||
public function collect(mixed $object = null, string $format = null, array $context = [], string $iri = null, mixed $data = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're closed to something nice, a few things still bothers me. Do we really need data
and object
? I think that this works using only object
.
If I think about an IriCollector for cache tags, I don't see the format in its signature, we can put this inside our $context no? I also think that $propertyMetadata
alone is fine as it should hold the $type
information. So my signature looks like this:
public function collect(mixed $object = null, string $iri = null, array $context = [], string $attribute = null, ApiProperty $propertyMetadata = null): void
Last but not least, I also think that the iri
can actually be computed by the tag collector if needed, so we'd end up with something like this ($context['iri']
should be defined for performances if available):
public function collect(mixed $object = null, array $context = [], string $attribute = null, ApiProperty $propertyMetadata = null): void
Not even sure if we couldn't remove $attribute
and $propertyMetadata
, are these really useful?
You could probably add your use case as functional test so that I see how you use it? You can add your service in our config_common.yml
inside the TestBundle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could probably add your use case as functional test so that I see how you use it? You can add your service in our
config_common.yml
inside the TestBundle.
I tried to integrate this into our own project yesterday. That might help a bit to evaluate some of the use cases:
https://github.com/ecamp/ecamp3/pull/3610/files#diff-92df153f2ab4331ccfd55e92699e78c72a3223ff10a38cb870ad59f769308fe0
Can also try to integrate some of this into the test suit.
For your other comments, will look at them and respond later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a functional test with commit a404dd7
Adding the custom TagCollector to config_common.yml
obviously breaks the existing tests in features/http_cache/tags.feature
. Not sure what the best way is to isolate this test and couldn't find a good example to copy from. Do I need to set up a separate behat suite which only adds the tag collector service?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#5758 (comment) should resolve the tests issue no?
*/ | ||
interface TagCollectorInterface | ||
{ | ||
public function collect(mixed $object = null, string $format = null, array $context = [], string $iri = null, mixed $data = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to find time to improve that interface, your tests helps a lot thanks!
I was struggling a bit to ensure Behat is using the custom tag collector service only for a subset of the tests. The best I could come up with is the latest commit: 889a614 It seems not that easy to override a service for a specific Behat scenario. It kind of works, when the kernel is not rebooted between scenarios (there's a whole discussion on this under FriendsOfBehat/SymfonyExtension#149). With this latest commit, it can at least switch between The caveat of this is that the old tests had to be adjusted slightly. @soyuka Do you mind reviewing this one more time. Do you know of any better method to enable/disable services for specific scenarios? |
{ | ||
$this->disableReboot($scope); | ||
$this->driverContainer->set('api_platform.http_cache.tag_collector', new TagCollectorCustom()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is quite awesome I wasn't aware we could do that
I like this, if you could:
Also about this interface, I suggest to reduce it to: interface TagCollectorInterface
{
public function collect(string $iri, array $context = []): void; Can I definitely think that all these can go to our TagCollector context:
I'm also okay to add an immutable option object if we don't want an array as $context, but all these are noisy in the function signature no? LMK what you think |
I will give a try to push everything into $context. Would you also accept the PR against 3.2 instead of main? Would be nice to start using it right away, as it kind of blocks anything around caching on our application side. |
889a614
to
159245f
Compare
@soyuka I pushed all the parameters into $context, so nothing else needed for the function signature. Let me know if you believe this looks clean. I was a bit woried about any side effects at different places, when touching the $context. But tests seem successful. |
hey @soyuka, just wanted to check if you had the chance to look into this? |
I was quite busy with the SymfonyCon + related I'm looking at this asap. It'll be merged onto |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice, this will be a quite nice extension point thanks @usu !! some last few changes and please target the main branch :).
|
||
public function collect(array $context = []): void | ||
{ | ||
$iri = $context['iri']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IRI and object can be null here no? (we probably don't care as it's a test fixture) I'd probably add a comment for readers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, iri
and object
could potentially both be null. I think this should be clearer now with the interface signature documentation.
In this specific implementation (TagCollectorCustom
), I'm checking further below for this (e.g. isset($iri)
).
0116a0d
to
140e796
Compare
c1d7196
to
2428cac
Compare
2428cac
to
2e2c2c2
Compare
All implemented as per comments and rebased against main. MongoDB tests and cs-fixer is failing. But doesn't seem related to any code I touched. Also, I had to disable the new tests |
That would actually be really nice, then we could already start test & integration with our own application. 😃 |
…rm#5758) * feat(serializer): collect cache tags using a TagCollector * simplify function signature * fix bug in JsonApi normalizer * minor changes as per latest review * disable new tests for Symfony lowest * cs --------- Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
@usu would it be possible to document this feature? BTW I released an alpha version today |
Sure. I can add a chapter to https://github.com/api-platform/docs/blob/main/core/performance.md. |
@usu I released would love some docs if you can |
Just opened a PR at #6348. |
Alternative implementation of #5753 but using a service instead of events