DefaultHtmlRouteProvider.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. namespace Drupal\Core\Entity\Routing;
  3. use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
  4. use Drupal\Core\Entity\Controller\EntityController;
  5. use Drupal\Core\Entity\EntityFieldManagerInterface;
  6. use Drupal\Core\Entity\EntityHandlerInterface;
  7. use Drupal\Core\Entity\EntityTypeInterface;
  8. use Drupal\Core\Entity\EntityTypeManagerInterface;
  9. use Drupal\Core\Entity\FieldableEntityInterface;
  10. use Symfony\Component\DependencyInjection\ContainerInterface;
  11. use Symfony\Component\Routing\Route;
  12. use Symfony\Component\Routing\RouteCollection;
  13. /**
  14. * Provides HTML routes for entities.
  15. *
  16. * This class provides the following routes for entities, with title and access
  17. * callbacks:
  18. * - canonical
  19. * - add-page
  20. * - add-form
  21. * - edit-form
  22. * - delete-form
  23. * - collection
  24. * - delete-multiple-form
  25. *
  26. * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider.
  27. */
  28. class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
  29. /**
  30. * The entity type manager.
  31. *
  32. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  33. */
  34. protected $entityTypeManager;
  35. /**
  36. * The entity field manager.
  37. *
  38. * @var \Drupal\Core\Entity\EntityFieldManagerInterface
  39. */
  40. protected $entityFieldManager;
  41. /**
  42. * Constructs a new DefaultHtmlRouteProvider.
  43. *
  44. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  45. * The entity type manager.
  46. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
  47. * The entity field manager.
  48. */
  49. public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
  50. $this->entityTypeManager = $entity_type_manager;
  51. $this->entityFieldManager = $entity_field_manager;
  52. }
  53. /**
  54. * {@inheritdoc}
  55. */
  56. public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
  57. return new static(
  58. $container->get('entity_type.manager'),
  59. $container->get('entity_field.manager')
  60. );
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public function getRoutes(EntityTypeInterface $entity_type) {
  66. $collection = new RouteCollection();
  67. $entity_type_id = $entity_type->id();
  68. if ($add_page_route = $this->getAddPageRoute($entity_type)) {
  69. $collection->add("entity.{$entity_type_id}.add_page", $add_page_route);
  70. }
  71. if ($add_form_route = $this->getAddFormRoute($entity_type)) {
  72. $collection->add("entity.{$entity_type_id}.add_form", $add_form_route);
  73. }
  74. if ($canonical_route = $this->getCanonicalRoute($entity_type)) {
  75. $collection->add("entity.{$entity_type_id}.canonical", $canonical_route);
  76. }
  77. if ($edit_route = $this->getEditFormRoute($entity_type)) {
  78. $collection->add("entity.{$entity_type_id}.edit_form", $edit_route);
  79. }
  80. if ($delete_route = $this->getDeleteFormRoute($entity_type)) {
  81. $collection->add("entity.{$entity_type_id}.delete_form", $delete_route);
  82. }
  83. if ($collection_route = $this->getCollectionRoute($entity_type)) {
  84. $collection->add("entity.{$entity_type_id}.collection", $collection_route);
  85. }
  86. if ($delete_multiple_route = $this->getDeleteMultipleFormRoute($entity_type)) {
  87. $collection->add('entity.' . $entity_type->id() . '.delete_multiple_form', $delete_multiple_route);
  88. }
  89. return $collection;
  90. }
  91. /**
  92. * Gets the add page route.
  93. *
  94. * Built only for entity types that have bundles.
  95. *
  96. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  97. * The entity type.
  98. *
  99. * @return \Symfony\Component\Routing\Route|null
  100. * The generated route, if available.
  101. */
  102. protected function getAddPageRoute(EntityTypeInterface $entity_type) {
  103. if ($entity_type->hasLinkTemplate('add-page') && $entity_type->getKey('bundle')) {
  104. $route = new Route($entity_type->getLinkTemplate('add-page'));
  105. $route->setDefault('_controller', EntityController::class . '::addPage');
  106. $route->setDefault('_title_callback', EntityController::class . '::addTitle');
  107. $route->setDefault('entity_type_id', $entity_type->id());
  108. $route->setRequirement('_entity_create_any_access', $entity_type->id());
  109. return $route;
  110. }
  111. }
  112. /**
  113. * Gets the add-form route.
  114. *
  115. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  116. * The entity type.
  117. *
  118. * @return \Symfony\Component\Routing\Route|null
  119. * The generated route, if available.
  120. */
  121. protected function getAddFormRoute(EntityTypeInterface $entity_type) {
  122. if ($entity_type->hasLinkTemplate('add-form')) {
  123. $entity_type_id = $entity_type->id();
  124. $route = new Route($entity_type->getLinkTemplate('add-form'));
  125. // Use the add form handler, if available, otherwise default.
  126. $operation = 'default';
  127. if ($entity_type->getFormClass('add')) {
  128. $operation = 'add';
  129. }
  130. $route->setDefaults([
  131. '_entity_form' => "{$entity_type_id}.{$operation}",
  132. 'entity_type_id' => $entity_type_id,
  133. ]);
  134. // If the entity has bundles, we can provide a bundle-specific title
  135. // and access requirements.
  136. $expected_parameter = $entity_type->getBundleEntityType() ?: $entity_type->getKey('bundle');
  137. // @todo: We have to check if a route contains a bundle in its path as
  138. // test entities have inconsistent usage of "add-form" link templates.
  139. // Fix it in https://www.drupal.org/node/2699959.
  140. if (($bundle_key = $entity_type->getKey('bundle')) && strpos($route->getPath(), '{' . $expected_parameter . '}') !== FALSE) {
  141. $route->setDefault('_title_callback', EntityController::class . '::addBundleTitle');
  142. // If the bundles are entities themselves, we can add parameter
  143. // information to the route options.
  144. if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
  145. $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
  146. $route
  147. // The title callback uses the value of the bundle parameter to
  148. // fetch the respective bundle at runtime.
  149. ->setDefault('bundle_parameter', $bundle_entity_type_id)
  150. ->setRequirement('_entity_create_access', $entity_type_id . ':{' . $bundle_entity_type_id . '}');
  151. // Entity types with serial IDs can specify this in their route
  152. // requirements, improving the matching process.
  153. if ($this->getEntityTypeIdKeyType($bundle_entity_type) === 'integer') {
  154. $route->setRequirement($entity_type_id, '\d+');
  155. }
  156. $bundle_entity_parameter = ['type' => 'entity:' . $bundle_entity_type_id];
  157. if ($bundle_entity_type instanceof ConfigEntityTypeInterface) {
  158. // The add page might be displayed on an admin path. Even then, we
  159. // need to load configuration overrides so that, for example, the
  160. // bundle label gets translated correctly.
  161. // @see \Drupal\Core\ParamConverter\AdminPathConfigEntityConverter
  162. $bundle_entity_parameter['with_config_overrides'] = TRUE;
  163. }
  164. $route->setOption('parameters', [$bundle_entity_type_id => $bundle_entity_parameter]);
  165. }
  166. else {
  167. // If the bundles are not entities, the bundle key is used as the
  168. // route parameter name directly.
  169. $route
  170. ->setDefault('bundle_parameter', $bundle_key)
  171. ->setRequirement('_entity_create_access', $entity_type_id . ':{' . $bundle_key . '}');
  172. }
  173. }
  174. else {
  175. $route
  176. ->setDefault('_title_callback', EntityController::class . '::addTitle')
  177. ->setRequirement('_entity_create_access', $entity_type_id);
  178. }
  179. return $route;
  180. }
  181. }
  182. /**
  183. * Gets the canonical route.
  184. *
  185. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  186. * The entity type.
  187. *
  188. * @return \Symfony\Component\Routing\Route|null
  189. * The generated route, if available.
  190. */
  191. protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
  192. if ($entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass()) {
  193. $entity_type_id = $entity_type->id();
  194. $route = new Route($entity_type->getLinkTemplate('canonical'));
  195. $route
  196. ->addDefaults([
  197. '_entity_view' => "{$entity_type_id}.full",
  198. '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
  199. ])
  200. ->setRequirement('_entity_access', "{$entity_type_id}.view")
  201. ->setOption('parameters', [
  202. $entity_type_id => ['type' => 'entity:' . $entity_type_id],
  203. ]);
  204. // Entity types with serial IDs can specify this in their route
  205. // requirements, improving the matching process.
  206. if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
  207. $route->setRequirement($entity_type_id, '\d+');
  208. }
  209. return $route;
  210. }
  211. }
  212. /**
  213. * Gets the edit-form route.
  214. *
  215. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  216. * The entity type.
  217. *
  218. * @return \Symfony\Component\Routing\Route|null
  219. * The generated route, if available.
  220. */
  221. protected function getEditFormRoute(EntityTypeInterface $entity_type) {
  222. if ($entity_type->hasLinkTemplate('edit-form')) {
  223. $entity_type_id = $entity_type->id();
  224. $route = new Route($entity_type->getLinkTemplate('edit-form'));
  225. // Use the edit form handler, if available, otherwise default.
  226. $operation = 'default';
  227. if ($entity_type->getFormClass('edit')) {
  228. $operation = 'edit';
  229. }
  230. $route
  231. ->setDefaults([
  232. '_entity_form' => "{$entity_type_id}.{$operation}",
  233. '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::editTitle',
  234. ])
  235. ->setRequirement('_entity_access', "{$entity_type_id}.update")
  236. ->setOption('parameters', [
  237. $entity_type_id => ['type' => 'entity:' . $entity_type_id],
  238. ]);
  239. // Entity types with serial IDs can specify this in their route
  240. // requirements, improving the matching process.
  241. if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
  242. $route->setRequirement($entity_type_id, '\d+');
  243. }
  244. return $route;
  245. }
  246. }
  247. /**
  248. * Gets the delete-form route.
  249. *
  250. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  251. * The entity type.
  252. *
  253. * @return \Symfony\Component\Routing\Route|null
  254. * The generated route, if available.
  255. */
  256. protected function getDeleteFormRoute(EntityTypeInterface $entity_type) {
  257. if ($entity_type->hasLinkTemplate('delete-form')) {
  258. $entity_type_id = $entity_type->id();
  259. $route = new Route($entity_type->getLinkTemplate('delete-form'));
  260. $route
  261. ->addDefaults([
  262. '_entity_form' => "{$entity_type_id}.delete",
  263. '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::deleteTitle',
  264. ])
  265. ->setRequirement('_entity_access', "{$entity_type_id}.delete")
  266. ->setOption('parameters', [
  267. $entity_type_id => ['type' => 'entity:' . $entity_type_id],
  268. ]);
  269. // Entity types with serial IDs can specify this in their route
  270. // requirements, improving the matching process.
  271. if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
  272. $route->setRequirement($entity_type_id, '\d+');
  273. }
  274. return $route;
  275. }
  276. }
  277. /**
  278. * Gets the collection route.
  279. *
  280. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  281. * The entity type.
  282. *
  283. * @return \Symfony\Component\Routing\Route|null
  284. * The generated route, if available.
  285. */
  286. protected function getCollectionRoute(EntityTypeInterface $entity_type) {
  287. // If the entity type does not provide an admin permission, there is no way
  288. // to control access, so we cannot provide a route in a sensible way.
  289. if ($entity_type->hasLinkTemplate('collection') && $entity_type->hasListBuilderClass() && ($admin_permission = $entity_type->getAdminPermission())) {
  290. /** @var \Drupal\Core\StringTranslation\TranslatableMarkup $label */
  291. $label = $entity_type->getCollectionLabel();
  292. $route = new Route($entity_type->getLinkTemplate('collection'));
  293. $route
  294. ->addDefaults([
  295. '_entity_list' => $entity_type->id(),
  296. '_title' => $label->getUntranslatedString(),
  297. '_title_arguments' => $label->getArguments(),
  298. '_title_context' => $label->getOption('context'),
  299. ])
  300. ->setRequirement('_permission', $admin_permission);
  301. return $route;
  302. }
  303. }
  304. /**
  305. * Gets the type of the ID key for a given entity type.
  306. *
  307. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  308. * An entity type.
  309. *
  310. * @return string|null
  311. * The type of the ID key for a given entity type, or NULL if the entity
  312. * type does not support fields.
  313. */
  314. protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
  315. if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
  316. return NULL;
  317. }
  318. $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
  319. return $field_storage_definitions[$entity_type->getKey('id')]->getType();
  320. }
  321. /**
  322. * Returns the delete multiple form route.
  323. *
  324. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  325. * The entity type.
  326. *
  327. * @return \Symfony\Component\Routing\Route|null
  328. * The generated route, if available.
  329. */
  330. protected function getDeleteMultipleFormRoute(EntityTypeInterface $entity_type) {
  331. if ($entity_type->hasLinkTemplate('delete-multiple-form') && $entity_type->hasHandlerClass('form', 'delete-multiple-confirm')) {
  332. $route = new Route($entity_type->getLinkTemplate('delete-multiple-form'));
  333. $route->setDefault('_form', $entity_type->getFormClass('delete-multiple-confirm'));
  334. $route->setDefault('entity_type_id', $entity_type->id());
  335. $route->setRequirement('_entity_delete_multiple_access', $entity_type->id());
  336. return $route;
  337. }
  338. }
  339. }