Problem/Motivation
JSON:API has \Drupal\jsonapi\ResourceType\ResourceTypeBuildEvent
to allow fields to be aliased, fields to be disabled and resource types to be disabled. Those were the three first public PHP APIs for the jsonapi
module.
A logical next one would be to allow resource types to be aliased.
Related: @bojanz in #3032787-21: [META] Start creating the public PHP API of the JSON:API module.
Proposed resolution
Allow entity types to declare a JSON:API resource type name using ajsonapi_resource_type_name
entity type annotation key.- Add a new method to
\Drupal\jsonapi\ResourceType\ResourceTypeBuildEvent
.
(Option 2 is preferred by @e0ipso, @gabesullice and @Wim Leers because it solved more problems: it allows any module to override it. So: more power, more consistency, smaller API surface.)
Remaining tasks
User interface changes
None.
API changes
A new method on \Drupal\jsonapi\ResourceType\ResourceTypeBuildEvent
.
Data model changes
None.
Release notes snippet
TBD
Comment | File | Size | Author |
---|---|---|---|
#85 | 3105318-85.patch | 18.88 KB | bbrala |
#83 | 3105318-83.patch | 19.74 KB | bbrala |
#83 | interdiff-82-83.txt | 779 bytes | bbrala |
#82 | interdiff_77-82.txt | 1.41 KB | ravi.shankar |
#82 | 3105318-82.patch | 18.83 KB | ravi.shankar |
Comments
Comment #2
Wim LeersComment #3
Wim LeersSomething like this? I'm not sure if the "public" vs "internal" distinction that we have for fields makes sense here too. For now, going with it.
(For fields, we have to have a mapping to the internal aka actual/storage field name. For resource types, we may not need this, although much if not all of the infrastructure does assume the entity type and bundle can be parsed from the JSON:API resource type name. So perhaps we need it there too.)
Comment #7
Wim LeersCrediting the people who participated in the Slack discussion that led to this.
Comment #8
gabesulliceWe do not need it. The infrastructure does not assume that, at least not on purpose. We have
ResourceType::getEntityTypeId()
andResourceType::getBundle()
for that. That's because JSON:API Extras has always allowed resource types to be renamed. Parsing the resource type name would be a bug IMO.Comment #9
Wim Leers👍 Perfect. That's what I thought at first, but upon cursory scan I wasn't so sure anymore. Thanks for confirming!
So, then shall we go with just
setResourceTypeName()
?This will also need test coverage, it can be added to
\Drupal\Tests\jsonapi\Kernel\ResourceType\ResourceTypeRepositoryTest
.Comment #10
gabesulliceWorks for me!
Comment #11
mglamanRenamed the method per #9 and #10. Added a test method to ResourceTypeRepositoryTest.
Comment #12
mglamanThis doesn't work.
\Drupal\jsonapi\ResourceType\ResourceType::__construct
still builds the typeName based off of the entity type ID and bundle. We'd need to modify the constructor to accept the resource type name. Or a method for overriding it.Comment #13
Wim Leers#12: that's what I feared in #3 and #9.
Comment #14
mglamanHere's a stab at changing the constructor and test changes. We'll see what we do.
Comment #15
mglamanI just realized this doesn't entirely solve our problem for the Commerce API.
The resource path is not built off of the resource type name. So you're going to have a
product--default
type available at/commerce_product/default
.Comment #16
mglamanI don't know if this is scope creep, but I feel like the path should reflect the resource type name as it currently does. So I've changed the path output to
Also:
Whoops. I couldn't run tests locally without modifying this. Didn't mean to patch.
Comment #18
Wim LeersAgreed.
Comment #19
mglamanSo I ran into a side effect here. There is no way to retrieve an aliased resource type for an entity type. Given an entity type ID you have to know how it was aliased so you can still run `\Drupal\jsonapi\ResourceType\ResourceTypeRepository::get`.
`get()` does the following:
Currently, as a workaround, I am using something like this:
Comment #20
mglamanOh, this totally breaks everything, like related fields
\Drupal\jsonapi\ResourceType\ResourceTypeRepository::getRelatableResourceTypesFromFieldDefinition
I think aliasing resource type names is going to be near impossible.
For Commerce, all we want is to change the paths.We want to control `type` and paths :/Comment #21
mglamanIt looks like some of the blockers are due to reverts in resource-type matching logic from #3018287: ResourceTypeRepository computes ResourceType value objects on *every request*
Comment #22
mglamanThis reverts changes in ResourceTypeRepository caused by #3018287 that break renamed resource types. It should cause this patch to pass. It also means JSON:API Extras is probably broken in 8.8.x as well if you rename resource types.
Comment #24
mglamanEdit: not a problem, I messed up my assertion code. 🤦♂️
Comment #25
gabesulliceIf you move
$type_name
to the end, we can skip the whole BC song and dance. However, if we do want to do that song and dance, let's go all-in. Make it the first argument so that$entity_type_id
and$bundle
can more easily be made optional in the future.... Actually, thinking about it more. Let's do that. Then we can move all the
$this->typeName
logic out of the constructor into the repository where you have$type_name = NULL;
.I like this reformulation. It gets us further away from entities.
Let's convert this to an
array_reduce
and simply drop theNULL
values. This is a bug waiting to happen (it's probably already there).Nit:
s/renamingResourceTypes/renameResourceType
Comment #26
gabesulliceI was pretty sure that this was probably a bug and I had a vague feeling that I had realized it before. I tracked that feeling down to this comment #3034786-37: ResourceIdentifier::getVirtualOrMissingResourceIdentifier() ignores field aliases; causes $relatable_resource_types field to be empty and results in an error.
I think we can/should just fix it here for efficiency's sake.
Edit: Actually, it's entirely appropriate to fix that bug here. The bug could only have been encountered if one were using JSON:API Extras. JSON:API core has no official way to alias resource types in JSON:API so, in theory, the bug doesn't exist and there was no good way to write a test for it. (JSON:API Extras does some tricky things to alias resource types and it wasn't ever "officially" supported)
Since this issue is all about creating a public, official API to alias resource types, it makes complete sense that the bug should be prevented in this issue.
Comment #27
xjmComment #28
mglamanPicking this back up
Comment #29
mglaman{$entity_type_id}--{$bundle}
array_reduce
would go through every resource type, even after we have determined the proper one. Or I'm not following the suggestion.#26 (follow up to 25.3) so we should do an
array_filter
to remove any keys fromgetRelatableResourceTypesFromFieldDefinition
which may be null? I don't see the gains in array_reduce, as we know it should exit once we find the first (and only) match.Comment #31
mglamanThis should address the test failures.
Comment #33
mglamanThis should fix the remaining tests – forgot to persist "unknown" as the type name for dangling references. Also the decorated ResourceTypeRepository in jsonapi_test_collection_count needed to be updated.
Comment #35
mglamanFailures were completely unrelated, queued the tests again and it passed.
Comment #36
bbralaAwesome patch, would love to see this in core. I went through the code and I have one concern. As mentioned in our discussion in slack about this patch.
When looking at this code i'm worried about the performance implications if removing the lookup table for finding the correct resources.
This would mean when serving a request with a set of resources the repository will loop through all resources for every access check for the set. This would possibly be quite a lot and probably even includes the related resources.
Perhaps we do need a static lookup table in some form again. Not sure how right now though.
Comment #37
mglaman#36 has a great point. This patch adds a static mapping, which prevents duplicated loops to find a resource type. It was modified in #3018287: ResourceTypeRepository computes ResourceType value objects on *every request* to improve performance but broke the aliasing of resource types.
It leverages the memory cache so that its caches can be invalidated.
Edit:
Forgot to update a doc block.
Comment #38
jibranDo we need a change notice here or need to amend an existing one?
Comment #39
bbralaWhy not use
cache.jsonapi_resource_types
which is a chained cache with memory first and then the default cache which has been introduced for the ResourceRepository and is already included?This cache could do the same, but with some persistence?
Comment #41
gabesulliceThe patch in #2996114-167: Argument 2 passed to Drupal\jsonapi\Routing\Routes::Drupal\jsonapi\Routing\{closure}() must be an instance of Drupal\jsonapi\ResourceType\ResourceType, NULL given references this issue as a follow up since it is introducing a test related to resource type aliasing.
Before this patch lands, we will need to clean up the @todos that it introduced (assuming it lands).
Comment #43
bbralaI was working on extra's and using build events there and came across issue again. I think it is time to get this moving again since #2996114: Argument 2 passed to Drupal\jsonapi\Routing\Routes::Drupal\jsonapi\Routing\{closure}() must be an instance of Drupal\jsonapi\ResourceType\ResourceType, NULL given has landed.
I've resolled the patch for 9.3 as a start.
Comment #44
bbralaMissing docblock for ResourceTypeRepository contructor.
Comment #45
bbralaRemove tests that were added in #2996114: Argument 2 passed to Drupal\jsonapi\Routing\Routes::Drupal\jsonapi\Routing\{closure}() must be an instance of Drupal\jsonapi\ResourceType\ResourceType, NULL given temporarily. The coverage for the API is already contained in this issue.
Comment #46
bbralaComment #49
bbralaFixed
Kernel/Normalizer/LinkCollectionNormalizerTest
since it instantiates aResourceType
and was missing an argument.Comment #50
bbralaInvalidated.
Comment #51
bbralaInvalidated.
Comment #52
bbralaComment #53
bbrala(I give up on commit style patches)
Cleaned up the code to lookup resources since we added a
lookupResourceType
method in a related issue. Made cache lookups consistent with the code in theall()
method.Also removed the extra cache from the service and class. We should just use the cache already present in the repository, no need to change the service. The CID can't overlap so it is fine to save it in permanent cache.
Comment #54
gabesulliceI think this is the correct thing to do, but I suspect committers will ask us to put
$type_name
as a final, optional argument to maintain BC on this internal API on a "best effort" basis.In another issue, we could consider adding a static
ResourceType::create
method in order to have a more elegant method signature.This looks like a bugfix? E.g. if
$this->bundle === '?'
then the path would contain an illegal character in the path segment of the URL.Yeah, I think the way to do the BC dance is to keep the fallback inside the
ResourceType
class and make$type_name
and optional argument withNULL
as the default.Could we add this caching in a separate issue?
Fine, but could be a Task issue.
😍
👏👏👏
These changes could be avoided by making
$type_name
optional.This is basically covering JSON:API Extras. Which, happily, you maintain! I'm not worried about removing this test, but I do think we should have a change record that describes the upgrade path from an aliasing repository to an event subscriber.
Comment #55
bbralaThanks for the review, at first glance they all make perfect sense. I'll get this cleaned up and perhaps split off some parts to keep it as small as possible.
Comment #56
bbralaResourceTypeBuildEvent
.$type_3 = new ResourceType('entity_type_3', '123', EntityInterface::class, NULL, TRUE);
. Guess that call was forgotten.Comment #57
bbralaComment #59
bbralaIf this issue is fixed we should look into #3171154: Cannot upload files if file--file resource type has been aliased..
Comment #60
bbralaReverted accidental remove of
jsonapi_test_field_aliasing
module.Comment #61
bbralaComment #62
gabesulliceLooks good! My only reservation after #60 was still the cache. I was worried that going over the network to a database, even one like Redis, would be slower than a loop in PHP. However, I confirmed that the cache back end is an in-memory one, which mitigates my concern.
Comment #63
mglamanSeeing this RTBC brings tears of joy to my eyes 🥲
Comment #64
bbralaDraft change record available here.
Comment #65
gabesulliceLooks good! Thank you.
Comment #66
larowlanThis is looking great, just a couple of suggestions
I think we should document that two hyphens will be used to construct the path, e.g -- becomes /
I don't think we need to initialise this - as passing null to the constructor will take care of that
this would read nicer if you flipped the logic, you'd avoid the second ternary
Comment #67
bbralaNULL
to be explicit about it.RecourseTypeRepository::all
method uses exactly this logic. I chose consistency over readability here.New patch incoming after I eat ;)
Comment #68
bbralaI've been thinking and #66.2 does mean some extra breaks in contrib since this would mean anyone who now uses double dashes in a renamed resource would end up with a new path so i think something like
This would mean less breakage through contrib, but would mean it is impossible to rename a bundle only.
Pros:
- Less breakage in contrib with names containing double dashes.
Cons:
- Less flexible renaming of resources since you cannot rename something like
node/page
tonode/content-page
.Another option is adding an option to enable/disable adding path separators. I'm looking for some extra opinions here I think.
Comment #69
mglamanOn mobile, so not s thorough review of recent updates. But we should not consider contrib here, in my opinion. This was never a public API and Amy contrib was hacks.
The only two contrib to worry about is JSONAPI Extras and Commerce API (which maintains it's kwk version of this code)
I like `--` becoming a standard constant to identify a split (like `:` for plugin derivatives)
Comment #70
bbralaAfter a discussion on Slack with @larowan, @mglaman and @e0ipso i decided to go through with this patch.
Drupal\jsonapi\ResourceType::TYPE_NAME_URI_PATH_SEPARATOR
that defines how resource names are split to uri paths.$typeName
variable is now initilized asNULL
inResourceTypeRepository::createResourceType
Putting this back to RTBC as discussed with @larowan on Slack.
Comment #71
BR0kENSuggestion:
$this->entityTypeId . self::TYPE_NAME_URI_PATH_SEPARATOR . $this->bundle
.Suggestion:
The path to access this resource type. This function replaces {@see TYPE_NAME_URI_PATH_SEPARATOR} with a "/" in the URI path. Example: "node--article" -> "node/article".
Suggestion:
return new static($entity_type->id() . ResourceType::TYPE_NAME_URI_PATH_SEPARATOR . $bundle, $fields);
Suggestion:
The resource type name. The {@see TYPE_NAME_URI_PATH_SEPARATOR} is replaced by a "/". Example: "node--article" -> "node/article".
Comment #72
bbralaComment #73
bbralaI'll have a look, thanks.
Comment #74
bbrala#72.1: Ok, the current contruction is indeed weird. But i think i rather
sprintf('%s%s%s', ...args)
then.#72.2: Good point.
#72.3: See 1.
#72.4: See 2.
Comment #75
bbralaUpdated docblocks a bit more to be more readable. Also replaces sprinf with normal string concatting.
Comment #76
BR0kENAssigning a value and then reassign it in case it's
NULL
seem useless. Can we do the following?It's
NULL
by default, no need to explicitly declare that.Since this is the new public method, can we explicitly declare it should have no return statement by adding the
void
return type annotation?Comment #77
bbrala1. I think the current way is more readable in my opinion.
2. This is true technically, sure.
3. Yes we actually can, and should.
Comment #78
bbralaThe cli tool seems to have rendered a broken interdiff.
Comment #79
BR0kENComment #80
gabesullice😍 loving the progress here.
Comment #81
larowlanJust a couple of minor things here.
It might be worth asking in #documentation for some clarification on the best way to handle these @see references, but it seems like they're not compliant with the standard
According to the docs, this needs to be fully qualified
I don't claim to be an expert, but the docs seems to indicate that's the case
According to the standard, @see needs to be on it's own line, with a space
https://www.drupal.org/docs/develop/standards/api-documentation-and-comm...
Comment #82
ravi.shankar CreditAttribution: ravi.shankar at OpenSense Labs commentedMade changes as per comment #81, please review.
Comment #83
bbralaThat is exactly what i wouldv'e done. I've updated the remaining docblock with the reference to the constant.
Setting to rtbc again since the changes are so minimal.
Comment #84
larowlanall these file permissions changes are unintended, can you please revert these
Comment #85
bbralaSorry, forgot to add the local branch while generating the diff. Interdiff is empty for some reason, but when manualy comaring the patch had just the last set of lines removed.
Comment #87
larowlanCommitted fc51b4b and pushed to 9.3.x. Thanks!
Published the change record.
Nice work folks.
Comment #88
bbralaAwesome! Thanks everyone!
Comment #89
Wim Leers👏👏👏