annotate core/modules/content_translation/src/Controller/ContentTranslationController.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\content_translation\Controller;
Chris@0 4
Chris@14 5 use Drupal\content_translation\ContentTranslationManager;
Chris@0 6 use Drupal\content_translation\ContentTranslationManagerInterface;
Chris@0 7 use Drupal\Core\Cache\CacheableMetadata;
Chris@0 8 use Drupal\Core\Controller\ControllerBase;
Chris@0 9 use Drupal\Core\Entity\ContentEntityInterface;
Chris@18 10 use Drupal\Core\Entity\EntityFieldManagerInterface;
Chris@0 11 use Drupal\Core\Language\LanguageInterface;
Chris@0 12 use Drupal\Core\Routing\RouteMatchInterface;
Chris@0 13 use Drupal\Core\Url;
Chris@0 14 use Symfony\Component\DependencyInjection\ContainerInterface;
Chris@0 15
Chris@0 16 /**
Chris@0 17 * Base class for entity translation controllers.
Chris@0 18 */
Chris@0 19 class ContentTranslationController extends ControllerBase {
Chris@0 20
Chris@0 21 /**
Chris@0 22 * The content translation manager.
Chris@0 23 *
Chris@0 24 * @var \Drupal\content_translation\ContentTranslationManagerInterface
Chris@0 25 */
Chris@0 26 protected $manager;
Chris@0 27
Chris@0 28 /**
Chris@18 29 * The entity field manager.
Chris@18 30 *
Chris@18 31 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
Chris@18 32 */
Chris@18 33 protected $entityFieldManager;
Chris@18 34
Chris@18 35 /**
Chris@0 36 * Initializes a content translation controller.
Chris@0 37 *
Chris@0 38 * @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
Chris@0 39 * A content translation manager instance.
Chris@18 40 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
Chris@18 41 * The entity field manager service.
Chris@0 42 */
Chris@18 43 public function __construct(ContentTranslationManagerInterface $manager, EntityFieldManagerInterface $entity_field_manager = NULL) {
Chris@0 44 $this->manager = $manager;
Chris@18 45 if (!$entity_field_manager) {
Chris@18 46 @trigger_error('The entity_field.manager service must be passed to ContentTranslationController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
Chris@18 47 $entity_field_manager = \Drupal::service('entity_field.manager');
Chris@18 48 }
Chris@18 49 $this->entityFieldManager = $entity_field_manager;
Chris@0 50 }
Chris@0 51
Chris@0 52 /**
Chris@0 53 * {@inheritdoc}
Chris@0 54 */
Chris@0 55 public static function create(ContainerInterface $container) {
Chris@18 56 return new static(
Chris@18 57 $container->get('content_translation.manager'),
Chris@18 58 $container->get('entity_field.manager')
Chris@18 59 );
Chris@0 60 }
Chris@0 61
Chris@0 62 /**
Chris@0 63 * Populates target values with the source values.
Chris@0 64 *
Chris@0 65 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
Chris@0 66 * The entity being translated.
Chris@0 67 * @param \Drupal\Core\Language\LanguageInterface $source
Chris@0 68 * The language to be used as source.
Chris@0 69 * @param \Drupal\Core\Language\LanguageInterface $target
Chris@0 70 * The language to be used as target.
Chris@0 71 */
Chris@0 72 public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) {
Chris@0 73 /* @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */
Chris@0 74 $source_translation = $entity->getTranslation($source->getId());
Chris@0 75 $target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray());
Chris@0 76
Chris@0 77 // Make sure we do not inherit the affected status from the source values.
Chris@0 78 if ($entity->getEntityType()->isRevisionable()) {
Chris@0 79 $target_translation->setRevisionTranslationAffected(NULL);
Chris@0 80 }
Chris@0 81
Chris@0 82 /** @var \Drupal\user\UserInterface $user */
Chris@18 83 $user = $this->entityTypeManager()->getStorage('user')->load($this->currentUser()->id());
Chris@0 84 $metadata = $this->manager->getTranslationMetadata($target_translation);
Chris@0 85
Chris@0 86 // Update the translation author to current user, as well the translation
Chris@0 87 // creation time.
Chris@0 88 $metadata->setAuthor($user);
Chris@0 89 $metadata->setCreatedTime(REQUEST_TIME);
Chris@0 90 }
Chris@0 91
Chris@0 92 /**
Chris@0 93 * Builds the translations overview page.
Chris@0 94 *
Chris@0 95 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@0 96 * The route match.
Chris@0 97 * @param string $entity_type_id
Chris@0 98 * (optional) The entity type ID.
Chris@0 99 * @return array
Chris@0 100 * Array of page elements to render.
Chris@0 101 */
Chris@0 102 public function overview(RouteMatchInterface $route_match, $entity_type_id = NULL) {
Chris@0 103 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
Chris@0 104 $entity = $route_match->getParameter($entity_type_id);
Chris@0 105 $account = $this->currentUser();
Chris@18 106 $handler = $this->entityTypeManager()->getHandler($entity_type_id, 'translation');
Chris@0 107 $manager = $this->manager;
Chris@0 108 $entity_type = $entity->getEntityType();
Chris@14 109 $use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle());
Chris@0 110
Chris@0 111 // Start collecting the cacheability metadata, starting with the entity and
Chris@0 112 // later merge in the access result cacheability metadata.
Chris@0 113 $cacheability = CacheableMetadata::createFromObject($entity);
Chris@0 114
Chris@0 115 $languages = $this->languageManager()->getLanguages();
Chris@0 116 $original = $entity->getUntranslated()->language()->getId();
Chris@0 117 $translations = $entity->getTranslationLanguages();
Chris@0 118 $field_ui = $this->moduleHandler()->moduleExists('field_ui') && $account->hasPermission('administer ' . $entity_type_id . ' fields');
Chris@0 119
Chris@0 120 $rows = [];
Chris@0 121 $show_source_column = FALSE;
Chris@14 122 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
Chris@14 123 $storage = $this->entityTypeManager()->getStorage($entity_type_id);
Chris@14 124 $default_revision = $storage->load($entity->id());
Chris@0 125
Chris@0 126 if ($this->languageManager()->isMultilingual()) {
Chris@0 127 // Determine whether the current entity is translatable.
Chris@0 128 $translatable = FALSE;
Chris@18 129 foreach ($this->entityFieldManager->getFieldDefinitions($entity_type_id, $entity->bundle()) as $instance) {
Chris@0 130 if ($instance->isTranslatable()) {
Chris@0 131 $translatable = TRUE;
Chris@0 132 break;
Chris@0 133 }
Chris@0 134 }
Chris@0 135
Chris@0 136 // Show source-language column if there are non-original source langcodes.
Chris@0 137 $additional_source_langcodes = array_filter(array_keys($translations), function ($langcode) use ($entity, $original, $manager) {
Chris@0 138 $source = $manager->getTranslationMetadata($entity->getTranslation($langcode))->getSource();
Chris@0 139 return $source != $original && $source != LanguageInterface::LANGCODE_NOT_SPECIFIED;
Chris@0 140 });
Chris@0 141 $show_source_column = !empty($additional_source_langcodes);
Chris@0 142
Chris@0 143 foreach ($languages as $language) {
Chris@0 144 $language_name = $language->getName();
Chris@0 145 $langcode = $language->getId();
Chris@0 146
Chris@14 147 // If the entity type is revisionable, we may have pending revisions
Chris@14 148 // with translations not available yet in the default revision. Thus we
Chris@14 149 // need to load the latest translation-affecting revision for each
Chris@14 150 // language to be sure we are listing all available translations.
Chris@14 151 if ($use_latest_revisions) {
Chris@14 152 $entity = $default_revision;
Chris@14 153 $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
Chris@14 154 if ($latest_revision_id) {
Chris@14 155 /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
Chris@14 156 $latest_revision = $storage->loadRevision($latest_revision_id);
Chris@14 157 // Make sure we do not list removed translations, i.e. translations
Chris@14 158 // that have been part of a default revision but no longer are.
Chris@14 159 if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) {
Chris@14 160 $entity = $latest_revision;
Chris@14 161 }
Chris@14 162 }
Chris@14 163 $translations = $entity->getTranslationLanguages();
Chris@14 164 }
Chris@14 165
Chris@17 166 $options = ['language' => $language];
Chris@17 167 $add_url = $entity->toUrl('drupal:content-translation-add', $options)
Chris@17 168 ->setRouteParameter('source', $original)
Chris@17 169 ->setRouteParameter('target', $language->getId());
Chris@17 170 $edit_url = $entity->toUrl('drupal:content-translation-edit', $options)
Chris@17 171 ->setRouteParameter('language', $language->getId());
Chris@17 172 $delete_url = $entity->toUrl('drupal:content-translation-delete', $options)
Chris@17 173 ->setRouteParameter('language', $language->getId());
Chris@0 174 $operations = [
Chris@0 175 'data' => [
Chris@0 176 '#type' => 'operations',
Chris@0 177 '#links' => [],
Chris@0 178 ],
Chris@0 179 ];
Chris@0 180
Chris@0 181 $links = &$operations['data']['#links'];
Chris@0 182 if (array_key_exists($langcode, $translations)) {
Chris@0 183 // Existing translation in the translation set: display status.
Chris@0 184 $translation = $entity->getTranslation($langcode);
Chris@0 185 $metadata = $manager->getTranslationMetadata($translation);
Chris@0 186 $source = $metadata->getSource() ?: LanguageInterface::LANGCODE_NOT_SPECIFIED;
Chris@0 187 $is_original = $langcode == $original;
Chris@0 188 $label = $entity->getTranslation($langcode)->label();
Chris@18 189 $link = isset($links->links[$langcode]['url']) ? $links->links[$langcode] : ['url' => $entity->toUrl()];
Chris@0 190 if (!empty($link['url'])) {
Chris@0 191 $link['url']->setOption('language', $language);
Chris@0 192 $row_title = $this->l($label, $link['url']);
Chris@0 193 }
Chris@0 194
Chris@0 195 if (empty($link['url'])) {
Chris@0 196 $row_title = $is_original ? $label : $this->t('n/a');
Chris@0 197 }
Chris@0 198
Chris@0 199 // If the user is allowed to edit the entity we point the edit link to
Chris@0 200 // the entity form, otherwise if we are not dealing with the original
Chris@0 201 // language we point the link to the translation form.
Chris@0 202 $update_access = $entity->access('update', NULL, TRUE);
Chris@0 203 $translation_access = $handler->getTranslationAccess($entity, 'update');
Chris@0 204 $cacheability = $cacheability
Chris@0 205 ->merge(CacheableMetadata::createFromObject($update_access))
Chris@0 206 ->merge(CacheableMetadata::createFromObject($translation_access));
Chris@0 207 if ($update_access->isAllowed() && $entity_type->hasLinkTemplate('edit-form')) {
Chris@18 208 $links['edit']['url'] = $entity->toUrl('edit-form');
Chris@0 209 $links['edit']['language'] = $language;
Chris@0 210 }
Chris@0 211 elseif (!$is_original && $translation_access->isAllowed()) {
Chris@0 212 $links['edit']['url'] = $edit_url;
Chris@0 213 }
Chris@0 214
Chris@0 215 if (isset($links['edit'])) {
Chris@0 216 $links['edit']['title'] = $this->t('Edit');
Chris@0 217 }
Chris@0 218 $status = [
Chris@0 219 'data' => [
Chris@0 220 '#type' => 'inline_template',
Chris@0 221 '#template' => '<span class="status">{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}</span>{% if outdated %} <span class="marker">{{ "outdated"|t }}</span>{% endif %}',
Chris@0 222 '#context' => [
Chris@0 223 'status' => $metadata->isPublished(),
Chris@0 224 'outdated' => $metadata->isOutdated(),
Chris@0 225 ],
Chris@0 226 ],
Chris@0 227 ];
Chris@0 228
Chris@0 229 if ($is_original) {
Chris@0 230 $language_name = $this->t('<strong>@language_name (Original language)</strong>', ['@language_name' => $language_name]);
Chris@0 231 $source_name = $this->t('n/a');
Chris@0 232 }
Chris@0 233 else {
Chris@14 234 /** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */
Chris@14 235 $delete_route_access = \Drupal::service('content_translation.delete_access')->checkAccess($translation);
Chris@14 236 $cacheability->addCacheableDependency($delete_route_access);
Chris@14 237
Chris@14 238 if ($delete_route_access->isAllowed()) {
Chris@14 239 $source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
Chris@14 240 $delete_access = $entity->access('delete', NULL, TRUE);
Chris@14 241 $translation_access = $handler->getTranslationAccess($entity, 'delete');
Chris@14 242 $cacheability
Chris@14 243 ->addCacheableDependency($delete_access)
Chris@14 244 ->addCacheableDependency($translation_access);
Chris@14 245
Chris@14 246 if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) {
Chris@14 247 $links['delete'] = [
Chris@14 248 'title' => $this->t('Delete'),
Chris@18 249 'url' => $entity->toUrl('delete-form'),
Chris@14 250 'language' => $language,
Chris@14 251 ];
Chris@14 252 }
Chris@14 253 elseif ($translation_access->isAllowed()) {
Chris@14 254 $links['delete'] = [
Chris@14 255 'title' => $this->t('Delete'),
Chris@14 256 'url' => $delete_url,
Chris@14 257 ];
Chris@14 258 }
Chris@0 259 }
Chris@14 260 else {
Chris@14 261 $this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE);
Chris@0 262 }
Chris@0 263 }
Chris@0 264 }
Chris@0 265 else {
Chris@0 266 // No such translation in the set yet: help user to create it.
Chris@0 267 $row_title = $source_name = $this->t('n/a');
Chris@0 268 $source = $entity->language()->getId();
Chris@0 269
Chris@0 270 $create_translation_access = $handler->getTranslationAccess($entity, 'create');
Chris@0 271 $cacheability = $cacheability
Chris@0 272 ->merge(CacheableMetadata::createFromObject($create_translation_access));
Chris@0 273 if ($source != $langcode && $create_translation_access->isAllowed()) {
Chris@0 274 if ($translatable) {
Chris@0 275 $links['add'] = [
Chris@0 276 'title' => $this->t('Add'),
Chris@0 277 'url' => $add_url,
Chris@0 278 ];
Chris@0 279 }
Chris@0 280 elseif ($field_ui) {
Chris@0 281 $url = new Url('language.content_settings_page');
Chris@0 282
Chris@0 283 // Link directly to the fields tab to make it easier to find the
Chris@0 284 // setting to enable translation on fields.
Chris@0 285 $links['nofields'] = [
Chris@0 286 'title' => $this->t('No translatable fields'),
Chris@0 287 'url' => $url,
Chris@0 288 ];
Chris@0 289 }
Chris@0 290 }
Chris@0 291
Chris@0 292 $status = $this->t('Not translated');
Chris@0 293 }
Chris@0 294 if ($show_source_column) {
Chris@0 295 $rows[] = [
Chris@0 296 $language_name,
Chris@0 297 $row_title,
Chris@0 298 $source_name,
Chris@0 299 $status,
Chris@0 300 $operations,
Chris@0 301 ];
Chris@0 302 }
Chris@0 303 else {
Chris@0 304 $rows[] = [$language_name, $row_title, $status, $operations];
Chris@0 305 }
Chris@0 306 }
Chris@0 307 }
Chris@0 308 if ($show_source_column) {
Chris@0 309 $header = [
Chris@0 310 $this->t('Language'),
Chris@0 311 $this->t('Translation'),
Chris@0 312 $this->t('Source language'),
Chris@0 313 $this->t('Status'),
Chris@0 314 $this->t('Operations'),
Chris@0 315 ];
Chris@0 316 }
Chris@0 317 else {
Chris@0 318 $header = [
Chris@0 319 $this->t('Language'),
Chris@0 320 $this->t('Translation'),
Chris@0 321 $this->t('Status'),
Chris@0 322 $this->t('Operations'),
Chris@0 323 ];
Chris@0 324 }
Chris@0 325
Chris@0 326 $build['#title'] = $this->t('Translations of %label', ['%label' => $entity->label()]);
Chris@0 327
Chris@0 328 // Add metadata to the build render array to let other modules know about
Chris@0 329 // which entity this is.
Chris@0 330 $build['#entity'] = $entity;
Chris@0 331 $cacheability
Chris@0 332 ->addCacheTags($entity->getCacheTags())
Chris@0 333 ->applyTo($build);
Chris@0 334
Chris@0 335 $build['content_translation_overview'] = [
Chris@0 336 '#theme' => 'table',
Chris@0 337 '#header' => $header,
Chris@0 338 '#rows' => $rows,
Chris@0 339 ];
Chris@0 340
Chris@0 341 return $build;
Chris@0 342 }
Chris@0 343
Chris@0 344 /**
Chris@0 345 * Builds an add translation page.
Chris@0 346 *
Chris@0 347 * @param \Drupal\Core\Language\LanguageInterface $source
Chris@0 348 * The language of the values being translated. Defaults to the entity
Chris@0 349 * language.
Chris@0 350 * @param \Drupal\Core\Language\LanguageInterface $target
Chris@0 351 * The language of the translated values. Defaults to the current content
Chris@0 352 * language.
Chris@0 353 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@0 354 * The route match object from which to extract the entity type.
Chris@0 355 * @param string $entity_type_id
Chris@0 356 * (optional) The entity type ID.
Chris@0 357 *
Chris@0 358 * @return array
Chris@0 359 * A processed form array ready to be rendered.
Chris@0 360 */
Chris@0 361 public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
Chris@14 362 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
Chris@0 363 $entity = $route_match->getParameter($entity_type_id);
Chris@0 364
Chris@14 365 // In case of a pending revision, make sure we load the latest
Chris@14 366 // translation-affecting revision for the source language, otherwise the
Chris@14 367 // initial form values may not be up-to-date.
Chris@14 368 if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
Chris@14 369 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
Chris@14 370 $storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId());
Chris@14 371 $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId());
Chris@14 372 if ($revision_id != $entity->getRevisionId()) {
Chris@14 373 $entity = $storage->loadRevision($revision_id);
Chris@14 374 }
Chris@14 375 }
Chris@14 376
Chris@0 377 // @todo Exploit the upcoming hook_entity_prepare() when available.
Chris@0 378 // See https://www.drupal.org/node/1810394.
Chris@0 379 $this->prepareTranslation($entity, $source, $target);
Chris@0 380
Chris@0 381 // @todo Provide a way to figure out the default form operation. Maybe like
Chris@0 382 // $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
Chris@0 383 // See https://www.drupal.org/node/2006348.
Chris@0 384
Chris@0 385 // Use the add form handler, if available, otherwise default.
Chris@0 386 $operation = $entity->getEntityType()->hasHandlerClass('form', 'add') ? 'add' : 'default';
Chris@0 387
Chris@0 388 $form_state_additions = [];
Chris@0 389 $form_state_additions['langcode'] = $target->getId();
Chris@0 390 $form_state_additions['content_translation']['source'] = $source;
Chris@0 391 $form_state_additions['content_translation']['target'] = $target;
Chris@0 392 $form_state_additions['content_translation']['translation_form'] = !$entity->access('update');
Chris@0 393
Chris@0 394 return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions);
Chris@0 395 }
Chris@0 396
Chris@0 397 /**
Chris@0 398 * Builds the edit translation page.
Chris@0 399 *
Chris@0 400 * @param \Drupal\Core\Language\LanguageInterface $language
Chris@0 401 * The language of the translated values. Defaults to the current content
Chris@0 402 * language.
Chris@0 403 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@0 404 * The route match object from which to extract the entity type.
Chris@0 405 * @param string $entity_type_id
Chris@0 406 * (optional) The entity type ID.
Chris@0 407 *
Chris@0 408 * @return array
Chris@0 409 * A processed form array ready to be rendered.
Chris@0 410 */
Chris@0 411 public function edit(LanguageInterface $language, RouteMatchInterface $route_match, $entity_type_id = NULL) {
Chris@0 412 $entity = $route_match->getParameter($entity_type_id);
Chris@0 413
Chris@0 414 // @todo Provide a way to figure out the default form operation. Maybe like
Chris@0 415 // $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
Chris@0 416 // See https://www.drupal.org/node/2006348.
Chris@0 417
Chris@0 418 // Use the edit form handler, if available, otherwise default.
Chris@0 419 $operation = $entity->getEntityType()->hasHandlerClass('form', 'edit') ? 'edit' : 'default';
Chris@0 420
Chris@0 421 $form_state_additions = [];
Chris@0 422 $form_state_additions['langcode'] = $language->getId();
Chris@0 423 $form_state_additions['content_translation']['translation_form'] = TRUE;
Chris@0 424
Chris@0 425 return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions);
Chris@0 426 }
Chris@0 427
Chris@0 428 }