3 namespace Drupal\Core\Entity\Routing;
5 use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
6 use Drupal\Core\Entity\Controller\EntityController;
7 use Drupal\Core\Entity\EntityFieldManagerInterface;
8 use Drupal\Core\Entity\EntityHandlerInterface;
9 use Drupal\Core\Entity\EntityTypeInterface;
10 use Drupal\Core\Entity\EntityTypeManagerInterface;
11 use Drupal\Core\Entity\FieldableEntityInterface;
12 use Symfony\Component\DependencyInjection\ContainerInterface;
13 use Symfony\Component\Routing\Route;
14 use Symfony\Component\Routing\RouteCollection;
17 * Provides HTML routes for entities.
19 * This class provides the following routes for entities, with title and access
28 * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider.
30 class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
33 * The entity type manager.
35 * @var \Drupal\Core\Entity\EntityManagerInterface
37 protected $entityTypeManager;
40 * The entity field manager.
42 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
44 protected $entityFieldManager;
47 * Constructs a new DefaultHtmlRouteProvider.
49 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
50 * The entity type manager.
51 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
52 * The entity field manager.
54 public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
55 $this->entityTypeManager = $entity_type_manager;
56 $this->entityFieldManager = $entity_field_manager;
62 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
64 $container->get('entity_type.manager'),
65 $container->get('entity_field.manager')
72 public function getRoutes(EntityTypeInterface $entity_type) {
73 $collection = new RouteCollection();
75 $entity_type_id = $entity_type->id();
77 if ($add_page_route = $this->getAddPageRoute($entity_type)) {
78 $collection->add("entity.{$entity_type_id}.add_page", $add_page_route);
81 if ($add_form_route = $this->getAddFormRoute($entity_type)) {
82 $collection->add("entity.{$entity_type_id}.add_form", $add_form_route);
85 if ($canonical_route = $this->getCanonicalRoute($entity_type)) {
86 $collection->add("entity.{$entity_type_id}.canonical", $canonical_route);
89 if ($edit_route = $this->getEditFormRoute($entity_type)) {
90 $collection->add("entity.{$entity_type_id}.edit_form", $edit_route);
93 if ($delete_route = $this->getDeleteFormRoute($entity_type)) {
94 $collection->add("entity.{$entity_type_id}.delete_form", $delete_route);
97 if ($collection_route = $this->getCollectionRoute($entity_type)) {
98 $collection->add("entity.{$entity_type_id}.collection", $collection_route);
105 * Gets the add page route.
107 * Built only for entity types that have bundles.
109 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
112 * @return \Symfony\Component\Routing\Route|null
113 * The generated route, if available.
115 protected function getAddPageRoute(EntityTypeInterface $entity_type) {
116 if ($entity_type->hasLinkTemplate('add-page') && $entity_type->getKey('bundle')) {
117 $route = new Route($entity_type->getLinkTemplate('add-page'));
118 $route->setDefault('_controller', EntityController::class . '::addPage');
119 $route->setDefault('_title_callback', EntityController::class . '::addTitle');
120 $route->setDefault('entity_type_id', $entity_type->id());
121 $route->setRequirement('_entity_create_any_access', $entity_type->id());
128 * Gets the add-form route.
130 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
133 * @return \Symfony\Component\Routing\Route|null
134 * The generated route, if available.
136 protected function getAddFormRoute(EntityTypeInterface $entity_type) {
137 if ($entity_type->hasLinkTemplate('add-form')) {
138 $entity_type_id = $entity_type->id();
139 $route = new Route($entity_type->getLinkTemplate('add-form'));
140 // Use the add form handler, if available, otherwise default.
141 $operation = 'default';
142 if ($entity_type->getFormClass('add')) {
145 $route->setDefaults([
146 '_entity_form' => "{$entity_type_id}.{$operation}",
147 'entity_type_id' => $entity_type_id,
150 // If the entity has bundles, we can provide a bundle-specific title
151 // and access requirements.
152 $expected_parameter = $entity_type->getBundleEntityType() ?: $entity_type->getKey('bundle');
153 // @todo: We have to check if a route contains a bundle in its path as
154 // test entities have inconsistent usage of "add-form" link templates.
155 // Fix it in https://www.drupal.org/node/2699959.
156 if (($bundle_key = $entity_type->getKey('bundle')) && strpos($route->getPath(), '{' . $expected_parameter . '}') !== FALSE) {
157 $route->setDefault('_title_callback', EntityController::class . '::addBundleTitle');
158 // If the bundles are entities themselves, we can add parameter
159 // information to the route options.
160 if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
161 $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
164 // The title callback uses the value of the bundle parameter to
165 // fetch the respective bundle at runtime.
166 ->setDefault('bundle_parameter', $bundle_entity_type_id)
167 ->setRequirement('_entity_create_access', $entity_type_id . ':{' . $bundle_entity_type_id . '}');
169 // Entity types with serial IDs can specify this in their route
170 // requirements, improving the matching process.
171 if ($this->getEntityTypeIdKeyType($bundle_entity_type) === 'integer') {
172 $route->setRequirement($entity_type_id, '\d+');
175 $bundle_entity_parameter = ['type' => 'entity:' . $bundle_entity_type_id];
176 if ($bundle_entity_type instanceof ConfigEntityTypeInterface) {
177 // The add page might be displayed on an admin path. Even then, we
178 // need to load configuration overrides so that, for example, the
179 // bundle label gets translated correctly.
180 // @see \Drupal\Core\ParamConverter\AdminPathConfigEntityConverter
181 $bundle_entity_parameter['with_config_overrides'] = TRUE;
183 $route->setOption('parameters', [$bundle_entity_type_id => $bundle_entity_parameter]);
186 // If the bundles are not entities, the bundle key is used as the
187 // route parameter name directly.
189 ->setDefault('bundle_parameter', $bundle_key)
190 ->setRequirement('_entity_create_access', $entity_type_id . ':{' . $bundle_key . '}');
195 ->setDefault('_title_callback', EntityController::class . '::addTitle')
196 ->setRequirement('_entity_create_access', $entity_type_id);
204 * Gets the canonical route.
206 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
209 * @return \Symfony\Component\Routing\Route|null
210 * The generated route, if available.
212 protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
213 if ($entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass()) {
214 $entity_type_id = $entity_type->id();
215 $route = new Route($entity_type->getLinkTemplate('canonical'));
218 '_entity_view' => "{$entity_type_id}.full",
219 '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
221 ->setRequirement('_entity_access', "{$entity_type_id}.view")
222 ->setOption('parameters', [
223 $entity_type_id => ['type' => 'entity:' . $entity_type_id],
226 // Entity types with serial IDs can specify this in their route
227 // requirements, improving the matching process.
228 if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
229 $route->setRequirement($entity_type_id, '\d+');
236 * Gets the edit-form route.
238 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
241 * @return \Symfony\Component\Routing\Route|null
242 * The generated route, if available.
244 protected function getEditFormRoute(EntityTypeInterface $entity_type) {
245 if ($entity_type->hasLinkTemplate('edit-form')) {
246 $entity_type_id = $entity_type->id();
247 $route = new Route($entity_type->getLinkTemplate('edit-form'));
248 // Use the edit form handler, if available, otherwise default.
249 $operation = 'default';
250 if ($entity_type->getFormClass('edit')) {
255 '_entity_form' => "{$entity_type_id}.{$operation}",
256 '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::editTitle'
258 ->setRequirement('_entity_access', "{$entity_type_id}.update")
259 ->setOption('parameters', [
260 $entity_type_id => ['type' => 'entity:' . $entity_type_id],
263 // Entity types with serial IDs can specify this in their route
264 // requirements, improving the matching process.
265 if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
266 $route->setRequirement($entity_type_id, '\d+');
273 * Gets the delete-form route.
275 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
278 * @return \Symfony\Component\Routing\Route|null
279 * The generated route, if available.
281 protected function getDeleteFormRoute(EntityTypeInterface $entity_type) {
282 if ($entity_type->hasLinkTemplate('delete-form')) {
283 $entity_type_id = $entity_type->id();
284 $route = new Route($entity_type->getLinkTemplate('delete-form'));
287 '_entity_form' => "{$entity_type_id}.delete",
288 '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::deleteTitle',
290 ->setRequirement('_entity_access', "{$entity_type_id}.delete")
291 ->setOption('parameters', [
292 $entity_type_id => ['type' => 'entity:' . $entity_type_id],
295 // Entity types with serial IDs can specify this in their route
296 // requirements, improving the matching process.
297 if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
298 $route->setRequirement($entity_type_id, '\d+');
305 * Gets the collection route.
307 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
310 * @return \Symfony\Component\Routing\Route|null
311 * The generated route, if available.
313 protected function getCollectionRoute(EntityTypeInterface $entity_type) {
314 // If the entity type does not provide an admin permission, there is no way
315 // to control access, so we cannot provide a route in a sensible way.
316 if ($entity_type->hasLinkTemplate('collection') && $entity_type->hasListBuilderClass() && ($admin_permission = $entity_type->getAdminPermission())) {
317 /** @var \Drupal\Core\StringTranslation\TranslatableMarkup $label */
318 $label = $entity_type->getCollectionLabel();
320 $route = new Route($entity_type->getLinkTemplate('collection'));
323 '_entity_list' => $entity_type->id(),
324 '_title' => $label->getUntranslatedString(),
325 '_title_arguments' => $label->getArguments(),
326 '_title_context' => $label->getOption('context'),
328 ->setRequirement('_permission', $admin_permission);
335 * Gets the type of the ID key for a given entity type.
337 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
340 * @return string|null
341 * The type of the ID key for a given entity type, or NULL if the entity
342 * type does not support fields.
344 protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
345 if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
349 $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
350 return $field_storage_definitions[$entity_type->getKey('id')]->getType();