Problem/Motivation

When customising the toUrl method of an entity, such as with bundle classes, its possible to return non-routable Url objects.

\Drupal\path\Plugin\Field\FieldType\PathFieldItemList::computeValue assumes that all Urls returned by \Drupal\Core\Entity\EntityInterface::toUrl are routable.

When this code path executes, with a non-routable Url, the following exception is thrown:

UnexpectedValueException: Unrouted URIs do not have internal representations. in Drupal\Core\Url->getInternalPath() (line 798 of core/lib/Drupal/Core/Url.php).

Drupal\path\Plugin\Field\FieldType\PathFieldItemList->computeValue() (Line: 33)
....

Path.module should simply handle non-routable Urls.

Original report

I'm using a bundle class to override the toUrl() method on a particular node type. In particular, there's a link field that, if populated, will be used by the toUrl() method. The reason is that we want to allow cards to link to internal or external links (or no link whatsoever) as elegantly as possible. The code looks like this:

  public function toUrl($rel = 'canonical', array $options = []) {
    if ($rel === 'canonical') {
      if (!empty($this->field_link->uri)) {
        return Url::fromUri($this->field_link->uri);
      }
      else {
        return Url::fromRoute('<nolink>');
      }
    }
    return parent::toUrl($rel, $options);
  }

I can successfully create such a node and the behavior of toUrl is as hoped.

Unfortunately, editing the node is not so smooth. The path module throws an erro:

UnexpectedValueException: Unrouted URIs do not have internal representations. in Drupal\Core\Url->getInternalPath() (line 798 of core/lib/Drupal/Core/Url.php).

Drupal\path\Plugin\Field\FieldType\PathFieldItemList->computeValue() (Line: 33)
....

When I fix that by hacking core a bit then I get to the next exception:

UnexpectedValueException: Unrouted URIs do not have internal representations. in Drupal\Core\Url->getInternalPath() (line 798 of core/lib/Drupal/Core/Url.php).

Drupal\path\Plugin\Field\FieldWidget\PathWidget->formElement(Object, 0, Array, Array, Object) (Line: 19)
....

When I fix that then everything is grand.

These exceptions happen even if the url alias element is not on the node form display for this node type.

Steps to reproduce

Return a non-routable Url from a entity::toUrl, such as with a bundle class.

Proposed resolution

path.module needs to check that $entity->toUrl()->isRouted() in addition to its existing checks for !$entity->isNew() in a couple places.

Remaining tasks

Tips for manual testing

Apply the test-only patch.

Enable entity_test, path, and path_entity_test_external. (To enable test modules, add $settings['extension_discovery_scan_tests'] = TRUE; to your settings.php.)

Then go to /entity_test_external/add and create and edit these entities. See the errors.

Then apply the fix and see that creating and editing these entities works. And everything should still work for nodes and media, etc.

User interface changes

N/A

API changes

TBD

Data model changes

N/A

Release notes snippet

TBD

Issue fork drupal-3342398

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

  • 3342398-11.x Comparechanges, plain diff MR !9209
  • 11.x Comparecompare
  • 2 hidden branches
  • main Comparecompare
  • 3342398-path-module-calls Comparecompare

Comments

danflanagan8 created an issue. See original summary.

danflanagan8’s picture

Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new1.44 KB

Here's a patch. I'm adding the Needs tests tag.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

#2 didn't seem to break anything and changes seem innocent enough. Think we good to start the tests?

danflanagan8’s picture

Issue tags: -Needs tests
StatusFileSize
new8.91 KB
new12.43 KB
new3.52 KB

It took all day, but I have tests! I also improved upon the patch in #2 a bit.

I started with a Unit test for PathFieldItemList, which was brutal. Then I made a functional test that leverages the entity_test_external entity type that was added as part of tests for #2914785: Entities with external urls as a uri relationship can not be deleted when menu_link_content is installed. The behavior of the entity_test_external entity type is extremely similar to what my snippet in the IS does:

  public function toUrl($rel = 'canonical', array $options = []) {
    if ($rel === 'canonical') {
      return Url::fromUri('http://example.com', $options);
    }
    return parent::toUrl($rel, $options);
  }

