Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / lib / Drupal / Core / ParamConverter / EntityConverter.php
1 <?php
2
3 namespace Drupal\Core\ParamConverter;
4
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Core\Entity\EntityManagerInterface;
7 use Drupal\Core\Entity\RevisionableInterface;
8 use Drupal\Core\Entity\TranslatableRevisionableInterface;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Language\LanguageManagerInterface;
11 use Drupal\Core\TypedData\TranslatableInterface;
12 use Symfony\Component\Routing\Route;
13
14 /**
15  * Parameter converter for upcasting entity IDs to full objects.
16  *
17  * This is useful in cases where the dynamic elements of the path can't be
18  * auto-determined; for example, if your path refers to multiple of the same
19  * type of entity ("example/{node1}/foo/{node2}") or if the path can act on any
20  * entity type ("example/{entity_type}/{entity}/foo").
21  *
22  * In order to use it you should specify some additional options in your route:
23  * @code
24  * example.route:
25  *   path: foo/{example}
26  *   options:
27  *     parameters:
28  *       example:
29  *         type: entity:node
30  * @endcode
31  *
32  * If you want to have the entity type itself dynamic in the url you can
33  * specify it like the following:
34  * @code
35  * example.route:
36  *   path: foo/{entity_type}/{example}
37  *   options:
38  *     parameters:
39  *       example:
40  *         type: entity:{entity_type}
41  * @endcode
42  *
43  * If your route needs to support pending revisions, you can specify the
44  * "load_latest_revision" parameter. This will ensure that the latest revision
45  * is returned, even if it is not the default one:
46  * @code
47  * example.route:
48  *   path: foo/{example}
49  *   options:
50  *     parameters:
51  *       example:
52  *         type: entity:node
53  *         load_latest_revision: TRUE
54  * @endcode
55  *
56  * When dealing with translatable entities, the "load_latest_revision" flag will
57  * make this converter load the latest revision affecting the translation
58  * matching the content language for the current request. If none can be found
59  * it will fall back to the latest revision. For instance, if an entity has an
60  * English default revision (revision 1) and an Italian pending revision
61  * (revision 2), "/foo/1" will return the former, while "/it/foo/1" will return
62  * the latter.
63  *
64  * @see entities_revisions_translations
65  */
66 class EntityConverter implements ParamConverterInterface {
67
68   /**
69    * Entity manager which performs the upcasting in the end.
70    *
71    * @var \Drupal\Core\Entity\EntityManagerInterface
72    */
73   protected $entityManager;
74
75   /**
76    * The language manager.
77    *
78    * @var \Drupal\Core\Language\LanguageManagerInterface
79    */
80   protected $languageManager;
81
82   /**
83    * Constructs a new EntityConverter.
84    *
85    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
86    *   The entity manager.
87    * @param \Drupal\Core\Language\LanguageManagerInterface|null $language_manager
88    *   (optional) The language manager. Defaults to none.
89    */
90   public function __construct(EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager = NULL) {
91     $this->entityManager = $entity_manager;
92     $this->languageManager = $language_manager;
93   }
94
95   /**
96    * {@inheritdoc}
97    */
98   public function convert($value, $definition, $name, array $defaults) {
99     $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
100     $storage = $this->entityManager->getStorage($entity_type_id);
101     $entity_definition = $this->entityManager->getDefinition($entity_type_id);
102
103     $entity = $storage->load($value);
104
105     // If the entity type is revisionable and the parameter has the
106     // "load_latest_revision" flag, load the latest revision.
107     if ($entity instanceof RevisionableInterface && !empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) {
108       // Retrieve the latest revision ID taking translations into account.
109       $langcode = $this->languageManager()
110         ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
111         ->getId();
112       $entity = $this->getLatestTranslationAffectedRevision($entity, $langcode);
113     }
114
115     // If the entity type is translatable, ensure we return the proper
116     // translation object for the current context.
117     if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
118       $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
119     }
120
121     return $entity;
122   }
123
124   /**
125    * Returns the ID of the latest revision translation of the specified entity.
126    *
127    * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $entity
128    *   The default revision of the entity being converted.
129    * @param string $langcode
130    *   The language of the revision translation to be loaded.
131    *
132    * @return \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface
133    *   The latest translation-affecting revision for the specified entity, or
134    *   just the latest revision, if the specified entity is not translatable or
135    *   does not have a matching translation yet.
136    */
137   protected function getLatestTranslationAffectedRevision(RevisionableInterface $entity, $langcode) {
138     $revision = NULL;
139     $storage = $this->entityManager->getStorage($entity->getEntityTypeId());
140
141     if ($entity instanceof TranslatableRevisionableInterface && $entity->isTranslatable()) {
142       /** @var \Drupal\Core\Entity\TranslatableRevisionableStorageInterface $storage */
143       $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
144
145       // If the latest translation-affecting revision was a default revision, it
146       // is fine to load the latest revision instead, because in this case the
147       // latest revision, regardless of it being default or pending, will always
148       // contain the most up-to-date values for the specified translation. This
149       // provides a BC behavior when the route is defined by a module always
150       // expecting the latest revision to be loaded and to be the default
151       // revision. In this particular case the latest revision is always going
152       // to be the default revision, since pending revisions would not be
153       // supported.
154       /** @var \Drupal\Core\Entity\TranslatableRevisionableInterface $revision */
155       $revision = $revision_id ? $this->loadRevision($entity, $revision_id) : NULL;
156       if (!$revision || ($revision->wasDefaultRevision() && !$revision->isDefaultRevision())) {
157         $revision = NULL;
158       }
159     }
160
161     // Fall back to the latest revisions if no affected revision for the current
162     // content language could be found. This is acceptable as it means the
163     // entity is not translated. This is the correct logic also on monolingual
164     // sites.
165     if (!isset($revision)) {
166       $revision_id = $storage->getLatestRevisionId($entity->id());
167       $revision = $this->loadRevision($entity, $revision_id);
168     }
169
170     return $revision;
171   }
172
173   /**
174    * Loads the specified entity revision.
175    *
176    * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $entity
177    *   The default revision of the entity being converted.
178    * @param string $revision_id
179    *   The identifier of the revision to be loaded.
180    *
181    * @return \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface
182    *   An entity revision object.
183    */
184   protected function loadRevision(RevisionableInterface $entity, $revision_id) {
185     // We explicitly perform a loose equality check, since a revision ID may
186     // be returned as an integer or a string.
187     if ($entity->getLoadedRevisionId() != $revision_id) {
188       $storage = $this->entityManager->getStorage($entity->getEntityTypeId());
189       return $storage->loadRevision($revision_id);
190     }
191     else {
192       return $entity;
193     }
194   }
195
196   /**
197    * {@inheritdoc}
198    */
199   public function applies($definition, $name, Route $route) {
200     if (!empty($definition['type']) && strpos($definition['type'], 'entity:') === 0) {
201       $entity_type_id = substr($definition['type'], strlen('entity:'));
202       if (strpos($definition['type'], '{') !== FALSE) {
203         $entity_type_slug = substr($entity_type_id, 1, -1);
204         return $name != $entity_type_slug && in_array($entity_type_slug, $route->compile()->getVariables(), TRUE);
205       }
206       return $this->entityManager->hasDefinition($entity_type_id);
207     }
208     return FALSE;
209   }
210
211   /**
212    * Determines the entity type ID given a route definition and route defaults.
213    *
214    * @param mixed $definition
215    *   The parameter definition provided in the route options.
216    * @param string $name
217    *   The name of the parameter.
218    * @param array $defaults
219    *   The route defaults array.
220    *
221    * @return string
222    *   The entity type ID.
223    *
224    * @throws \Drupal\Core\ParamConverter\ParamNotConvertedException
225    *   Thrown when the dynamic entity type is not found in the route defaults.
226    */
227   protected function getEntityTypeFromDefaults($definition, $name, array $defaults) {
228     $entity_type_id = substr($definition['type'], strlen('entity:'));
229
230     // If the entity type is dynamic, it will be pulled from the route defaults.
231     if (strpos($entity_type_id, '{') === 0) {
232       $entity_type_slug = substr($entity_type_id, 1, -1);
233       if (!isset($defaults[$entity_type_slug])) {
234         throw new ParamNotConvertedException(sprintf('The "%s" parameter was not converted because the "%s" parameter is missing', $name, $entity_type_slug));
235       }
236       $entity_type_id = $defaults[$entity_type_slug];
237     }
238     return $entity_type_id;
239   }
240
241   /**
242    * Returns a language manager instance.
243    *
244    * @return \Drupal\Core\Language\LanguageManagerInterface
245    *   The language manager.
246    *
247    * @internal
248    */
249   protected function languageManager() {
250     if (!isset($this->languageManager)) {
251       $this->languageManager = \Drupal::languageManager();
252       // @todo Turn this into a proper error (E_USER_ERROR) in
253       //   https://www.drupal.org/node/2938929.
254       @trigger_error('The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0 when requesting the latest translation-affected revision of an entity.', E_USER_DEPRECATED);
255     }
256     return $this->languageManager;
257   }
258
259 }