Title says it all. There are increasingly many issues being reported against jsonapi_extras that are caused by this incompatibility: #3003023: TypeError: Argument 4 passed to Drupal\jsonapi_extras\... + #2994885: JSONAPI Version Requirements prevent installation via Composer + …
Let's get that fixed.
| Comment | File | Size | Author |
|---|---|---|---|
| #81 | on-commit.txt | 2.84 KB | e0ipso |
| #72 | 3004582-73.patch | 37.75 KB | wim leers |
| #72 | interdiff.txt | 1.62 KB | wim leers |
| #70 | 3004582-70.patch | 37.63 KB | wim leers |
| #70 | interdiff.txt | 427 bytes | wim leers |
Comments
Comment #2
wim leersIf one installs
jsonapi1.23 andjsonapi_extras2.9, then updates from 1.23 to 2.0-beta2 and runscore/rebuild.php, one sees:Let's look at that warning first. #2995111: shouldBeInternalResourceType et al. should receive the resource type, not the entity type added a new
$bundleparameter toisMutableResourceType()(specifically forjsonapi_extras!), butjsonapi_extraswas not updated accordingly in #2992557: Inject additional services manually to ConfigurableResourceType.Easy fix. Compatible with both
jsonapi1.x & 2.x.Comment #3
wim leersNow we're down to
When running
/core/rebuild.php.This one is a bit trickier. #2984886: Trigger route rebuild when new bundles/fields are added/removed added a new parameter to
ResourceTypeRepositoryinjsonapi's 2.x branch.jsonapi_extras'ConfigurableResourceTypeRepository, which decorates that same class, must therefore also pass this parameter.Fortunately,
jsonapi1.x only expects 3 parameters, and will continue to do so. We have two possible solutions here:jsonapi_extras' overrideConfigurableResourceTypeRepositoryneeds be injected via setters rather than via the constructor.ConfigurableResourceTypeRepositorywill need to implement all methods required by the interface. It already implements one, but it would then have to start implementing the other two too. This would also require injecting all services thatConfigurableResourceTypeRepositoryuses, which includes many of those inResourceTypeRepository. But worse, it'd require mapping everyResourceTypeobject to aConfigurableResourceTypeobject.The first option is vastly simpler, even though the second may arguably be better in the long term. I also tried the second approach, but it'd require massive code changes. Perhaps we want to do it in a follow-up issue. But I think we prefer the minimal set of changes to get to a cross-compatible
jsonapi_extrasmodule? :)So, here's that first approach.
Comment #4
wim leersStill passing on JSON API 1.x ✅
On 2.x, it's now failing with this output:
#2977669: Spec Compliance: some entity types have an "id", "type" or "uuid" field, this violates the spec causes this failure, because it made this change:
That was added in #2977669-69: Spec Compliance: some entity types have an "id", "type" or "uuid" field, this violates the spec. This was a bugfix to ensure that relatable resource types are discovered using their public name (the "alias") rather than the internal name. Without this, JSON API Extras also didn't work correctly!
This means that
\Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType::getPublicName()gets called earlier than it did before. If we look at\Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository::all(), then:Because of this,
ConfigurableResourceTypeobjects do not yet have the additional services injected and hence fail.Of course, ideally
jsonapi_extraswould use the built-in aliasing capabilities provided byjsonapisince #2977669: Spec Compliance: some entity types have an "id", "type" or "uuid" field, this violates the spec rather than layering on its own, as proposed in #2986408: Use aliasing capabilities provided by JSON API 2.x, but doing that would not be compatible with JSON API 1.x.Solution: still TBD.
Comment #5
wim leersSo, let's say we comment out that early invocation of
ConfigurableResourceType::getPublicName:Where do we get then?
Well … something that is literally impossible to solve:
You see, #2936754: Avoid using the Serialization component for JSON API specific tasks explicitly started disallowing custom normalizers:
I don't see any way we can fix that. @e0ipso +1'd this at #2936754-26: Avoid using the Serialization component for JSON API specific tasks, but in #32 of that issue, he writes:
I … am not sure what this means. @e0ipso, can you explain what you meant?
Comment #6
gabesullice@Wim Leers, re: #5, I just found a way to do something similar here: https://cgit.drupalcode.org/jsonapi_hypermedia/commit/?id=a4477cf5af177c...
Since the JSON API serializer "falls back" to the regular serialization system, I think we can play a little trick. If we use a
CompilerPass, we can remove the JSON API normalizers that we want to override, then, we'll set a very high priority on the Extras normalizer and register them into the default serializer using the tagnormalizerinstead ofjsonapi_normalizer_do_not_use_removal_imminent.Then when normalization happens, the JSON API serializer will find no applicable normalizer and "fall back" to the Extras normalizers.
Now, here's the tricky bit... once we "fall back", the serializer that is set inside the Extras normalizers will no longer the JSON API one, it'll be the JSON API one. Meaning that we've completely exited the world of JSON API normalization. That happens because the normalizers all have
SerializerAwareTraiton them. When those normalizers are added to a serializer, the serializer calls$normalizer->setSerializer($this);on each one of them.Why is that a problem? Because JSON API Extras overrides JSON API's EntityNormalizer, which calls
$this->serializer->normalizer($field). If$this->serializerisn't the JSON API serializer, we'll have a broken system.What we need to do, somehow, is make sure that instead of the standard serializer being set on the Extras normalizers, the JSON API serializer is set. I think we can do this by overriding the
setSerializer()method. Hopefully, we can just turn it into a no-op method and set$this->serializerin the constructor. That might cause a loop in the DI container though, in which case we'll need to probably do something even uglier.Comment #7
wim leersThis … doesn't sound very reliable and definitely not easy to understand/maintain. I do very much appreciate that you're trying to help to move this along though :)
Comment #8
e0ipsoWhat I meant is that we may need to allow some extensibility from within JSON API… or we can come up with a creative/clever solution.
@gabesullice shows a possible path in #6. Another idea is to provide a class map in
composer.jsonthat will allow us to have classes namespaced by\Drupal\jsonapi\Normalizersinside the JSON API Extras module. That'd be a succinct solution, although like #6 it violates the principle of least astonishment. I'm not sure we can have a creative/clever solution without being astonishing.The other possible path is to find a different solution to #2936754: Avoid using the Serialization component for JSON API specific tasks that allows us to add these normalizers.
Comment #9
gabesulliceI completely agree. But it is a solution and a step forward from being something that "is literally impossible to solve."
Unfortunately, I think that is a right solution here. Not as long as the existing JSON API serialization system is as complex as it is and as long as we feel unsafe opening up for extension.
I think my proposal is a stop-gap and we can solve this in the right way when we refactor or replace the JSON API serialization system.
Comment #10
wim leersWOAH
😂
I was personally looking into how we could do the work we want to do in a different phase. I was looking at
\Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber::renderResponseBody(), whrere we're dealing with aJsonApiDocumentTopLevelNormalizerValueinstance. That feels like it could be the place where we can reliably do this. I don't know how yet exactly though.Comment #11
gabesulliceI went pretty far down this path in my first attempt within
jsonapi_hypermedia. I can't say it's impossible, but I can say that I was not able to make it work in a few hours of tinkering.Comment #12
wim leersIt is!
But before embarking on that path I'd just want all three of us to have spent a day or two pondering the problem. See my suggestion in #10 for a different direction (although unlike your direction, it doesn't have a concrete working proposal yet).
Comment #13
e0ipsoAlso worth noting, that if normalizers were tracking schema, then I'd feel much better on allowing others to add normalizers for JSON API.
I've been wanting to take a bite to that issue for a long time.
Comment #14
gabesulliceI think this will lead to a lot of new code that we'll need to write for JSON API Extras. That seems out of the scope here, which is just getting Extras to be compatible so we can release an JSON API 2.x RC.
I'll say this: I do agree that that would be a very clean place to inject something. I might even go a step further and say that Extras could simply have its own
KernelEvents::RESPONSEsubscriber to be completely separated from JSON API.In essence, your suggestion will force JSON API Extras to work only on the "rasterized" array representing the entire JSON API document. IOW, Extras will need to operate on big, multi-dimensional arrays rather than just the typed objects where it has overridden things in a targeted way (i.e. the EntityNormalizer is called once for every entity). OTOH, working with "reshaping" arrays is exactly how @e0ipso's shaper library works, so maybe that'd be okay... It seems like a lot to tackle though.
I don't believe this issue is the place to start down the road of such a large re-architecture.
I will definitely keep pondering :P
Comment #15
gabesulliceFWIW,
\Drupal\jsonapi_extras\Normalizer\FieldItemNormalizeralready overridessetSerializer, which was the hairiest part of my #6 proposal.Comment #16
e0ipsoThanks for the awesome job you are doing here!
I think this makes it clear that we want to have a dedicated branch
jsonapi_extras-3.xfor compatibility withjsonapi-2.x. Unless anyone disagrees we can move forward with that assumption. @Wim Leers thanks for finding the exact thing that makes us do the thing we suspected we'd have to do.Comment #17
wim leers#14:
That is one possibility. The other possibility is that
jsonapi_extrasruns its own response subscriber beforejsonapi's, so it can modify theJsonApiDocumentTopLevelobject before it gets normalized into aJsonApiDocumentTopLevelNormalizerValueobject by\Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber::renderResponseBody(). That seems simpler/more reliable forjsonapi_extras.#16:
I don't think it's a hard requirement, but I do think it'll make
jsonapi_extrasa lot easier to maintain. Because #2977669: Spec Compliance: some entity types have an "id", "type" or "uuid" field, this violates the spec effectively moved a significant portion of the infrastructure thatjsonapi_extrasneeds intojsonapiitself, which means it has to do far less work!So yes, I personally do think it's worth creating a new branch of
jsonapi_extras, because it will make it a lot easier to maintain.Comment #18
wim leersBeen working on this all day. Update later tonight, and if not tonight, then tomorrow.
Comment #19
e0ipsoExciting!
Comment #20
wim leersI had something workable for solving #5's first problem earlier today, but it was 2.x-only. I could make it work with 1.x too, but then we'd need 1.x to change, which then still means you need to update more things, which still means no nice smooth update path.
I now managed to get something to work that allows
jsonapi_extras2.x to continue to work with any existing 1.x site, and start working with 2.x. It will require a small change in JSON API 2.x. Super small. Super trivial. And it'll allow JSON API Extras to become simpler in the future, when it drops 1.x support.I did not yet solve the normalizer problem though. Still, exciting to see that I might be able to not require a new branch!
Need to call it a day now though. More tomorrow!
Comment #21
wim leersWe can solve the first problem described in #5 by making it possible for
ConfigurableResourceTypeRepository(JSON API Extras' override) to create the "correct"ResourceTypevalue objects the first time, rather than relying on decoration first.To do that, we also need to provide a
$field_mappingparameter, which the big hunk in #2986408-2: Use aliasing capabilities provided by JSON API 2.x gets us.Now, the early call to
$resource_type->getPublicName()no longer causes a problem :)Best of all: I can make this work on both 1.x and 2.x if we either apply the attached
jsonapi_*patches to the respective branches, or if we keep the 2.x detection logic in this interdiff. Keeping the 2.x detection logic is preferable because it significantly simplifies the update path for sites on JSON API 1.x today: they can first update JSON API Extras and then update JSON API from 1.x to 2.x at any time of their choosing.Compatible with
1.xand2.x.Comment #22
wim leersNew problem (on 2.x):
Because as of #2995111: shouldBeInternalResourceType et al. should receive the resource type, not the entity type, the
(Configurable)ResourceTypeobjects are being stored in a static cache. Doing this results in the services injected intoConfigurableResourceTypeto be serialized as well. (Because it's not a true value object anymore due to those services.)use DependencySerializationTrait;to the rescue!Comment #23
wim leersSeveral methods can be removed when only 2.x is supported by
jsonapi_extras. But that's explicitly not the goal of this issue!Still, while we're working on this, we should add
@todos to mark those things for removal to make things simpler in the future. It also gives us a sense of how much simpler things become oncejsonapi_extrasonly supportsjsonapi2.x.Comment #24
wim leersOne method already was 2.x-only, and is no longer necessary in a world where there is a
::createResourcetype(). :) Slight simplification, yay!Comment #25
wim leersMy
\Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber::renderResponseBody()-based proposal for solving the remaining problem (normalizers) actually only solves half the problem: it only works for normalization, not denormalization.So, I started to implement the work-around @gabesullice proposed in #6. One thing he didn't foresee is that it's actually not possible to remove the
serializer.normalizer.field_item.jsonapiserializer, becauseserializer.normalizer.field_item.jsonapi_extrasdecorates it! And similarly, we need JSON API Extras' subclasses to pass all necessary parameters to the other two overridden normalizers, which means they need to be inherited to be able to work on both 1.x and 2.x. That means that rather than removing, we can make them private untagged services.His other key prediction, about us somehow needing to make sure JSON API Extras'
EntityNormalizeruses JSON API's serializer did come true though. I solved that by inheritingjsonapi'sFieldItemListnormalizer into the core's serializer, with a similarly high priority. This means it gets selected in lieu of core'sListNormalizer, and hence keeps things working fine.Best of all: while this approach is necessary for
jsonapi2.x, it also works fine for 1.x. So we don't need conditional logic between 1.x and 2.x here!Comment #27
wim leersFinally: since #2955615: Field properties are not being denormalized, the signature of the protected
::prepareInput()changed. It makes sense to not have warnings on 2.x, especially because it actually uses that additional data. There can be warnings on JSON API 1.x, but they're harmless.Comment #29
wim leersFix CS violations.
Comment #30
wim leersI need to run now. Sadly, with this patch still red.
I never ran these tests locally, I instead actually used JSON API, both 1.x and 2.x, with the following JSON API Extras customizations turned on:
And it works!
It'd be great if somebody else could fix those last few failures. I'd LOVE to see this committed in the next few days and hopefully even today!
Comment #31
wim leersForgot to mention: the
jsonapipatches in #21 have ajsonapiissue: #3006270: Add ResourceTypeRepository::createResourceType() for easier JSON API Extras support and simpler code.Comment #32
wim leersCould it be this simple? This appears to satisfy PHP on both JSON API 1.x and 2.x.
Comment #34
gabesullice#3006270: Add ResourceTypeRepository::createResourceType() for easier JSON API Extras support and simpler code landed.
Comment #35
wim leersThe remaining failures in
EntityToJsonApiTestare legitimate. That test was introduced in [#29842455].Ever since #2971040: PHP 7.1 Compatibility Warning, in the JSON API 2.x branch, the
JsonApiDocumentTopLevelclass was moved to be compatible with PHP 7.2. So we need to conditionally use the correct class. That's easy enough.But on top of that, the constructor of the
JsonApiDocumentTopLevelclass evolved in the 2.x branch: it now requires more parameters.Comment #37
wim leersAfter fixing that, turns out that the changes so far are somehow causing relationships to be normalized differently (incorrectly). It took some debugging, but then in hindsight it was obvious: increasing the service priorities in #25 cause
\Drupal\jsonapi\Normalizer\FieldNormalizerto be picked instead of\Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizerfor entity reference fields. I was able to fix that by also decoratingserializer.normalizer.entity_reference_field.jsonapi. But then I had to also decorateserializer.normalizer.entity_reference_item.jsonapi. And more and more …So, time to go back to @gabesullice's original proposal:
Now at least
EntityToJsonApiTestis passing!Comment #39
wim leersThe test run confirms this.
Next up: a circular reference to be solved. Symfony's error message is (as usual) utterly useless: it says
But the circle is triggered like so:
jsonapi.request_handler -> serializer -> serializer.normalizer.entity.jsonapi_extras -> jsonapi.serializer_do_not_use_removal_imminent -> serializeri.e. theserializer.normalizer.entity.jsonapi_extrasis created byserializerbut also has that as one of its dependencies.So: time for something even uglier! Hopefully @gabesullice can do that while I sleep, otherwise I'll do it tomorrow.
Comment #40
wim leersOn this again.
Comment #41
wim leersFixing the CS violations is easy of course, but the circular dependency will require some more thought, tinkering and cursing.
Comment #43
e0ipsoThanks for the hard battle you're fighting on this @Wim Leers!
Comment #44
wim leers✊
Comment #45
wim leersI managed to find a work-around :)
Now all tests should pass on
jsonapi1.x.Comment #47
wim leersFixed the failure in
EntityToJsonApiTest.And gahhhhhhhh 😭
JsonExtrasApiFunctionalTest::testWrite()is passing fine locally! Added debug output.Comment #48
wim leersFixed CS violations and refactored the decorator to be less sensitive to typos.
Comment #51
wim leersLooks like the refactoring of the decorator in #48 actually fixed it. That also explains why I couldn't reproduce it locally!
Removing debug output and fixing newly introduced CS violations.
🤞
Comment #52
wim leersD'oh.
Comment #53
wim leersFINALLY!
Now, let's see what happens when running this against JSON API 2.x. I think this is how I can get it to run against 2.x, let's see …
Comment #55
wim leersIt's using
2.0-beta2, but we need it to install54f560dea96c1cdd32165e23639cb19bc588134f, which includes #3006270: Add ResourceTypeRepository::createResourceType() for easier JSON API Extras support and simpler code.Comment #57
wim leersAARGH JSON …
Rather than me being able to come back after lunch to a useful test run, now I need to wait again. 😡
Comment #59
wim leersFinally, a test run of JSON API Extras against JSON API 2.x!
Per https://www.drupal.org/node/2982678, we need to make the datetime enhancer a bit smarter.
Comment #61
wim leersStill same failure count, but failing later:
That's happening here:
Which passes on 1.x but not on 2.x. Because #2953346: Define related/relationship routes per field, not dynamically (with route parameters that need validating) changed how this is handled: the
relatedroute is still not created in case all target resource types are internal, but therelationshiproute is created, meaning this now results in a 200 response with'data' : nullin the response document. The 2.x branch does this differently since #2953346, specifically since #2953346-26: Define related/relationship routes per field, not dynamically (with route parameters that need validating). This change was not explicitly discussed. If we want to reconsider this, we can open ajsonapiissue. For now, I'm just adding a 2.x-specific test expectation.Comment #63
wim leersLet's look at the other failures, those in
Drupal\Tests\jsonapi_extras\Kernel\Controller\EntityResourceTest.They're easy to fix by adding 2.x and 1.x logic.
Comment #64
wim leersLooks like I made a c/p error in #61.
Comment #67
wim leersLast failure!
If we look at the actual response, we get:
Note how there are zero destination resource types:
()!Turns out this is because ever since JSON API 2.x gained a "field mapping" (for aliasing and disabling fields), we did not correctly update the relationship normalizer accordingly. AFAICT I introduced this in #2977669: Spec Compliance: some entity types have an "id", "type" or "uuid" field, this violates the spec. That issue resulted in far less probability of JSON API Extras breaking wrt disabling/aliasing of fields, because JSON API itself now does this. But in the case of JSON API, the fields we happen to alias automatically are all relationships to the corresponding bundle. And that just happens to be the one field that is in fact optional in a POST request. In other words: we only gained normalization test coverage in JSON API, not denormalization. The one case where it is now failing is in denormalization of relationships! i.e. when you
POSTorPATCHaRESOURCETYPE/relationships/RELATIONSHIPNAMEroute.Since you cannot change a resource's bundle, and the bundle field is the only relationship that is automatically aliased by
jsonapi, we'll needjsonapito create a regression test for this.Comment #68
wim leersCreated #3007113: Follow-up for #2977669: denormalizing aliased relationships fails to fix #67.
Comment #69
wim leers#3007113: Follow-up for #2977669: denormalizing aliased relationships fails landed! 🤘
Reuploading #64's patch to retest.
Comment #70
wim leers#69 is green against
jsonapi2.x.#51 was green against
jsonapi1.x, and so will this patch.This is now in @gabesullice's hands for review, and especially in @e0ipso's!
Comment #71
e0ipsoVery close already!
This is brittle, but I suspect we'll have to live with it.
Maybe a copy & paste leftover from
normalize()? I don't see$referenced_entitiesbeing used in the method in this diff.Everything in here is very clever. I can see that this took a long time to brew.
Great work!
Thanks for this. It will be incredibly useful some day in the future.
Please elaborate on the comment. I wondered Mostly the same as what?
I don't feel great about this pattern. I think it adds some unnecessary magic. However I'm ready to buy it if you tell me how is this better over:
Comment #72
wim leersI don't feel strongly about this though — I'm happy to do what you describe. That's what I was originally doing in #47 too.
Comment #73
e0ipso.
This resembles a lot to
__call, but we cannot use__callbecause then we don't comply with the interfaces without Reflection conjurations. I have a preference for explicitness, but I see your point about typos.I'm OK with leaving it as is.
Comment #74
wim leersIndeed.
I share that preference. But above anything else, I care about correctness and reliability. This is explicitly (without crazy hacks) relaying a call to another object. I think this is still equally explicit, at the cost of requiring to understand two relatively advanced PHP features:
call_user_func_array()andfunc_get_args().So then … what remains to be done for this to land? :)
I'm perfectly fine with you still preferring not using
call_user_func_array()andfunc_get_args(). Your decision.Comment #75
e0ipsoI'm ready to merge this. I'm just giving it time for @gabesullice to wake up and give it a quick look before merging.
Comment #76
wim leers👍
Comment #77
gabesulliceAh, I was wondering how you would handle that. Looks good.
"Format from which the given data was extracted."
Can be fixed on commit.
Nit: I think 1 line would be more readable.
Let's make a followup to ensure that this will be true even after JSON API is in core but 1.x is installed in contrib.
LGTM. I don't think anything above should block a merge, the things I've mentioned can be fixed on commit or done in a followup.
Comment #78
wim leers🎉
Comment #79
gabesulliceFollowup created: #3007341: Followup to #3004582: Ensure JSON API 1.x/2.x detection is correct, even with 2.x in core.
Comment #81
e0ipsoFixed.
Multiple commit credits for @Wim Leers. You rock.
I made some changes on commit.
Comment #82
wim leersWoot! Also great to see https://www.drupal.org/project/jsonapi_extras/releases/8.x-2.10 already released. Thank you, @e0ipso! :)
Comment #83
ndobromirov commentedSuper!
Will be testing this one soon along with jsonapi 2.x :).
We are struggling with single node views with many relationships and includes.
This issue was a blocker for the migration to the new version.
Comment #84
wim leersExciting! :D
Note that you cannot use 2.0-beta2, you need the version after that (not yet released), or the latest dev snapshot. Because #3006270: Add ResourceTypeRepository::createResourceType() for easier JSON API Extras support and simpler code is needed.
Comment #85
stevieb commentedI tried installing JSON API Extras 8.x-2.10 & 8.x-2.x-dev using composer both failed
with the following releases JSON API 8.x-2.0-rc1 & 8.x-2.x-dev
the composer output says only JSON API 8.x-1/dev are compattable
Comment #86
wim leersProbably due to to
"drupal/jsonapi": "^1.22",then.@stevieb, could you open a new issue for this? Thanks!
Comment #87
ndobromirov commentedThere is one: #2994885: JSONAPI Version Requirements prevent installation via Composer
Comment #88
wim leersAh, right, good point! Updated the patch there: #2994885-12: JSONAPI Version Requirements prevent installation via Composer.