diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 9d0e5d63b10398d581ef51756c1d51f113a5b45d..f554197c635ca741eaf6ef49e1b7343244e98bfe 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -138,12 +138,19 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C protected $isDefaultRevision = TRUE; /** - * Holds entity keys like the ID, bundle and revision ID. + * Holds translatable entity keys such as the ID, bundle and revision ID. * * @var array */ protected $entityKeys = array(); + /** + * Holds translatable entity keys such as the label. + * + * @var array + */ + protected $translatableEntityKeys = array(); + /** * Overrides Entity::__construct(). */ @@ -165,14 +172,36 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans $this->values = $values; foreach ($this->getEntityType()->getKeys() as $key => $field_name) { if (isset($this->values[$field_name])) { - if (is_array($this->values[$field_name]) && isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) { - if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) { - if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) { - $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value']; + if (is_array($this->values[$field_name])) { + // We store untranslatable fields into an entity key without using a + // langcode key. + if (!$this->getFieldDefinition($field_name)->isTranslatable()) { + if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) { + if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) { + if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) { + $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value']; + } + } + else { + $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT]; + } } } else { - $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT]; + // We save translatable fields such as the publishing status of a node + // into an entity key array keyed by langcode as a performance + // optimization, so we don't have to go through TypedData when we + // need these values. + foreach ($this->values[$field_name] as $langcode => $field_value) { + if (is_array($this->values[$field_name][$langcode])) { + if (isset($this->values[$field_name][$langcode][0]['value'])) { + $this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode][0]['value']; + } + } + else { + $this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode]; + } + } } } } @@ -537,12 +566,12 @@ protected function setDefaultLangcode() { // Get the language code if the property exists. // Try to read the value directly from the list of entity keys which got // initialized in __construct(). This avoids creating a field item object. - if (isset($this->entityKeys['langcode'])) { - $this->defaultLangcode = $this->entityKeys['langcode']; + if (isset($this->translatableEntityKeys['langcode'][$this->activeLangcode])) { + $this->defaultLangcode = $this->translatableEntityKeys['langcode'][$this->activeLangcode]; } elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) { $this->defaultLangcode = $item->language->getId(); - $this->entityKeys['langcode'] = $this->defaultLangcode; + $this->translatableEntityKeys['langcode'][$this->activeLangcode] = $this->defaultLangcode; } if (empty($this->defaultLangcode)) { @@ -583,8 +612,13 @@ public function onChange($name) { // that check, as it ready only and must not change, unsetting it could // lead to recursions. if ($key = array_search($name, $this->getEntityType()->getKeys())) { - if (isset($this->entityKeys[$key]) && $key != 'bundle') { - unset($this->entityKeys[$key]); + if ($key != 'bundle') { + if (isset($this->entityKeys[$key])) { + unset($this->entityKeys[$key]); + } + elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) { + unset($this->translatableEntityKeys[$key][$this->activeLangcode]); + } } } @@ -710,8 +744,6 @@ protected function initializeTranslation($langcode) { $translation->enforceIsNew = &$this->enforceIsNew; $translation->newRevision = &$this->newRevision; $translation->translationInitialize = FALSE; - // Reset language-dependent properties. - unset($translation->entityKeys['label']); $translation->typedData = NULL; return $translation; @@ -1020,18 +1052,34 @@ public function referencedEntities() { * The value of the entity key, NULL if not defined. */ protected function getEntityKey($key) { - if (!isset($this->entityKeys[$key]) || !array_key_exists($key, $this->entityKeys)) { - if ($this->getEntityType()->hasKey($key)) { - $field_name = $this->getEntityType()->getKey($key); - $property = $this->getFieldDefinition($field_name)->getFieldStorageDefinition()->getMainPropertyName(); - $this->entityKeys[$key] = $this->get($field_name)->$property; + // If the value is known already, return it. + if (isset($this->entityKeys[$key])) { + return $this->entityKeys[$key]; + } + if (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) { + return $this->translatableEntityKeys[$key][$this->activeLangcode]; + } + + // Otherwise fetch the value by creating a field object. + $value = NULL; + if ($this->getEntityType()->hasKey($key)) { + $field_name = $this->getEntityType()->getKey($key); + $definition = $this->getFieldDefinition($field_name); + $property = $definition->getFieldStorageDefinition()->getMainPropertyName(); + $value = $this->get($field_name)->$property; + + // Put it in the right array, depending on whether it is translatable. + if ($definition->isTranslatable()) { + $this->translatableEntityKeys[$key][$this->activeLangcode] = $value; } else { - $this->entityKeys[$key] = NULL; + $this->entityKeys[$key] = $value; } - } - return $this->entityKeys[$key]; + else { + $this->entityKeys[$key] = $value; + } + return $value; } /** diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 2f57843bf4b9313c84881980db3688545b1a5f50..afa31e3c797b0fca32d8b98c1bb005b11e1dd2a9 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -53,7 +53,9 @@ * "bundle" = "type", * "label" = "title", * "langcode" = "langcode", - * "uuid" = "uuid" + * "uuid" = "uuid", + * "status" = "status", + * "uid" = "uid", * }, * bundle_entity_type = "node_type", * field_ui_base_route = "entity.node_type.edit_form", @@ -72,6 +74,13 @@ class Node extends ContentEntityBase implements NodeInterface { use EntityChangedTrait; + /** + * Whether the node is being previewed or not. + * + * @var true|null + */ + public $in_preview = NULL; + /** * {@inheritdoc} */ @@ -258,7 +267,7 @@ public function setSticky($sticky) { * {@inheritdoc} */ public function isPublished() { - return (bool) $this->get('status')->value; + return (bool) $this->getEntityKey('status'); } /** @@ -280,7 +289,7 @@ public function getOwner() { * {@inheritdoc} */ public function getOwnerId() { - return $this->get('uid')->target_id; + return $this->getEntityKey('uid'); } /** diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php index 33dec701f940d8c37716672448b492164d10d33b..c5846606831da4ba23fb730eba94476f51752874 100644 --- a/core/modules/node/src/NodeForm.php +++ b/core/modules/node/src/NodeForm.php @@ -7,7 +7,6 @@ namespace Drupal\node; -use Drupal\Component\Utility\Html; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; @@ -204,9 +203,32 @@ public function form(array $form, FormStateInterface $form_state) { $form['#attached']['library'][] = 'node/form'; + $form['#entity_builders']['update_status'] = [$this, 'updateStatus']; + return $form; } + /** + * Entity builder updating the node status with the submitted value. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\node\NodeInterface $node + * The node updated with the submitted values. + * @param array $form + * The complete form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see \Drupal\node\NodeForm::form() + */ + function updateStatus($entity_type_id, NodeInterface $node, array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + if (isset($element['#published_status'])) { + $node->setPublished($element['#published_status']); + } + } + /** * {@inheritdoc} */ @@ -232,6 +254,8 @@ protected function actions(array $form, FormStateInterface $form_state) { // Add a "Publish" button. $element['publish'] = $element['submit']; + // If the "Publish" button is clicked, we want to update the status to "published". + $element['publish']['#published_status'] = TRUE; $element['publish']['#dropbutton'] = 'save'; if ($node->isNew()) { $element['publish']['#value'] = t('Save and publish'); @@ -240,10 +264,11 @@ protected function actions(array $form, FormStateInterface $form_state) { $element['publish']['#value'] = $node->isPublished() ? t('Save and keep published') : t('Save and publish'); } $element['publish']['#weight'] = 0; - array_unshift($element['publish']['#submit'], '::publish'); // Add a "Unpublish" button. $element['unpublish'] = $element['submit']; + // If the "Unpublish" button is clicked, we want to update the status to "unpublished". + $element['unpublish']['#published_status'] = FALSE; $element['unpublish']['#dropbutton'] = 'save'; if ($node->isNew()) { $element['unpublish']['#value'] = t('Save as unpublished'); @@ -252,7 +277,6 @@ protected function actions(array $form, FormStateInterface $form_state) { $element['unpublish']['#value'] = !$node->isPublished() ? t('Save and keep unpublished') : t('Save and unpublish'); } $element['unpublish']['#weight'] = 10; - array_unshift($element['unpublish']['#submit'], '::unpublish'); // If already published, the 'publish' button is primary. if ($node->isPublished()) { @@ -327,34 +351,6 @@ public function preview(array $form, FormStateInterface $form_state) { )); } - /** - * Form submission handler for the 'publish' action. - * - * @param $form - * An associative array containing the structure of the form. - * @param $form_state - * The current state of the form. - */ - public function publish(array $form, FormStateInterface $form_state) { - $node = $this->entity; - $node->setPublished(TRUE); - return $node; - } - - /** - * Form submission handler for the 'unpublish' action. - * - * @param $form - * An associative array containing the structure of the form. - * @param $form_state - * The current state of the form. - */ - public function unpublish(array $form, FormStateInterface $form_state) { - $node = $this->entity; - $node->setPublished(FALSE); - return $node; - } - /** * {@inheritdoc} */ diff --git a/core/modules/node/src/NodeTranslationHandler.php b/core/modules/node/src/NodeTranslationHandler.php index 3692c8b767ee60a13af5aa0ba3474f5ea6ce3862..41c6909535219432a6e1fdf2218d40639d2d825a 100644 --- a/core/modules/node/src/NodeTranslationHandler.php +++ b/core/modules/node/src/NodeTranslationHandler.php @@ -7,8 +7,8 @@ namespace Drupal\node; -use Drupal\Core\Entity\EntityInterface; use Drupal\content_translation\ContentTranslationHandler; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; /** @@ -76,9 +76,8 @@ protected function entityFormTitle(EntityInterface $entity) { */ public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) { if ($form_state->hasValue('content_translation')) { - $form_object = $form_state->getFormObject(); $translation = &$form_state->getValue('content_translation'); - $translation['status'] = $form_object->getEntity()->isPublished(); + $translation['status'] = $entity->isPublished(); // $form['content_translation']['name'] is the equivalent field // for translation author uid. $account = $entity->uid->entity; diff --git a/core/modules/node/src/Tests/NodeTranslationUITest.php b/core/modules/node/src/Tests/NodeTranslationUITest.php index 608ab43e406b8fb1d942980adc978ff01354f083..287920bf6937270f0c125c86d982e81dc5081419 100644 --- a/core/modules/node/src/Tests/NodeTranslationUITest.php +++ b/core/modules/node/src/Tests/NodeTranslationUITest.php @@ -12,6 +12,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Url; use Drupal\node\Entity\Node; +use Drupal\language\Entity\ConfigurableLanguage; /** * Tests the Node Translation UI. @@ -57,6 +58,42 @@ function testTranslationUI() { $this->doUninstallTest(); } + /** + * Tests changing the published status on a node without fields. + */ + function testPublishedStatusNoFields() { + // Test changing the published status of an article without fields. + $this->drupalLogin($this->administrator); + // Delete all fields. + $this->drupalGet('admin/structure/types/manage/article/fields'); + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete', array(), t('Delete')); + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_tags/delete', array(), t('Delete')); + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/delete', array(), t('Delete')); + + // Add a node. + $default_langcode = $this->langcodes[0]; + $values[$default_langcode] = array('title' => array(array('value' => $this->randomMachineName()))); + $entity_id = $this->createEntity($values[$default_langcode], $default_langcode); + $entity = entity_load($this->entityTypeId, $entity_id, TRUE); + + // Add a content translation. + $langcode = 'fr'; + $language = ConfigurableLanguage::load($langcode); + $values[$langcode] = array('title' => array(array('value' => $this->randomMachineName()))); + + $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $default_langcode, + 'target' => $langcode + ], array('language' => $language)); + $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), t('Save and unpublish (this translation)')); + + $entity = entity_load($this->entityTypeId, $this->entityId, TRUE); + $translation = $entity->getTranslation($langcode); + // Make sure we unpublished the node correctly. + $this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.'); + } + /** * Overrides \Drupal\content_translation\Tests\ContentTranslationUITestBase::getTranslatorPermission(). */ diff --git a/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php b/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php index 7f3a66ffec1c7f2947189727b8d170aa2b7a1988..215f60ef2bf0ad745c6f6724188a3bc28844ff6c 100644 --- a/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php @@ -57,6 +57,8 @@ public function testInstaller() { $this->assertText('German'); $this->assertNoText('English'); + // The current container still has the english as current language, rebuild. + $this->rebuildContainer(); /** @var \Drupal\user\Entity\User $account */ $account = User::load(0); $this->assertEqual($account->language()->getId(), 'en', 'Anonymous user is English.');