Commit a47cadae authored by catch's avatar catch

Issue #2498919 by stefan.r, Berdir, catch: Node::isPublished() and Node::getOwnerId() are expensive

parent 9907029c
...@@ -138,12 +138,19 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C ...@@ -138,12 +138,19 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
protected $isDefaultRevision = TRUE; 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 * @var array
*/ */
protected $entityKeys = array(); protected $entityKeys = array();
/**
* Holds translatable entity keys such as the label.
*
* @var array
*/
protected $translatableEntityKeys = array();
/** /**
* Overrides Entity::__construct(). * Overrides Entity::__construct().
*/ */
...@@ -165,7 +172,11 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans ...@@ -165,7 +172,11 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans
$this->values = $values; $this->values = $values;
foreach ($this->getEntityType()->getKeys() as $key => $field_name) { foreach ($this->getEntityType()->getKeys() as $key => $field_name) {
if (isset($this->values[$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])) {
// 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 (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) { if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value']; $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
...@@ -176,6 +187,24 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans ...@@ -176,6 +187,24 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans
} }
} }
} }
else {
// 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];
}
}
}
}
}
} }
// Initialize translations. Ensure we have at least an entry for the default // Initialize translations. Ensure we have at least an entry for the default
...@@ -537,12 +566,12 @@ protected function setDefaultLangcode() { ...@@ -537,12 +566,12 @@ protected function setDefaultLangcode() {
// Get the language code if the property exists. // Get the language code if the property exists.
// Try to read the value directly from the list of entity keys which got // Try to read the value directly from the list of entity keys which got
// initialized in __construct(). This avoids creating a field item object. // initialized in __construct(). This avoids creating a field item object.
if (isset($this->entityKeys['langcode'])) { if (isset($this->translatableEntityKeys['langcode'][$this->activeLangcode])) {
$this->defaultLangcode = $this->entityKeys['langcode']; $this->defaultLangcode = $this->translatableEntityKeys['langcode'][$this->activeLangcode];
} }
elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) { elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) {
$this->defaultLangcode = $item->language->getId(); $this->defaultLangcode = $item->language->getId();
$this->entityKeys['langcode'] = $this->defaultLangcode; $this->translatableEntityKeys['langcode'][$this->activeLangcode] = $this->defaultLangcode;
} }
if (empty($this->defaultLangcode)) { if (empty($this->defaultLangcode)) {
...@@ -583,9 +612,14 @@ public function onChange($name) { ...@@ -583,9 +612,14 @@ public function onChange($name) {
// that check, as it ready only and must not change, unsetting it could // that check, as it ready only and must not change, unsetting it could
// lead to recursions. // lead to recursions.
if ($key = array_search($name, $this->getEntityType()->getKeys())) { if ($key = array_search($name, $this->getEntityType()->getKeys())) {
if (isset($this->entityKeys[$key]) && $key != 'bundle') { if ($key != 'bundle') {
if (isset($this->entityKeys[$key])) {
unset($this->entityKeys[$key]); unset($this->entityKeys[$key]);
} }
elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
}
}
} }
switch ($name) { switch ($name) {
...@@ -710,8 +744,6 @@ protected function initializeTranslation($langcode) { ...@@ -710,8 +744,6 @@ protected function initializeTranslation($langcode) {
$translation->enforceIsNew = &$this->enforceIsNew; $translation->enforceIsNew = &$this->enforceIsNew;
$translation->newRevision = &$this->newRevision; $translation->newRevision = &$this->newRevision;
$translation->translationInitialize = FALSE; $translation->translationInitialize = FALSE;
// Reset language-dependent properties.
unset($translation->entityKeys['label']);
$translation->typedData = NULL; $translation->typedData = NULL;
return $translation; return $translation;
...@@ -1020,18 +1052,34 @@ public function referencedEntities() { ...@@ -1020,18 +1052,34 @@ public function referencedEntities() {
* The value of the entity key, NULL if not defined. * The value of the entity key, NULL if not defined.
*/ */
protected function getEntityKey($key) { protected function getEntityKey($key) {
if (!isset($this->entityKeys[$key]) || !array_key_exists($key, $this->entityKeys)) { // 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)) { if ($this->getEntityType()->hasKey($key)) {
$field_name = $this->getEntityType()->getKey($key); $field_name = $this->getEntityType()->getKey($key);
$property = $this->getFieldDefinition($field_name)->getFieldStorageDefinition()->getMainPropertyName(); $definition = $this->getFieldDefinition($field_name);
$this->entityKeys[$key] = $this->get($field_name)->$property; $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 { else {
$this->entityKeys[$key] = NULL; $this->entityKeys[$key] = $value;
} }
} }
return $this->entityKeys[$key]; else {
$this->entityKeys[$key] = $value;
}
return $value;
} }
/** /**
......
...@@ -53,7 +53,9 @@ ...@@ -53,7 +53,9 @@
* "bundle" = "type", * "bundle" = "type",
* "label" = "title", * "label" = "title",
* "langcode" = "langcode", * "langcode" = "langcode",
* "uuid" = "uuid" * "uuid" = "uuid",
* "status" = "status",
* "uid" = "uid",
* }, * },
* bundle_entity_type = "node_type", * bundle_entity_type = "node_type",
* field_ui_base_route = "entity.node_type.edit_form", * field_ui_base_route = "entity.node_type.edit_form",
...@@ -72,6 +74,13 @@ class Node extends ContentEntityBase implements NodeInterface { ...@@ -72,6 +74,13 @@ class Node extends ContentEntityBase implements NodeInterface {
use EntityChangedTrait; use EntityChangedTrait;
/**
* Whether the node is being previewed or not.
*
* @var true|null
*/
public $in_preview = NULL;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -258,7 +267,7 @@ public function setSticky($sticky) { ...@@ -258,7 +267,7 @@ public function setSticky($sticky) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function isPublished() { public function isPublished() {
return (bool) $this->get('status')->value; return (bool) $this->getEntityKey('status');
} }
/** /**
...@@ -280,7 +289,7 @@ public function getOwner() { ...@@ -280,7 +289,7 @@ public function getOwner() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getOwnerId() { public function getOwnerId() {
return $this->get('uid')->target_id; return $this->getEntityKey('uid');
} }
/** /**
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
namespace Drupal\node; namespace Drupal\node;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
...@@ -204,9 +203,32 @@ public function form(array $form, FormStateInterface $form_state) { ...@@ -204,9 +203,32 @@ public function form(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'node/form'; $form['#attached']['library'][] = 'node/form';
$form['#entity_builders']['update_status'] = [$this, 'updateStatus'];
return $form; 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} * {@inheritdoc}
*/ */
...@@ -232,6 +254,8 @@ protected function actions(array $form, FormStateInterface $form_state) { ...@@ -232,6 +254,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
// Add a "Publish" button. // Add a "Publish" button.
$element['publish'] = $element['submit']; $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'; $element['publish']['#dropbutton'] = 'save';
if ($node->isNew()) { if ($node->isNew()) {
$element['publish']['#value'] = t('Save and publish'); $element['publish']['#value'] = t('Save and publish');
...@@ -240,10 +264,11 @@ protected function actions(array $form, FormStateInterface $form_state) { ...@@ -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']['#value'] = $node->isPublished() ? t('Save and keep published') : t('Save and publish');
} }
$element['publish']['#weight'] = 0; $element['publish']['#weight'] = 0;
array_unshift($element['publish']['#submit'], '::publish');
// Add a "Unpublish" button. // Add a "Unpublish" button.
$element['unpublish'] = $element['submit']; $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'; $element['unpublish']['#dropbutton'] = 'save';
if ($node->isNew()) { if ($node->isNew()) {
$element['unpublish']['#value'] = t('Save as unpublished'); $element['unpublish']['#value'] = t('Save as unpublished');
...@@ -252,7 +277,6 @@ protected function actions(array $form, FormStateInterface $form_state) { ...@@ -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']['#value'] = !$node->isPublished() ? t('Save and keep unpublished') : t('Save and unpublish');
} }
$element['unpublish']['#weight'] = 10; $element['unpublish']['#weight'] = 10;
array_unshift($element['unpublish']['#submit'], '::unpublish');
// If already published, the 'publish' button is primary. // If already published, the 'publish' button is primary.
if ($node->isPublished()) { if ($node->isPublished()) {
...@@ -327,34 +351,6 @@ public function preview(array $form, FormStateInterface $form_state) { ...@@ -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} * {@inheritdoc}
*/ */
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
namespace Drupal\node; namespace Drupal\node;
use Drupal\Core\Entity\EntityInterface;
use Drupal\content_translation\ContentTranslationHandler; use Drupal\content_translation\ContentTranslationHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
/** /**
...@@ -76,9 +76,8 @@ protected function entityFormTitle(EntityInterface $entity) { ...@@ -76,9 +76,8 @@ protected function entityFormTitle(EntityInterface $entity) {
*/ */
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) { public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
if ($form_state->hasValue('content_translation')) { if ($form_state->hasValue('content_translation')) {
$form_object = $form_state->getFormObject();
$translation = &$form_state->getValue('content_translation'); $translation = &$form_state->getValue('content_translation');
$translation['status'] = $form_object->getEntity()->isPublished(); $translation['status'] = $entity->isPublished();
// $form['content_translation']['name'] is the equivalent field // $form['content_translation']['name'] is the equivalent field
// for translation author uid. // for translation author uid.
$account = $entity->uid->entity; $account = $entity->uid->entity;
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\node\Entity\Node; use Drupal\node\Entity\Node;
use Drupal\language\Entity\ConfigurableLanguage;
/** /**
* Tests the Node Translation UI. * Tests the Node Translation UI.
...@@ -57,6 +58,42 @@ function testTranslationUI() { ...@@ -57,6 +58,42 @@ function testTranslationUI() {
$this->doUninstallTest(); $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(). * Overrides \Drupal\content_translation\Tests\ContentTranslationUITestBase::getTranslatorPermission().
*/ */
......
...@@ -57,6 +57,8 @@ public function testInstaller() { ...@@ -57,6 +57,8 @@ public function testInstaller() {
$this->assertText('German'); $this->assertText('German');
$this->assertNoText('English'); $this->assertNoText('English');
// The current container still has the english as current language, rebuild.
$this->rebuildContainer();
/** @var \Drupal\user\Entity\User $account */ /** @var \Drupal\user\Entity\User $account */
$account = User::load(0); $account = User::load(0);
$this->assertEqual($account->language()->getId(), 'en', 'Anonymous user is English.'); $this->assertEqual($account->language()->getId(), 'en', 'Anonymous user is English.');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment