DefaultHtmlRouteProvider.php 13 KB

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