So I didn't have to make a new entity class, but I did have to add a few things to the annotation so that I could get add and edit forms.

It turns out the functional test exposes the same bug exposed by the Unit test. So I'm not sure the Unit test provides a ton of value. It's there if we want it, but I'd be fine if we were to decide to remove it from the patch.

danflanagan8’s picture

Status: Needs work » Needs review
danflanagan8’s picture

The Functional test exposes several bugs in series, but it can obviously only fail once at a time. So here I'll take you through how each hunk of the fix causes the test to get farther before failing.

If I run the tests with no fix, I get this failure error when trying to create the entity:

UnexpectedValueException: Unrouted URIs do not have internal representations. in Drupal\Core\Url->getInternalPath() (line 804 of core/lib/Drupal/Core/Url.php).
Drupal\path\Plugin\Field\FieldType\PathFieldItemList->computeValue() (Line: 34)

The hunk that fixes this is:

+++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
@@ -25,7 +25,7 @@ protected function computeValue() {
+    if (!$entity->isNew() && $entity->toUrl()->isRouted()) {

The next error I get is this when I try to edit the entity:

UnexpectedValueException: Unrouted URIs do not have internal representations. in Drupal\Core\Url->getInternalPath() (line 804 of core/lib/Drupal/Core/Url.php).
Drupal\path\Plugin\Field\FieldWidget\PathWidget->formElement(Object, 0, Array, Array, Object) (Line: 349)

The fix for that is this:

+++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
@@ -43,7 +43,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
-      '#value' => !$entity->isNew() ? '/' . $entity->toUrl()->getInternalPath() : NULL,
+      '#value' => !$entity->isNew() && $entity->toUrl()->isRouted() ? '/' . $entity->toUrl()->getInternalPath() : NULL,

Theeeeen, the test fails when I try to set a path for the entity and update the entity:

Drupal\Core\Entity\EntityStorageException: Unrouted URIs do not have internal representations. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 816 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).
Drupal\path\Plugin\Field\FieldType\PathItem->postSave(1)

That gets fixed by the changes to `Drupal\path\Plugin\Field\FieldType\PathItem`.

After that the test would pass fine except that I thought it would be nice to add a validation error if a path is being set on an entity that isn't routed.

+++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
@@ -87,6 +87,11 @@ public static function validateFormElement(array &$element, FormStateInterface $
+      $entity = $form_state->getFormObject()->getEntity();
+      if (!$entity->isNew() && !$entity->toUrl()->isRouted()) {
+        $form_state->setError($element['alias'], t('An unrouted entity cannot have a path.'));
+      }

Without that code, there aren't any exceptions or errors, but the value of the url alias field is cleared out without any notice to the user. I think this validation error is better than that. This validation does not work if the entity is new, however. If the url alias field is populated for new unrouted entity, the content of that field simply get cleared out during PathItem::postSave(). Maybe we should add a message if that happens?

danflanagan8’s picture

StatusFileSize
new9.33 KB
new12.85 KB
new3.52 KB

Ignore patches in #4. All the comments in #4 and #6 are still valid. Don't ignore those. :)

danflanagan8’s picture

StatusFileSize
new8.91 KB
new435 bytes

Oy! The violations only increase when the fix is in place. So this new fail test backs out the change to the baseline. Apologies for the mess.

The last submitted patch, 7: 3342398-7.patch, failed testing. View results

danflanagan8’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 8: 3342398-8-FAIL.patch, failed testing. View results

danflanagan8’s picture

Status: Needs work » Needs review
StatusFileSize
new12.96 KB
new9.44 KB

Gosh, I had the wrong namespace for the new Unit test.

I need to change:

namespace Drupal\Tests\path\Unit;

to

namespace Drupal\Tests\path\Unit\Field;

Here are new fail and pass patches that only run the new unit test.

danflanagan8’s picture

StatusFileSize
new8.92 KB
new12.86 KB
new3.94 KB

I'm going to stop trying to be cute. Let's just make correct fail and pass patches and post them. Deep breath.

Let's hope 13 is my lucky number :)

The last submitted patch, 13: 3342398-13-FAIL.patch, failed testing. View results

danflanagan8’s picture

Issue summary: View changes
danflanagan8’s picture

Status: Needs review » Needs work

I ran across another exception when I deleted an entity like this:

Drupal\Core\Entity\EntityStorageException: Unrouted URIs do not have internal representations. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->delete() (line 761 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).
Drupal\path\Plugin\Field\FieldType\PathFieldItemList->delete() (Line: 941)

So I need to add to the test such that we delete one of the external entities. And I need to fix it, of course.

danflanagan8’s picture

Status: Needs work » Needs review
StatusFileSize
new13.91 KB
new2.12 KB

Here's a patch that address what I mention in #16, both the test coverage and the fix.

danflanagan8’s picture

StatusFileSize
new13.48 KB

Here's a patch that should apply for D9.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Test coverage seems good and the change makes sense.

danflanagan8’s picture

Thanks for the review, @smustgrave.

Test coverage seems good

There's one piece of code that I want to call out as not covered by the automated testing. It's this

+++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
@@ -91,8 +92,9 @@ public function postSave($update) {
-    elseif ($this->pid && !$this->alias) {
-      // Otherwise, delete the old alias if the user erased it.
+    elseif ($this->pid && (!$this->alias || !$entity->toUrl()->isRouted())) {
+      // Otherwise, delete the old alias if the user erased it or the entity's
+      // url has become unrouted.

It's the "if the url has become unrouted" part of that conditional. It would be possible to update the coverage to cover that, but it would require using a different entity than entity_test_external. That entity always has an unrouted url, so it can't be used to test the case where an entity has a routed url and then gets updated such that its url is unrouted. That's actually something that happens with the bundle class I describe in the IS.

I liked the idea of re-using the class that was already in the codebase for writing these tests. But it's not quite complete coverage. But I think everything else is covered in want ends up being a pretty simple functional test.

I'm also adding a related issue which is the issue for which the entity_test_external class was added.

smustgrave’s picture

Will see what the committer thinks. Also like reusing what's there if possible. But if we do need coverage for that could we still reuse that entity and make a new one for the one scenario? Seems overkill just speaking out loud.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

needs-review-queue-bot’s picture

Status: Reviewed & tested by the community » Needs work
StatusFileSize
new4.98 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

bharath-kondeti made their first commit to this issue’s fork.

bharath-kondeti’s picture

StatusFileSize
new6.11 KB

Rebased the patch for 11.x

danflanagan8’s picture

Status: Needs work » Needs review
StatusFileSize
new13.91 KB

There's something clearly wrong with the patch in #25. It's half the size of the most recent patch I posted. Let's ignore it.

I'm hoping that the review bot kicked this back because the most recent patch I uploaded was for 9.5.x. I would not expect that one to work against 10.x or 11.x. I'm going to re-upload the patch from #17, which is really the one that @smustgrave RTBC'd. Hopefully it still works.

danflanagan8’s picture

Status: Needs review » Reviewed & tested by the community

Yes, looks like I was correct. I'm going to reset this to RTBC since it's the same patch that @smustgrave RTBC'd in #21. It just got kicked back because I uploaded a D9.5 patch in #18 and that's the one that the review-queue bot was checking.

catch’s picture

Status: Reviewed & tested by the community » Needs work

Just a couple of nits but one I'm not sure how to fix on commit.

  1. +++ b/core/modules/path/tests/src/Functional/PathEntityTestExternalFormTest.php
    @@ -0,0 +1,57 @@
    +
    +  /**
    +   * Tests the node form ui.
    +   */
    

    Should this be Path form UI as in the class phpdoc? LOoks like it could be copypasta.

  2. +++ b/core/modules/path/tests/src/Unit/Field/PathFieldItemListTest.php
    @@ -0,0 +1,125 @@
    +
    +    // Dependency injection is not used twice within the scope of the covered
    +    // code. Mock the field type manager and path alias repository and place
    +    // use $path_field_list when mocking $field_type_manager.
    +    $field_type_manager = $this->createMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
    

    'place use' looks like an editing typo but I'm not sure what it's actually describing here.

danflanagan8’s picture

Status: Needs work » Needs review
StatusFileSize
new14.25 KB
new2.02 KB

Hi @catch,
I'm terribly sorry about those broken comments. Regarding #28.2, not even I could figure out what I was trying to say! I wonder if I deleted a whole line or two on accident? The point of the comment was to explain why I was setting a couple things on the container. So I kind of started from scratch on that comment and now I think it makes sense.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Seems points in 28 have been addressed

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 29: 3342398-29.patch, failed testing. View results

danflanagan8’s picture

Status: Needs work » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 29: 3342398-29.patch, failed testing. View results

danflanagan8’s picture

Status: Needs work » Reviewed & tested by the community

Same fail as described in #32. Back to RTBC.

quietone’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs followup
StatusFileSize
new1.3 KB
new14.26 KB

I'm triaging RTBC issues. I re-read the IS and the comments. I didn't find any unanswered questions or other work to do.

I read the Issue Summary and the comments. Of special note is the details on what is not being tested in #20.

I skimmed that patch and this error message caught my eye.

+++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
@@ -87,6 +87,11 @@ public static function validateFormElement(array &$element, FormStateInterface $
+        $form_state->setError($element['alias'], t('An unrouted entity cannot have a path.'));

Since this will show in the UI I think it should be easier for an admin to understand and to fix. And it should not continue to use 'unrouted' which is a misspelled word in the dictionary. From the IS I gather that this is an uncommon case. Therefore, how about we at least avoid the spelling error and make a followup, tagged usability, to improve the message. Unless someone has a better idea how about, "An entity without a route cannot have a path."?

Actually, I have a moment and I will update the patch. Tagging for a follow up.

Status: Needs review » Needs work

The last submitted patch, 35: 3342398-35.patch, failed testing. View results

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new798 bytes
new14.27 KB

Completely forgot about the test.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs followup
Related issues: +#3384376: Improve message in PathWidget

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 37: 3342398-37.patch, failed testing. View results

dpi’s picture

Thanks for the this, just what I needed.

dpi changed the visibility of the branch 3342398-path-module-calls to hidden.

dpi changed the visibility of the branch 11.x to hidden.

dpi’s picture

Fixed conflicts for 10.2 and 11.x and created a MR for 11.x.

No changes other than merge fixes, deleting the since deleted PHPStan neon file.

dpi’s picture

Issue summary: View changes

Small IS rewrite.

samit.310@gmail.com made their first commit to this issue’s fork.

samitk’s picture

StatusFileSize
new88.62 KB

Hi @dpi,

I observe a issue, that post enabling Path entity_test_external module, The URL alias field value is not getting saved while saving the entity_test_external entity.

attaching the Screenshot.

Thanks
Samit K.

mlncn’s picture

Status: Needs work » Needs review

@samit My understanding of the test is that the entity should not be able to be saved, and so the alias does not get set, because it does not have a route (the test checks for the error message stating that).

If this still passes tests it should be back to RTBC.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mlncn’s picture

Assigned: Unassigned » mlncn
Issue tags: -Needs Review Queue Initiative +Needs reroll
mlncn’s picture

Assigned: mlncn » Unassigned
Status: Needs work » Needs review
Issue tags: -Needs reroll +Needs Review Queue Initiative

Re-roll complete; the phpstan error is a 500 Internal Server Error so probably not due to our code here.

mlncn’s picture

StatusFileSize
new14.45 KB

Here is a version that applies to Drupal 11.1 for anyone who needs that.

Sorry, is there a convention for patches we want the bot to ignore?

smustgrave’s picture

Status: Needs review » Needs work

Left some comments on the MR.

mighty_webber’s picture

Patch for Drupal 10.4.9

mighty_webber’s picture

StatusFileSize
new12.71 KB

Fix for Patch for Drupal 10.4.9

quadrexdev made their first commit to this issue’s fork.

dpi’s picture

Pushed a merge into main (11.3)

dpi changed the visibility of the branch main to hidden.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

brayn7 changed the visibility of the branch 11.x to active.