Note that this is tagged against 8.1.x currently. If retitled "No way to retrieve an entity's URL alias via Rest" this could also be seen as a bug report and tagged against 8.0.x, but not sure about that.
Problem/Motivation
When normalizing a node or another entity with a path field, the alias is not part of the normalized entity.
This is because when loading an entity the path field item list never gets any field items, it only acts upon the saving of the entity. If there are no field items, the default field normalizer (rightfully) skips the field.
Proposed resolution
Provide a dedicated normalizer for path fields that fetches an entity's alias in the same way that \Drupal\path\Plugin\Field\FieldWidget\PathWidget::formElement() does and creates a field item for it.
The pid of the alias is not contained in the field item for the same reason we do not expose the entity ID in the normalization. We cannot convert the pid into a UUID (because aliases do not have UUIDs) so we simply skip it.
Remaining tasks
| Task | Novice task? | Contributor instructions | Complete? |
|---|---|---|---|
| Create a patch | Instructions | ||
| Add automated tests | Instructions | Yes | |
| Draft a change record for the API changes | Instructions | ||
| Review patch to ensure that it fixes the issue, stays within scope, is properly documented, and follows coding standards | Instructions |
User interface changes
None.
API changes
None.
Data model changes
The normalized version of entities with path fields now contain said fields with the alias value.
| Comment | File | Size | Author |
|---|---|---|---|
| #79 | interdiff-2649646-77.txt | 2.32 KB | damiankloip |
| #79 | 2649646-77.patch | 2.66 KB | damiankloip |
| #71 | 2649646-71.patch | 2.55 KB | damiankloip |
| #70 | interdiff-2649646-65-70.txt | 2.36 KB | dremy |
| #70 | 2649646-70.patch | 8.24 KB | dremy |
Comments
Comment #2
tstoecklerMoving this to HAL module. To put this into Path module we would have to implement a service provider which dynamically registers this normalizer service depending on whether HAL is installed or not. I thought that was a little cumbersome, but if people prefer that, we could of course move it back.
I chose
$supportedInterfaceOrClass = PathFieldItemList::classeven though we are working around a limitation ofPathItemnotPathFieldItemList, so I suppose it would be more correct to actually check the field item's field definition and then get the type from that and check if it's of typepath. I.e. with this the normalizer would not be used if someone alters outPathFieldItemList(but notPathItem). Because that would not allow use the convenient$supportedInterfaceOrClassand because I don't think that's a use-case we need to support I went the easy way here.Other than that I think it's pretty straightforward. Should be easy to write a unit test for this but we probably also want integration test coverage, assuming this doesn't already fail (which would be nice).
I verified using the Default Content module that aliases are successfully exported with this as part of a node and are then imported on a different site along with the node. (Note that I copied the code from a custom module to create this patch, just in case there are syntax errors or something that I overlooked and people question the previous statement.)
Comment #3
tstoecklerGaarrhrhrrhhr!!!! (Bad-copy paste from the original, obsolete implementation I had locally.) Could also use a trailing period.
Leaving at needs review.
Comment #4
larowlanI think it belongs in path, with a service provider, but let's see if we can get consensus first. There's a sample service provider in file entity module (contrib) that does the same thing.
Comment #5
tstoecklerNaahh, I just went ahead with it. I agree that it is the better solution, I was just too lazy to do it initially...
Thanks for the pointer! That saved me a bunch of trial-and-error, yay.
Also added a unit test that seems pretty complete (didn't run coverage, though TBH) and a basic integration test. Should be good to go from my point of view.
The added files are in a separate interdiff because I was lazy and didn't create a branch locally. And you will notice that the moving of the file normalizer from HAL to Path module is not in the interdiff because PHPStorm automatically staged that for me, because it thought it was being smart...
Comment #7
tstoecklerAhh, it's annoying that our test system fails so hard because of a missing
@groupin the test (but I could also stop forgetting it...).Let's see.
Comment #8
wim leersPatch looks great.
Why only for
hal, and not for other serializations?Comment #9
wim leersComment #10
wim leers#2609824: REST: how to set an entity's path alias when POSTing, and how to get an entity's path alias when GETting asked about this too, both for JSON and HAL+JSON.
Comment #11
tstoecklerRe #8: We check for HAL module because the FieldNormalizer that we are extending comes from HAL module. I'm not sure what the best approach is to enable this for JSON off the top of my head, will have to look into that. Not sure if that's a needs work or a separate issue, but leaning to the former (unfortunately) as it might turn out that we have to have two classes (because of the different inheritance chains with hal_json and (nohal_)json) and then utilize a trait for that. Not sure though, so leaving at needs review for now.
Comment #12
wim leersProgress FTW.
Comment #13
catchThis needs to say why - i.e. that the normalizer extends the HAL FieldNormalizer.
For a minute I was going to say this should live in HAL module, since alias storage lives in \Drupal\Core, but then it depends on PathFieldItemList which is in path module, so we'd still have an optional dependency either way.
Part of me is thinking about a path_hal module with a dependency on both, but then that clutters the module list a bit.
Also another way to get path aliases for entities would be to have path aliases themselves exposed to REST, then you could look up aliases by the system path for the entity and get it that way - which would work regardless of the internals of whether a site has path module itself installed or not.
The path field item makes sense in terms of cleaning up the form integration of path module, but URL aliases really aren't a part of the entity as such in the way they're stored at the moment, so moving back to CNR to discuss a bit more.
Comment #14
dawehnerWell, is there no way that field items could specify that even they are computed, they should be normalized?
Comment #15
wim leers#14 is a great question. Who would know the answer to that?
Comment #17
davidwbarratt commentedI accidentally created this duplicate issue #2693077: Path is not returned in REST response.
The real problem is, is that the data is never retrieved from the database (or rather, it is, but only when you load the widget). I think it would be better to get the data from the database when the field/entity is loaded. Doing it that way exposes the data to all of the existing systems (Form API, REST API, Serializes, Caching, etc.) for free.
Perhaps we could listen to entity load, check to see if a field of type "path" is on it, and add it to the loading?
Comment #18
davidwbarratt commentedI really wish there was something like
getComputedValueon \Drupal\Core\Field\FieldItemListInterface that would be called when the entity is loaded.But since that doesn't exist (afaik), then I think we should use hook_entity_storage_load() to also load the
path(only when an entity has thepathfield, which may be under a different name).Comment #19
davidwbarratt commentedAttached is a patch that should fix the issue, did this again
8.0.xsince I think it's a critical issue, but I can merge8.1.xor8.2.xif that is more approriate.Since the values are already set by the time you load the widget, I cleaned up the widget a bit. I also tested this and it seems to work perfectly in
xml,json, andhal_jsonComment #20
davidwbarratt commentedWhoops... should be
continuenotreturn.Comment #23
davidwbarratt commentedAdd the langcode.
Comment #24
davidwbarratt commentedComment #26
davidwbarratt commentedAdd the language to the query conditions.
Comment #28
davidwbarratt commentedAdd defaults to hopefully resolve the test failures. :/
Comment #29
davidwbarratt commentedDefault alias value is an empty string.
Comment #30
davidwbarratt commentedWell, I'm not sure why the test is failing, so if someone smarter than me could take a look at it, I would greatly appreciate it. :)
Comment #33
tstoecklerI think always loading the path alias into the entity should be a separate issue. And if that does make it in, we can close this one (or adapt it accordingly). But for now utilizing a dedicated normalizer has its own value IMO and I would like to pursue it indepedently. Thanks!
Comment #34
davidwbarratt commentedI don't understand, loading the path is what should happen since it's a "field". All fields are loaded when he entity is loaded. Doing this resolves this issue.
If you have a separate normalizer, you are tying the path to the form and to the normalizer, but not the entity object itself. That means anywhere in Drupal you want to modify the path, you have to look it up yourself, rather than just have it on the entity like every other field.
If we didn't want this behavior, we shouldn't have made it a field in the first place.
Comment #35
davidwbarratt commentedAlso, I don't know how you're going to get out of the caching issue. If you add it to a normalizer, it will be cached with the request, but not added as a cache dependency (as if it would if it was already on the entity). Because of this, if I were to go in and modify the alias directly (and not the entity itself), the cache will not be invalidated and the wrong data will be returned to the user.
I guess if you also want to override the REST Resource for any entity that has this field, you could do that (or add a check in the
restmodule), but all of that seems like nonsense, just add it to the entity and be done with it.Yes, it causes an extra database lookup, but there are few instances you don't need the alias (imho).
Comment #36
davidwbarratt commentedAnd... if someone really doesn't want the extra database lookup, they can just remove the base field from the entity. You'll still be able to have aliases, it just wont be directly tied to the entity itself.
Comment #37
tstoecklerI wasn't arguing on a technical level, but on a process level. But that's fine, when I find the time to revive this, I will move my previous patch to a separate issue. Don't mind me.
Comment #38
davidwbarratt commentedOh I see, well either way, I don't want to take over this issue, I just think this is a better solution is all. If it really belong in another issue that's fine, I can make another one, but I don't see why.
Comment #39
tstoecklerNo it's fine. I don't have much time to work on anyway at the moment, so feel free.
Comment #40
catchI really don't think we should further entangle path aliases and entities, and said the same in #13.
Comment #41
davidwbarratt commentedThen why is it a field in the first place?
Are we really ok saying that a path field value, must always be retrieved by the module/application that needs to modify it?
For instance, if I load a node from the database, the only way to modify the path field is by then loading the path with the path storage. Should other computed fields do this as well?
I just don't think it should be a field at all if it's not going to be loaded with the entity. We should at least remove it from the entity object (some how?) so there isn't an expectation of it having a value. At the same time, if it's not returned with the entity, then it should not be updated with the PHP or REST API.
Like I said, ideally, there would be a way in Drupal core to dynamically load computed fields when they are needed, but I don't think that API exists. :(
So, if we don't load it with the entity, then I think we need to stop making it a field all together. We can have the form just be a standard form alter that is added to nodes and taxonomy terms (and any other entity that wants it) and then we'll make a REST endpoint to retrieve a url alias by source (which is inconsistent with the Entity REST API, but I don't see a way around it).
The choice is yours, I think it's gross either way, but adding it to the entity seems to be the lesser of two evils at this point.
Comment #42
catchThe issue that changed this from a standard form alter is #1980822: Support any entity with path.module - that and related issues has most of the history. I fully agree that what we have currently is a half-measure, I just don't think making it a full-measure is necessarily the right direction.
That's what I suggested earlier in the issue. Given that we support non-entity path aliases, and that entities can have multiple path aliases in both different languages and the same language, we might need that anyway for a complete REST API.
There's also #1553358: Figure out a new implementation for url aliases open which could change all of this again if it ever happens.
I agree it's all gross, but that's why I tried to push back a bit before pushing ahead with this implementation. I don't know which I really think is the least worst at the moment.
Also loading the path alias into the entity means extra i/o on entity loads, for something which is never, ever accessed as a property of the entity except for providing default values in forms and REST requests. Not keen on that either. Also can't usefully take advantage of entity caching since path aliases can be changed via the path alias UI independent of entity CRUD.
Comment #43
davidwbarratt commentedIs there a way to make something a cache dependency of an entity (like a page cache)? (I'm assuming the path item can be a cache dependency?). Even if we make a custom serializer, we have the same issue (but perhaps it's easier to solve).
I think it actually makes sense to have the alias as a computed field, but it seems like Drupal is missing some key APIs for computed fields (i.e. only retrieving the alias when needed, storing the computed value in cache, as a dependency, if possible, etc.). I thought about overriding
getValue()method on the List class, but that didn't seem like a good idea. :/I mean, in reality, you should be able to have a computed field be anything from
1+1to an HTTP Request to an external service and Drupal core should take care of everything else as if it were a standard base field, for you (imho).Comment #44
davidwbarratt commentedAlso, I think a good rule of thumb is "API First" and the way path module is now, it's not API first at all; so either way we go, I think that's what we need to go for.
Comment #45
davidwbarratt commentedSince I don't think we can remove the field (and technically "break" the API, which is already broken imho).
Here's some possible solutions:
sourceand clear out the entity cache for that entity. This is the same thing you'd do if you were to allow someone to modify a field independent of an entity, you'd have to get the parent entity and clear the cache for it.The only alternative that I can see is that we change path field to store the
pid. That would return thepidwhen you load the entity (it would be on the base table). Also, you'd have it available to look up via REST or other API's and you could easily look it up for the form, etc. Also, don't have to worry about cache (other than a delete), but that's the same thing that happens for entity reference. I don't really like this idea, but I think it's better than not loading thepidat all when the entity is loaded.Comment #47
davidwbarratt commentedReroll
Comment #48
davidwbarratt commentedDrupal 8.1 version of the patch.
Comment #49
davidwbarratt commentedAnother way we could do this, is just add the
pidto the entity. That way the entity's cache is not dependent on any changes to the alias.Comment #53
dagmarRerolled against 8.3.x
Comment #55
wim leersI completely agree with all of the arguments that @davidwbarratt outlines: either it's a field and then it should be normalized (which it is not today), or if it doesn't get normalized, then it shouldn't be a field.
That being said, @catch is also right in two important aspects:
I think the patch can be updated to support point 1. Marking for that.
However, updating it to support point 2 is going to be very difficult: it's essentially blocked on #2480077: Using the URL alias UI to change aliases doesn't do necessary invalidations: path aliases don't have cache tags.
Comment #56
wim leersPer #17, #2693077: Path is not returned in REST response was marked as a duplicate of this issue. Adding that to the list of related issues.
Comment #57
wim leersBerdir commented at #2480077-21: Using the URL alias UI to change aliases doesn't do necessary invalidations: path aliases don't have cache tags:
As usual, Berdir is right. It looks like that could be the better solution.
Comment #58
larowlanComment #59
tedbowOk, this patch takes into account there might be multiple paths for translations. Probably still needs work but wanted to get this out to get feedback.
Comment #61
wim leersThe main thing I have to think about when I review #59's interdiff is: . i.e. single alias, single translated alias, multiple aliases, multiple translated aliases, and combinations.
The second thing I have to think of:
Comment #62
tedbowOk this patch fixes the test fail PathLanguageTest
Basically we have to check if the when dealing with a new translation that we don't use the existing pathitem from the source.
@Wim Leers agreed on the need for test coverage.
Comment #64
tedbowAssiging to myself. Working on fix.
Comment #65
tedbowOk the test failures in #62 I believe because I was not testing for a translatable entity type with an entity that was not translatable. Now I am instead checking if the actual path field is translatable.
Comment #66
tstoecklerNot entirely sure if it's good that this is being pursued in the way it is when @catch said above:
Assigning to him, as he is also the sole maintainer of path.module and this is a pretty massive change to how entity aliases work.
Comment #67
berdirYes, not sure about doing this on storage_load. That means that information will be cached together with the node, so when you edit it in the admin UI, the edit form will still show the old values. And it also does additional queries for every node that is being loaded, although we will most likely *not* use those values.
What we need is to be able to load it on-demand when the values are accessed. Problem is that this is currently a bit complicated and we have no proper API for it (also due to get() vs. __get()). #2413471: FieldItemBase must check property definitions instead of already instantiated properties might help with that, need to review that.
Comment #68
tedbowPostoning on #2413471: FieldItemBase must check property definitions instead of already instantiated properties.
I updated the patch there that should fix the test failures. Otherwise seems like simple fix.
Comment #69
catchUnassigning me, Berdir's comment in #67 covers my objections. I'd also add there's #2336597: Convert path aliases to full featured entities open and likely to be revived as part of the workflow initiative, so we should bear in mind what things would look like after that too.
Comment #70
dremy commentedAfter applying patch on #65, I received the following error on "node/add/{bundle}" pages:
After looking around, node/add/{bundle} page's don't have $entity->id() for $unchanged_entity to be set to, leading to a "null" value and the subsequent error.
So I've added a check for null and patch to solve.
Comment #71
damiankloip commentedHere is a new (rough) patch and approach. I'm not sure we need to postpone on #2413471: FieldItemBase must check property definitions instead of already instantiated properties. We don't need the field item as such? As path fields are a really special kind of snowflake right now, we can just adda normalizer for the PathFieldItemList instead and lookup the entity alias directly from the parent (as I think catch was getting at above somewhere).
Comment #72
tstoecklerI think this patch is a nice middleground for the current half-field that path is. Not exactly sure if @catch would be fine with this, but I think yes, if I read the last comment correctly.
I was about to comment why we don't use
PathItem. I guess that's because no field items are ever created on load right? Would be nice to add a comment to this effect either to the class or the$supportedInterfaceOrClassproperty.Normalizers are only called once (i.e. not per translation), so we should fetch aliases in all languages here and add them in the way that translated field values otherwise get added to the rest output.
Comment #73
berdir1. Path is a computed field now, so there should always be a PathItem now.
Comment #74
damiankloip commentedThanks both for your input.
@berdir, yes that's correct, however \Drupal\hal\Normalizer\FieldNormalizer::normalizeFieldItems() checks first whether the field is empty or not, which returns TRUE for path most of the time...
@tstoekler:
1. Yes, pretty much, plus the comment above based on #73. So we have a field item but the field is considered empty still.
2. Hmm, we don't return all translation values for all fields, do we? Maybe just in hal? if so, we might need to just make sure the hal specific normalizer honours the current langcode from $context. As each translated field item will get normalized anyway.
3. Shouldn't denormalization get handled anyway if 'alias' is posted back and the field item is saved. The logic in PathItem looks to handle this.
But as mentioned, this is still rough, and has no tests either.
Comment #75
tstoeckler1. Ahh makes sense, thanks. That's actually interesting, though. I guess we could provide a "proper" implementation of
isEmpty()inPathItemwhich actually uses the alias storage? Definitely not this issue, though. An in-code comment explaining would still be much appreciated, though :-)2. Looked into this (though still far from being an expert on this) and it seems that the translation handling is in fact HAL-specific. Hal's
FieldNormalizer(which normalizes on the field item list level) calls normalize on each "translated field item". So if we in fact were to somehow move to the field item level (per 1.) we would get translation handling for free, otherwise I'm afraid we have to iterate over the entity's translation languages ourselves.3. Ahh right, totally missed that. Makes sense. Would also be helpful to add a comment about to the class docs, though.
Comment #76
damiankloip commentedYes, if we could use the item level again, we would get the hal translations for free. So we really just need to fix the isEmpty() issue then we could actually do that...
Comment #77
damiankloip commentedHere is an updated patch to make it use field item level, and check $context for a langcode (this is a generic pattern, but means it makes it work 100% with hal).
So this now leads to the question, could we have a smaller separate issue JUST for isEmpty() on the PathFieldItemList class, then postpone this issue on that?
Comment #78
tedbow@damiankloip I think you forgot to upload the new patch ;)
Comment #79
damiankloip commentedHaha, yes indeed!
Comment #80
tedbowWhy is not extending \Drupal\hal\Normalizer\FieldItemNormalizer? It seems that all the Hal specific normalizers that I have seen extend \Drupal\hal\Normalizer\FieldItemNormalizer.
Would there ever be a case where \Drupal\hal\Normalizer\FieldItemNormalizer gets update with change that should affect all Normlaizers that extend it for hal and this normalizer won't get the change.
I would suppose if the Hal normalizer didn't extend this class then this could be move to a trait.
Comment #81
damiankloip commentedI'm not sure what you mean entirely, the PathItem normalizer already just extends the FieldItem normalizer. Not sure I see the use of a trait here. Or did I miss something else? We would be using a trait but both classes would extend from the same base class?
Comment #82
tedbow@damiankloip sorry updated my comment. mentioned wrong class
Comment #83
damiankloip commentedTedbow, thanks - makes more sense now! You're correct the current class hierarchy worked nicely for field level, not field item level. I'll update this.
Comment #84
dawehnerHere is an issue which tries to solve things properly: #2846554: Make the PathItem field type actually computed and auto-load stored aliases
Comment #86
damiankloip commentedLet's postpone this on #2846554: Make the PathItem field type actually computed and auto-load stored aliases and see if we need anything after. Likely we will just close this one out.
Comment #87
berdirThat issue now provides full REST test coverage, so I think we can officially close this one.
Comment #88
wim leersI came here to do exactly that :) Thanks!