diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php index 0836df17ab..679c98299e 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php @@ -25,7 +25,7 @@ protected function computeValue() { $value = ['langcode' => $this->getLangcode()]; $entity = $this->getEntity(); - if (!$entity->isNew()) { + if (!$entity->isNew() && $entity->toUrl()->isRouted()) { /** @var \Drupal\path_alias\AliasRepositoryInterface $path_alias_repository */ $path_alias_repository = \Drupal::service('path_alias.repository'); @@ -57,12 +57,14 @@ public function defaultAccess($operation = 'view', AccountInterface $account = N public function delete() { // Delete all aliases associated with this entity in the current language. $entity = $this->getEntity(); - $path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias'); - $entities = $path_alias_storage->loadByProperties([ - 'path' => '/' . $entity->toUrl()->getInternalPath(), - 'langcode' => $entity->language()->getId(), - ]); - $path_alias_storage->delete($entities); + if ($entity->toUrl()->isRouted()) { + $path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias'); + $entities = $path_alias_storage->loadByProperties([ + 'path' => '/' . $entity->toUrl()->getInternalPath(), + 'langcode' => $entity->language()->getId(), + ]); + $path_alias_storage->delete($entities); + } } } diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index ba57f2467c..d1106a3caa 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -71,8 +71,9 @@ public function postSave($update) { // unspecified even if the field/entity has a specific langcode. $alias_langcode = ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode(); - // If we have an alias, we need to create or update a path alias entity. - if ($this->alias) { + // If we have an alias and the entity has an internal path, + // we need to create or update a path alias entity. + if ($this->alias && $entity->toUrl()->isRouted()) { if (!$update || !$this->pid) { $path_alias = $path_alias_storage->create([ 'path' => '/' . $entity->toUrl()->getInternalPath(), @@ -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. $path_alias = $path_alias_storage->load($this->pid); if ($entity->isDefaultRevision()) { $path_alias_storage->delete([$path_alias]); diff --git a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php index 088ef784e2..75547321cc 100644 --- a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php +++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php @@ -43,7 +43,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen ]; $element['source'] = [ '#type' => 'value', - '#value' => !$entity->isNew() ? '/' . $entity->toUrl()->getInternalPath() : NULL, + '#value' => !$entity->isNew() && $entity->toUrl()->isRouted() ? '/' . $entity->toUrl()->getInternalPath() : NULL, ]; $element['langcode'] = [ '#type' => 'value', @@ -87,6 +87,11 @@ public static function validateFormElement(array &$element, FormStateInterface $ if ($alias !== '') { $form_state->setValueForElement($element['alias'], $alias); + $entity = $form_state->getFormObject()->getEntity(); + if (!$entity->isNew() && !$entity->toUrl()->isRouted()) { + $form_state->setError($element['alias'], t('An unrouted entity cannot have a path.')); + } + /** @var \Drupal\path_alias\PathAliasInterface $path_alias */ $path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([ 'path' => $element['source']['#value'], diff --git a/core/modules/path/tests/modules/path_entity_test_external/path_entity_test_external.info.yml b/core/modules/path/tests/modules/path_entity_test_external/path_entity_test_external.info.yml new file mode 100644 index 0000000000..7e5a5d23ba --- /dev/null +++ b/core/modules/path/tests/modules/path_entity_test_external/path_entity_test_external.info.yml @@ -0,0 +1,5 @@ +name: 'Path entity_test_external' +type: module +description: 'Supports test with entity_test_external entity type.' +package: Testing +version: VERSION diff --git a/core/modules/path/tests/modules/path_entity_test_external/path_entity_test_external.module b/core/modules/path/tests/modules/path_entity_test_external/path_entity_test_external.module new file mode 100644 index 0000000000..a9fcb24c62 --- /dev/null +++ b/core/modules/path/tests/modules/path_entity_test_external/path_entity_test_external.module @@ -0,0 +1,28 @@ +id() === 'entity_test_external') { + $fields['path'] = BaseFieldDefinition::create('path') + ->setLabel(t('URL alias')) + ->setTranslatable(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'path', + 'weight' => 30, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setComputed(TRUE); + + return $fields; + } +} diff --git a/core/modules/path/tests/src/Functional/PathEntityTestExternalFormTest.php b/core/modules/path/tests/src/Functional/PathEntityTestExternalFormTest.php new file mode 100644 index 0000000000..2e04f22290 --- /dev/null +++ b/core/modules/path/tests/src/Functional/PathEntityTestExternalFormTest.php @@ -0,0 +1,57 @@ +drupalLogin($this->drupalCreateUser([ + 'administer entity_test content', + 'create url aliases', + ])); + + // Create an entity_test_external entity. + $this->drupalGet('/entity_test_external/add'); + $this->submitForm([], 'Save'); + $this->assertSession()->statusMessageContains('entity_test_external 1 has been created.', 'status'); + // Update the entity. + $this->drupalGet('/entity_test_external/1/edit'); + $this->submitForm([], 'Save'); + $this->assertSession()->statusMessageContains('entity_test_external 1 has been updated.', 'status'); + // Try to set a path. + $this->drupalGet('/entity_test_external/1/edit'); + $this->submitForm(['path[0][alias]' => '/something'], 'Save'); + $this->assertSession()->statusMessageContains('An unrouted entity cannot have a path.', 'error'); + // Delete the entity. + $entity = EntityTestExternal::load('1'); + $entity->delete(); + } + +} diff --git a/core/modules/path/tests/src/Unit/Field/PathFieldItemListTest.php b/core/modules/path/tests/src/Unit/Field/PathFieldItemListTest.php new file mode 100644 index 0000000000..f6173ba84c --- /dev/null +++ b/core/modules/path/tests/src/Unit/Field/PathFieldItemListTest.php @@ -0,0 +1,130 @@ + $expected_path_alias['alias'], + 'pid' => $expected_path_alias['id'], + 'langcode' => $expected_path_alias['langcode'], + ]; + } + else { + $expected['langcode'] = 'und'; + } + + $created_value = $this->createMock('Drupal\Core\TypedData\TypedDataInterface'); + $created_value->expects($this->any()) + ->method('getValue') + ->willReturn($expected); + + $url = $this->createMock('Drupal\Core\Url'); + $url->expects($this->any()) + ->method('isRouted') + ->willReturn($is_routed); + if ($is_routed) { + $url->expects($this->any()) + ->method('getInternalPath') + ->willReturn('some_internal_path'); + } + else { + $url->expects($this->any()) + ->method('getInternalPath') + ->willThrowException(new \UnexpectedValueException()); + } + + $parent_entity = $this->createMock('Drupal\Core\Entity\EntityInterface'); + $parent_entity->expects($this->once()) + ->method('isNew') + ->willReturn($is_new); + $parent_entity->expects($this->any()) + ->method('toUrl') + ->willReturn($url); + + $parent_typed_data = $this->createMock('Drupal\Core\TypedData\TypedDataInterface'); + $parent_typed_data->expects($this->once()) + ->method('getValue') + ->willReturn($parent_entity); + + $field_definition = $this->createMock('Drupal\Core\Field\FieldDefinitionInterface'); + + $path_field_list = new PathFieldItemList($field_definition, NULL, $parent_typed_data); + + // Dependency injection is not used twice within the scope of the covered + // code. First, \Drupal::service('path_alias.repository') is called + // in PathFieldItemList::computeValue() (which itself is called from + // PathFieldItemList::getValue()). Second, + // \Drupal::service('plugin.manager.field.field_type') is called from + // PathFieldItemList::createItem() (which itself is called from + // PathFieldItemList::computeValue()). Therefore, we must mock the field + // type manager and path alias repository and set them on the container. + $field_type_manager = $this->createMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); + $field_type_manager->expects($this->any()) + ->method('createFieldItem') + ->with($path_field_list, 0, $expected) + ->willReturn($created_value); + $path_alias_repository = $this->createMock('Drupal\path_alias\AliasRepositoryInterface'); + $path_alias_repository->expects($this->any()) + ->method('lookupBySystemPath') + ->willReturn($expected_path_alias); + $container = new ContainerBuilder(); + $container->set('path_alias.repository', $path_alias_repository); + $container->set('plugin.manager.field.field_type', $field_type_manager); + \Drupal::setContainer($container); + + $this->assertSame($expected, $path_field_list->getValue()[0]); + } + + /** + * Data provider for testComputeValue. + */ + public function providerTestComputeValue(): array { + return [ + 'new entity' => [ + 'is_routed' => FALSE, + 'is_new' => TRUE, + 'expected_path_alias' => NULL, + ], + 'entity with internal path' => [ + 'is_routed' => TRUE, + 'is_new' => FALSE, + 'expected_path_alias' => [ + 'alias' => 'some_alias', + 'id' => 123, + 'langcode' => 'und', + ], + ], + 'entity without internal path' => [ + 'is_routed' => FALSE, + 'is_new' => FALSE, + 'expected_path_alias' => NULL, + ], + ]; + } + +} diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestExternal.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestExternal.php index b6587a9a9e..c2a80bdeb2 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestExternal.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestExternal.php @@ -10,14 +10,26 @@ * @ContentEntityType( * id = "entity_test_external", * label = @Translation("Entity test external"), + * handlers = { + * "access" = "Drupal\entity_test\EntityTestAccessControlHandler", + * "form" = { + * "default" = "Drupal\entity_test\EntityTestForm", + * }, + * "route_provider" = { + * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", + * }, + * }, * base_table = "entity_test_external", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "bundle" = "type", + * "label" = "name", * }, * links = { - * "canonical" = "/entity_test_external/{entity_test_external}" + * "canonical" = "/entity_test_external/{entity_test_external}", + * "add-form" = "/entity_test_external/add", + * "edit-form" = "/entity_test_external/{entity_test_external}/edit", * }, * ) */ diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon index b4aabda84a..a073f950db 100644 --- a/core/phpstan-baseline.neon +++ b/core/phpstan-baseline.neon @@ -1737,7 +1737,7 @@ parameters: - message: "#^Access to an undefined property Drupal\\\\path\\\\Plugin\\\\Field\\\\FieldType\\\\PathItem\\:\\:\\$alias\\.$#" - count: 3 + count: 4 path: modules/path/src/Plugin/Field/FieldType/PathItem.php -