diff --git a/core/core.services.yml b/core/core.services.yml index 6f8cce6..58611e2 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -194,6 +194,9 @@ services: language_manager: class: Drupal\Core\Language\LanguageManager arguments: ['@state'] + language_fallback_manager: + class: Drupal\Core\Language\NoFallbackManager + arguments: ['@language_manager'] string_translator.custom_strings: class: Drupal\Core\StringTranslation\Translator\CustomStrings tags: diff --git a/core/includes/language.inc b/core/includes/language.inc index e63acaa..eef7202 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -542,20 +542,14 @@ function language_url_split_prefix($path, $languages) { * * @return * An array of language codes. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getCandidates() + * + * @deprecated This has been deprectaed in favor of the language fallback + * manager. */ function language_fallback_get_candidates($type = Language::TYPE_CONTENT) { - $fallback_candidates = &drupal_static(__FUNCTION__); - - if (!isset($fallback_candidates)) { - // Get languages ordered by weight, add Language::LANGCODE_NOT_SPECIFIED at the end. - $fallback_candidates = array_keys(language_list()); - $fallback_candidates[] = Language::LANGCODE_NOT_SPECIFIED; - - // Let other modules hook in and add/change candidates. - drupal_alter('language_fallback_candidates', $fallback_candidates); - } - - return $fallback_candidates; + return Drupal::service('language_fallback_manager')->getCandidates(array('data' => $type)); } /** diff --git a/core/includes/schema.inc b/core/includes/schema.inc index 77faade..b50062d 100644 --- a/core/includes/schema.inc +++ b/core/includes/schema.inc @@ -525,14 +525,17 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { * The converted value. */ function drupal_schema_get_field_value(array $info, $value) { - if ($info['type'] == 'int' || $info['type'] == 'serial') { - $value = (int) $value; - } - elseif ($info['type'] == 'float') { - $value = (float) $value; - } - else { - $value = (string) $value; + // Preserve legal NULL values. + if (isset($value) || !empty($info['not null'])) { + if ($info['type'] == 'int' || $info['type'] == 'serial') { + $value = (int) $value; + } + elseif ($info['type'] == 'float') { + $value = (float) $value; + } + else { + $value = (string) $value; + } } return $value; } diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index e2dc1b4..93fe769 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -7,7 +7,9 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use PDO; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\Query\QueryInterface; @@ -59,13 +61,29 @@ class DatabaseStorageController extends EntityStorageControllerBase { protected $database; /** + * The language manager service. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + + /** + * The language fallback manager service. + * + * @var \Drupal\Core\Language\FallbackManagerInterface; + */ + protected $languageFallbackManager; + + /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { return new static( $entity_type, $entity_info, - $container->get('database') + $container->get('database'), + $container->get('language_manager'), + $container->get('language_fallback_manager') ); } @@ -78,11 +96,15 @@ public static function createInstance(ContainerInterface $container, $entity_typ * An array of entity info for the entity type. * @param \Drupal\Core\Database\Connection $database * The database connection to be used. + * @param \Drupal\Core\Language\FallbackManagerInterface + * The language fallback manager. */ - public function __construct($entity_type, array $entity_info, Connection $database) { + public function __construct($entity_type, array $entity_info, Connection $database, LanguageManager $language_manager, FallbackManagerInterface $fallback_manager) { parent::__construct($entity_type, $entity_info); $this->database = $database; + $this->languageManager = $language_manager; + $this->languageFallbackManager = $fallback_manager; // Check if the entity type supports IDs. if (isset($this->entityInfo['entity_keys']['id'])) { @@ -367,6 +389,8 @@ public function create(array $values) { $entity_class::preCreate($this, $values); $entity = new $entity_class($values, $this->entityType); + $entity->setLanguageManager($this->languageManager); + $entity->setLanguageFallbackManager($this->languageFallbackManager); // Assign a new UUID if there is none yet. if ($this->uuidKey && !isset($entity->{$this->uuidKey})) { diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index 143a2ab..a4a84cb 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -7,7 +7,9 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use PDO; use Drupal\Core\Entity\Query\QueryInterface; @@ -53,8 +55,9 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { /** * Overrides DatabaseStorageController::__construct(). */ - public function __construct($entity_type, array $entity_info, Connection $database) { - parent::__construct($entity_type,$entity_info, $database); + public function __construct($entity_type, array $entity_info, Connection $database, LanguageManager $languageManager, FallbackManagerInterface $fallback_manager) { + parent::__construct($entity_type, $entity_info, $database, $languageManager, $fallback_manager); + $this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE; $this->entityClass = $this->entityInfo['class']; @@ -109,6 +112,8 @@ public function create(array $values) { $bundle = $values[$this->bundleKey]; } $entity = new $this->entityClass(array(), $this->entityType, $bundle); + $entity->setLanguageManager($this->languageManager); + $entity->setLanguageFallbackManager($this->languageFallbackManager); foreach ($entity as $name => $field) { if (isset($values[$name])) { @@ -248,6 +253,8 @@ protected function mapFromStorageRecords(array $records, $load_revision = FALSE) $bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE; // Turn the record into an entity class. $entities[$id] = new $this->entityClass($entities[$id], $this->entityType, $bundle); + $entities[$id]->setLanguageManager($this->languageManager); + $entities[$id]->setLanguageFallbackManager($this->languageFallbackManager); } } $this->attachPropertyData($entities, $load_revision); @@ -319,6 +326,8 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { $bundle = $this->bundleKey ? $values[$this->bundleKey][Language::LANGCODE_DEFAULT] : FALSE; // Turn the record into an entity class. $entities[$id] = new $this->entityClass($values, $this->entityType, $bundle, array_keys($translations[$id])); + $entities[$id]->setLanguageManager($this->languageManager); + $entities[$id]->setLanguageFallbackManager($this->languageFallbackManager); } } } diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index ec05c1a..1c7941a 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -8,7 +8,9 @@ namespace Drupal\Core\Entity; use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; use IteratorAggregate; @@ -60,6 +62,22 @@ class Entity implements IteratorAggregate, EntityInterface { protected $isDefaultRevision = TRUE; /** + * The language manager to be used to retrieve languages and retrieve the + * current content language. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + + /** + * The language fallback manager to be used to determine the current + * translation and field values fallback. + * + * @var \Drupal\Core\Language\FallbackManagerInterface + */ + protected $languageFallbackManager; + + /** * Constructs an Entity object. * * @param array $values @@ -120,6 +138,20 @@ public function setNewRevision($value = TRUE) { } /** + * {@inheritdoc} + */ + public function setLanguageManager(LanguageManager $language_manager) { + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager) { + $this->languageFallbackManager = $fallback_manager; + } + + /** * Implements \Drupal\Core\Entity\EntityInterface::entityType(). */ public function entityType() { @@ -309,6 +341,17 @@ public function getTranslation($langcode) { } /** + * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation(). + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + return $this; + } + + /** * Returns the languages the entity is translated to. * * @todo: Remove once all entity types implement the entity field API. @@ -642,4 +685,14 @@ public function initTranslation($langcode) { // http://drupal.org/node/2004244 } + /** + * {@inheritdoc} + */ + public function applyLanguageFallback($context = array()) { + // @todo Config entities do not support entity translation hence we need to + // move the TranslatableInterface implementation to EntityNG. See + // http://drupal.org/node/2004244 + return $this; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php index a3ffcb9..b622969 100644 --- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -7,7 +7,9 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use IteratorAggregate; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\TypedData\TypedDataInterface; @@ -269,6 +271,20 @@ public function setPropertyValues($values) { } /** + * {@inheritdoc} + */ + public function setLanguageManager(LanguageManager $language_manager) { + $this->decorated->setLanguageManager($language_manager); + } + + /** + * {@inheritdoc} + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager) { + $this->decorated->setLanguageFallbackManager($fallback_manager); + } + + /** * Forwards the call to the decorated entity. */ public function getPropertyDefinition($name) { @@ -430,6 +446,13 @@ public function getTranslation($langcode) { } /** + * {@inheritdoc} + */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + return $this->decorated->getCurrentTranslation($langcode = NULL, $context = array()); + } + + /** * Forwards the call to the decorated entity. */ public function getType() { @@ -626,4 +649,11 @@ public function initTranslation($langcode) { $this->decorated->initTranslation($langcode); } + /** + * {@inheritdoc} + */ + public function applyLanguageFallback($context = array()) { + return $this->decorated->applyLanguageFallback($context); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index ceb09de..78f4267 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -403,15 +403,7 @@ public function getFormLangcode(array $form_state) { $langcode = $form_state['langcode']; } else { - // If no form langcode was provided we default to the current content - // language and inspect existing translations to find a valid fallback, - // if any. - $translations = $entity->getTranslationLanguages(); - $langcode = language(Language::TYPE_CONTENT)->id; - $fallback = language_multilingual() ? language_fallback_get_candidates() : array(); - while (!empty($langcode) && !isset($translations[$langcode])) { - $langcode = array_shift($fallback); - } + $langcode = $entity->getCurrentTranslation()->language()->id; } // If the site is not multilingual or no translation for the given form diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 62b5bfb..61f6332 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -7,8 +7,11 @@ namespace Drupal\Core\Entity; + use Drupal\Core\Language\Language; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\TypedData\LanguageFallbackData; +use Drupal\Core\TypedData\LanguageFallbackDataInterface; use Drupal\Core\TypedData\TypedDataInterface; use ArrayIterator; use InvalidArgumentException; @@ -24,7 +27,7 @@ * @todo: Once all entity types have been converted, merge improvements into the * Entity class and overhaul the EntityInterface. */ -class EntityNG extends Entity { +class EntityNG extends Entity implements LanguageFallbackDataInterface { /** * Status code indentifying a removed translation. @@ -463,12 +466,23 @@ protected function getDefaultLanguage() { /** * {@inheritdoc} */ - public function onChange($property_name) { - if ($property_name == 'langcode') { + public function onChange($name) { + if ($name == 'langcode') { // Avoid using unset as this unnecessarily triggers magic methods later // on. $this->language = NULL; } + + // TODO + if (count($this->translations) > 1 && isset($this->fields[$name][$this->activeLangcode]) && empty($this->fieldDefinitions[$name]['computed'])) { + // Update values so we can detect NULL fields. + $field_value = $this->fields[$name][$this->activeLangcode]->getValue(); + // A field where we have a single NULL item is considered NULL. + if (is_array($field_value) && count($field_value) == 1 && current($field_value) === NULL) { + $field_value = NULL; + } + $this->values[$name][$this->activeLangcode] = $field_value; + } } /** @@ -569,7 +583,45 @@ protected function initializeTranslation($langcode) { /** * {@inheritdoc} */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + $translation = $this; + + // Determine the requested language. Default to the current content + // language. + if (!isset($langcode)) { + $langcode = $this->languageManager->getLanguage(Language::TYPE_CONTENT)->id; + } + + if (!empty($langcode)) { + // TODO + $context['langcode'] = $langcode; + $context['data'] = $this; + $context += array('operation' => 'entity_view'); + $candidates = $this->languageFallbackManager->getCandidates($context); + + // TODO + $default_language = $this->language ?: $this->getDefaultLanguage(); + $candidates[$default_language->id] = Language::LANGCODE_DEFAULT; + + foreach ($candidates as $candidate) { + if (isset($this->translations[$candidate])) { + $translation = $this->getTranslation($candidate); + break; + } + } + } + + return $translation; + } + + /** + * {@inheritdoc} + */ public function hasTranslation($langcode) { + $default_language = $this->language ?: $this->getDefaultLanguage(); + if ($langcode == $default_language->id) { + $langcode = Language::LANGCODE_DEFAULT; + } return !empty($this->translations[$langcode]['status']); } @@ -664,6 +716,29 @@ public function translations() { } /** + * {@inheritdoc} + * + * @return \Drupal\Core\Entity\LanguageFallbackManagerInterface + */ + public function applyLanguageFallback($context = array()) { + $return = $this; + if (count($this->translations) > 1) { + $context['langcode'] = $this->activeLangcode; + $context['data'] = $this; + $context += array('operation' => 'entity_view'); + $return = new LanguageFallbackData($this->languageFallbackManager, $this, $this->values, $context); + } + return $return; + } + + /** + * {@inheritdoc} + */ + public function getPropertyLanguage($name) { + return $this->languages[$this->activeLangcode]; + } + + /** * Overrides Entity::getBCEntity(). */ public function getBCEntity() { diff --git a/core/lib/Drupal/Core/Language/FallbackManagerInterface.php b/core/lib/Drupal/Core/Language/FallbackManagerInterface.php new file mode 100644 index 0000000..209dd66 --- /dev/null +++ b/core/lib/Drupal/Core/Language/FallbackManagerInterface.php @@ -0,0 +1,35 @@ +weight) ? $a->weight : 0; $b_weight = isset($b->weight) ? $b->weight : 0; diff --git a/core/lib/Drupal/Core/Language/NoFallbackManager.php b/core/lib/Drupal/Core/Language/NoFallbackManager.php new file mode 100644 index 0000000..fe4f607 --- /dev/null +++ b/core/lib/Drupal/Core/Language/NoFallbackManager.php @@ -0,0 +1,43 @@ +languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function getCandidates(array $context = array()) { + return array(Language::LANGCODE_NOT_SPECIFIED); + } + + /** + * {@inheritdoc} + */ + public function getValuesMap(array $values, $langcode, array $context = array()) { + return array(); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/LanguageFallbackData.php b/core/lib/Drupal/Core/TypedData/LanguageFallbackData.php new file mode 100644 index 0000000..429f37d --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/LanguageFallbackData.php @@ -0,0 +1,350 @@ +fallbackManager = $fallbackManager; + $this->data = $data; + $this->values = &$values; + $this->context = $context; + $this->activeLangcode = $data->language()->id; + } + else { + // TODO + throw new \InvalidArgumentException(); + } + } + + /** + * {@inheritdoc} + */ + public function getPropertyLanguage($name) { + $langcode = NULL; + if (isset($this->context)) { + if (!isset($this->fallbackMap)) { + $this->initializeFallbackMap(); + } + if (isset($this->fallbackMap[$name])) { + $langcode = $this->fallbackMap[$name]; + } + } + if (!isset($this->languages[$langcode])) { + $this->languages += language_list(Language::STATE_ALL); + } + return isset($this->languages[$langcode]) ? $this->languages[$langcode] : NULL; + } + + /** + * TODO + */ + protected function initializeFallbackMap() { + $this->fallbackMap = $this->fallbackManager->getValuesMap($this->values, $this->activeLangcode, $this->context); + } + + /** + * Implements the magic method for getting object properties. + */ + public function &__get($name) { + if (!isset($this->fields[$name])) { + $translation = $this->data; + + if (!isset($this->fieldDefinitions)) { + $this->fieldDefinitions = $this->data->getPropertyDefinitions(); + } + + if (isset($this->fieldDefinitions[$name]) && !isset($this->values[$name][$this->activeLangcode])) { + if (!isset($this->fallbackMap)) { + $this->initializeFallbackMap(); + } + if (isset($this->fallbackMap[$name])) { + $langcode = $this->fallbackMap[$name]; + $translation = $this->data->getTranslation($langcode); + } + } + + $this->fields[$name] = clone $translation->__get($name); + $this->fields[$name]->setContext($name, $this); + } + + return $this->fields[$name]; + } + + /** + * Implements the magic method for setting object properties. + * + * TODO + */ + public function __set($name, $value) { + throw new \LogicException(); + } + + /** + * Implements the magic method for isset(). + */ + public function __isset($name) { + return $this->data->__isset($name); + } + + /** + * Implements the magic method for unset(). + */ + public function __unset($name) { + throw new \LogicException(); + } + + /** + * Implements the magic method for clone(). + */ + function __clone() { + $this->data = clone $this->data; + } + + /** + * {@inheritdoc} + */ + public function get($name) { + return isset($this->fields[$name]) ? $this->fields[$name] : $this->__get($name); + } + + /** + * {@inheritdoc} + */ + public function set($name, $value, $notify = TRUE) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if ($include_computed || empty($definition['computed'])) { + $properties[$name] = $this->__get($name); + } + } + return $properties; + } + + /** + * {@inheritdoc} + */ + public function getPropertyValues() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function setPropertyValues($values) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function getPropertyDefinition($name) { + return $this->data->getPropertyDefinition($name); + } + + /** + * {@inheritdoc} + */ + public function getPropertyDefinitions() { + return $this->data->getPropertyDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return $this->data->isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function onChange($name) { + // TODO + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + // A Traversable class can either be an IteratorAggregate or an Iterator. + return $this->data instanceof IteratorAggregate ? $this->data->getIterator() : $this->data; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->data->getType(); + } + + /** + * {@inheritdoc} + */ + public function getDefinition() { + return $this->data->getDefinition(); + } + + /** + * {@inheritdoc} + */ + public function getValue() { + return $this->data->getValue(); + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->data->setValue($value, $notify); + } + + /** + * {@inheritdoc} + */ + public function getString() { + return $this->data->getString(); + } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + return $this->data->getConstraints(); + } + + /** + * {@inheritdoc} + */ + public function validate() { + return $this->data->validate(); + } + + /** + * {@inheritdoc} + */ + public function applyDefaultValue($notify = TRUE) { + return $this->data->applyDefaultValue($notify); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->data->getName(); + } + + /** + * {@inheritdoc} + */ + public function getParent() { + return $this->data->getParent(); + } + + /** + * {@inheritdoc} + */ + public function getRoot() { + return $this->data->getRoot(); + } + + /** + * {@inheritdoc} + */ + public function getPropertyPath() { + return $this->data->getPropertyPath(); + } + + /** + * {@inheritdoc} + */ + public function setContext($name = NULL, TypedDataInterface $parent = NULL) { + return $this->data->setContext($name, $parent); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/LanguageFallbackDataInterface.php b/core/lib/Drupal/Core/TypedData/LanguageFallbackDataInterface.php new file mode 100644 index 0000000..39baa50 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/LanguageFallbackDataInterface.php @@ -0,0 +1,20 @@ +entityType(); - - if (isset($entity->translation[$context['langcode']]) && $entity->isTranslatable() && !content_translation_view_access($entity, $context['langcode'])) { - $instances = field_info_instances($entity_type, $entity->bundle()); - // Avoid altering the real entity. - $entity = clone($entity); - $entity_langcode = $entity->getUntranslated()->language()->id; - - foreach ($entity->translation as $langcode => $translation) { - if ($langcode == $context['langcode'] || !content_translation_view_access($entity, $langcode)) { - // Unset unaccessible field translations: if the field is untranslatable - // unsetting a language different from Language::LANGCODE_NOT_SPECIFIED has no - // effect. - foreach ($instances as $instance) { - // @todo BC entities have the same value accessibile both with the - // entity language and with Language::LANGCODE_DEFAULT. We need need to unset - // both until we remove the BC layer. - if ($langcode == $entity_langcode) { - unset($entity->{$instance['field_name']}[Language::LANGCODE_DEFAULT]); - } - unset($entity->{$instance['field_name']}[$langcode]); - } - } +function content_translation_language_fallback_candidates_entity_view_alter(&$candidates, $context) { + $entity = $context['data']; + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + if (!content_translation_view_access($entity, $langcode)) { + unset($candidates[$langcode]); } - - // Find the new fallback values. - field_language_fallback($display_language, $entity, $context['langcode']); } } diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 312ebb5..382f0ed 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -465,6 +465,8 @@ function hook_field_attach_view_alter(&$output, $context) { } /** + * TODO remove + * * Perform alterations on field_language() values. * * This hook is invoked to alter the array of display language codes for the @@ -477,7 +479,7 @@ function hook_field_attach_view_alter(&$output, $context) { * - entity: The entity with fields to render. * - langcode: The language code $entity has to be displayed in. */ -function hook_field_language_alter(&$display_langcode, $context) { +function _REMOVE_hook_field_language_alter(&$display_langcode, $context) { // Do not apply core language fallback rules if they are disabled or if Locale // is not registered as a translation handler. if (field_language_fallback_enabled() && field_has_translation_handler($context['entity']->entityType())) { diff --git a/core/modules/field/field.install b/core/modules/field/field.install index b425169..2fe0f60 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -371,11 +371,7 @@ function field_update_8003() { * @ingroup config_upgrade */ function field_update_8004() { - update_variable_set('field_language_fallback', TRUE); - update_variables_to_config('field.settings', array( - 'field_storage_default' => 'default_storage', - 'field_language_fallback' => 'language_fallback', - )); + // The 'field_language_fallback' variable has been removed. } /** diff --git a/core/modules/field/field.module b/core/modules/field/field.module index f8ec211..928147e 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -332,6 +332,8 @@ function field_field_widget_info_alter(&$info) { } /** + * // TODO + * * Applies language fallback rules to the fields attached to the given entity. * * Core language fallback rules simply check if fields have a field translation @@ -348,30 +350,16 @@ function field_field_widget_info_alter(&$info) { * The entity to be displayed. * @param $langcode * The language code $entity has to be displayed in. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getValuesMap() + * + * @deprecated This has been deprecated in favor of the language fallback + * manager. */ function field_language_fallback(&$field_langcodes, EntityInterface $entity, $langcode) { - // Lazily init fallback candidates to avoid unnecessary calls. - $fallback_candidates = NULL; - - foreach ($field_langcodes as $field_name => $field_langcode) { - // If the requested language is defined for the current field use it, - // otherwise search for a fallback value among the fallback candidates. - if (isset($entity->{$field_name}[$langcode])) { - $field_langcodes[$field_name] = $langcode; - } - elseif (!empty($entity->{$field_name})) { - if (!isset($fallback_candidates)) { - require_once DRUPAL_ROOT . '/core/includes/language.inc'; - $fallback_candidates = language_fallback_get_candidates(); - } - foreach ($fallback_candidates as $fallback_langcode) { - if (isset($entity->{$field_name}[$fallback_langcode])) { - $field_langcodes[$field_name] = $fallback_langcode; - break; - } - } - } - } + $values = $entity->getNGEntity()->values; + $map = Drupal::service('language_fallback_manager')->getValuesMap($values, $langcode); + $field_langcodes = array_intersect_key($map, $field_langcodes); } /** diff --git a/core/modules/field/field.multilingual.inc b/core/modules/field/field.multilingual.inc index ca33a31..bc7607f 100644 --- a/core/modules/field/field.multilingual.inc +++ b/core/modules/field/field.multilingual.inc @@ -51,6 +51,8 @@ * - Provide a value in a different language as fallback. By default, the * fallback logic is applied separately to each field to ensure that there * is a value for each field to display. + * + * // TODO remove * The field language fallback logic relies on the global language fallback * configuration. Therefore, the displayed field values can be in the * requested language, but may be different if no values for the requested @@ -153,9 +155,14 @@ function field_content_languages() { /** * Checks whether field language fallback is enabled. + * + * @see \Drupal\Core\Language\FallbackManagerInterface + * + * @deprecated Language fallback is always enabled. This concept has been + * deprecated in favor of fallback contexts and manager swappability. */ function field_language_fallback_enabled() { - return language_multilingual() && config('field.settings')->get('language_fallback'); + return TRUE; } /** @@ -224,6 +231,8 @@ function field_valid_language($langcode, $default = TRUE) { } /** + * TODO remove + * * Returns the display language code for the fields attached to the given * entity. * @@ -252,59 +261,28 @@ function field_valid_language($langcode, $default = TRUE) { * @return * A language code if a field name is specified, an array of language codes * keyed by field name otherwise. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getValuesMap() + * @see \Drupal\Core\Entity\EntityInterface::getFieldLangcode() + * + * @deprecated This has been deprecated in favor of the Entity Field API. */ function field_language(EntityInterface $entity, $field_name = NULL, $langcode = NULL) { - $display_langcodes = &drupal_static(__FUNCTION__, array()); - $id = $entity->id(); - $bundle = $entity->bundle(); - $entity_type = $entity->entityType(); - $langcode = field_valid_language($langcode, FALSE); - if (!isset($display_langcodes[$entity_type][$id][$langcode])) { - $display_langcode = array(); - - // By default, display language is set to one of the locked languages - // if the field translation is not available. It is up to translation - // handlers to implement language fallback rules. - foreach (field_info_instances($entity_type, $bundle) as $instance) { - if (isset($entity->{$instance['field_name']}[$langcode])) { - $display_langcode[$instance['field_name']] = $langcode; - } - else { - // If the field has a value for one of the locked languages, then use - // that language for display. If not, the default one will be - // Language::LANGCODE_NOT_SPECIFIED. - $display_langcode[$instance['field_name']] = Language::LANGCODE_NOT_SPECIFIED; - foreach (language_list(Language::STATE_LOCKED) as $language_locked) { - if (isset($entity->{$instance['field_name']}[$language_locked->id])) { - $display_langcode[$instance['field_name']] = $language_locked->id; - break; - } - } + /* @var $data \Drupal\Core\TypedData\LanguageFallbackDataInterface */ + $translation = $entity->getCurrentTranslation($langcode); + $data = $translation->applyLanguageFallback(); + $definitions = $data->getPropertyDefinitions(); + $translatable = field_has_translation_handler($entity->entityType()); + if (!isset($field_name)) { + $display_langcodes = array(); + foreach ($definitions as $name => $definition) { + if (!empty($definition['configurable'])) { + $display_langcodes[$name] = $translatable ? $data->getPropertyLanguage($name)->id : Language::LANGCODE_NOT_SPECIFIED; } } - - if (field_has_translation_handler($entity_type)) { - $context = array( - 'entity' => $entity, - 'langcode' => $langcode, - ); - // Do not apply core language fallback rules if they are disabled or if - // the entity does not have a translation handler registered. - if (field_language_fallback_enabled() && field_has_translation_handler($entity_type)) { - field_language_fallback($display_langcode, $context['entity'], $context['langcode']); - } - drupal_alter('field_language', $display_langcode, $context); - } - - $display_langcodes[$entity_type][$id][$langcode] = $display_langcode; + return $display_langcodes; } - - $display_langcode = $display_langcodes[$entity_type][$id][$langcode]; - - // Single-field mode. - if (isset($field_name)) { - return isset($display_langcode[$field_name]) ? $display_langcode[$field_name] : FALSE; + elseif (!empty($definitions[$field_name]['configurable'])) { + return $translatable ? $data->getPropertyLanguage($field_name)->id : Language::LANGCODE_NOT_SPECIFIED; } - - return $display_langcode; } diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php b/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php index 7ce432b..226dc58 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php +++ b/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php @@ -8,6 +8,7 @@ namespace Drupal\field\Plugin\views\field; use Drupal\Core\Language\Language; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\field\Plugin\Type\Formatter\FormatterPluginManager; use Drupal\views\ViewExecutable; @@ -76,6 +77,13 @@ class Field extends FieldPluginBase { protected $formatterPluginManager; /** + * The language fallback manager. + * + * @var \Drupal\Core\Language\FallbackManagerInterface; + */ + protected $languageFallbackManager; + + /** * Constructs a \Drupal\field\Plugin\views\field\Field object. * * @param array $configuration @@ -87,10 +95,11 @@ class Field extends FieldPluginBase { * @param \Drupal\field\Plugin\Type\Formatter\FormatterPluginManager $formatter_plugin_manager * The field formatter plugin manager. */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, FormatterPluginManager $formatter_plugin_manager) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, FormatterPluginManager $formatter_plugin_manager, FallbackManagerInterface $fallback_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->formatterPluginManager = $formatter_plugin_manager; + $this->languageFallbackManager = $fallback_manager; } /** @@ -101,7 +110,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('plugin.manager.field.formatter') + $container->get('plugin.manager.field.formatter'), + $container->get('language_fallback_manager') ); } @@ -231,14 +241,7 @@ public function query($use_groupby = FALSE) { array(drupal_container()->get(Language::TYPE_CONTENT)->id, $default_langcode), $this->view->display_handler->options['field_langcode']); $placeholder = $this->placeholder(); - $langcode_fallback_candidates = array($langcode); - if (field_language_fallback_enabled()) { - require_once DRUPAL_ROOT . '/includes/language.inc'; - $langcode_fallback_candidates = array_merge($langcode_fallback_candidates, language_fallback_get_candidates()); - } - else { - $langcode_fallback_candidates[] = Language::LANGCODE_NOT_SPECIFIED; - } + $langcode_fallback_candidates = $this->languageFallbackManager->getCandidates(array('operation' => 'views_query', 'langcode' => $langcode, 'data' => $this)); $this->query->addWhereExpression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $langcode_fallback_candidates)); } } @@ -836,11 +839,13 @@ function field_langcode(EntityInterface $entity) { array(drupal_container()->get(Language::TYPE_CONTENT)->id, $default_langcode), $this->view->display_handler->options['field_language']); - // Give the Field Language API a chance to fallback to a different language - // (or Language::LANGCODE_NOT_SPECIFIED), in case the field has no data for the selected language. - // field_view_field() does this as well, but since the returned language code - // is used before calling it, the fallback needs to happen explicitly. - $langcode = field_language($entity, $this->field_info['field_name'], $langcode); + // Give the Entity Field API a chance to fallback to a different language + // (or Language::LANGCODE_NOT_SPECIFIED), in case the field has no data + // for the selected language. field_view_field() does this as well, but + // since the returned language code is used before calling it, the + // fallback needs to happen explicitly. + $name = $this->field_info['field_name']; + $langcode = $entity->getCurrentTranslation($langcode)->applyLanguageFallback()->getPropertyLanguage($name)->id; return $langcode; } diff --git a/core/modules/language/lib/Drupal/language/FallbackManager.php b/core/modules/language/lib/Drupal/language/FallbackManager.php new file mode 100644 index 0000000..ad9e593 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/FallbackManager.php @@ -0,0 +1,112 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getCandidates(array $context = array()) { + // Get languages ordered by weight, add Language::LANGCODE_NOT_SPECIFIED at + // the end. + $candidates = array_keys(language_list()); + $candidates[] = Language::LANGCODE_NOT_SPECIFIED; + $candidates = MapArray::copyValuesToKeys($candidates); + + // The first candidate should always be the requested language if any. + if (!empty($context['langcode'])) { + $langcode = $context['langcode']; + $candidates = array($langcode => $langcode) + $candidates; + } + + // Let other modules hook in and add/change candidates. + $this->alter('language_fallback_candidates', $candidates, $context); + + return $candidates; + } + + /** + * {@inheritdoc} + */ + public function getValuesMap(array $values, $langcode, array $context = array()) { + $map = array(); + + // Lazily init fallback candidates to avoid unnecessary calls. + $candidates = NULL; + foreach ($values as $name => $value) { + // If the requested language is defined for the current value use it, + // otherwise search for a fallback value among the fallback candidates. + // FIXME! + $map[$name] = Language::LANGCODE_NOT_SPECIFIED; + if (is_array($value)) { + if (!empty($value[$langcode])) { + $map[$name] = $langcode; + } + elseif (!empty($values[$name])) { + if (!isset($candidates)) { + $context['langcode'] = $langcode; + $candidates = $this->getCandidates($context); + // Remove the requested language as we already test it above. + unset($candidates[$langcode]); + } + foreach ($candidates as $fallback_langcode) { + // FIXME! + if (!empty($values[$name][$fallback_langcode])) { + $map[$name] = $fallback_langcode; + break; + } + } + } + } + } + + // TODO + $context = array('values' => $values, 'langcode' => $langcode) + $context; + $this->alter('language_fallback_values', $map, $context); + + return $map; + } + + /** + * TODO + */ + protected function alter($type, &$data, array $context) { + $types = array($type); + if (!empty($context['operation'])) { + $types[] = $type . '_' . $context['operation']; + } + return $this->moduleHandler->alter($types, $data, $context); + } + +} diff --git a/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php new file mode 100644 index 0000000..9e34c57 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php @@ -0,0 +1,36 @@ +getDefinition('language_fallback_manager'); + $definition->setClass('Drupal\language\FallbackManager'); + $definition->addArgument(new Reference('module_handler')); + } + +} diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php index 43d54ce..80ee321 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php @@ -11,6 +11,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Database\Connection; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Cmf\Component\Routing\RouteProviderInterface; @@ -55,8 +57,8 @@ class MenuLinkStorageController extends DatabaseStorageController implements Men * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider * The route provider service. */ - public function __construct($entity_type, array $entity_info, Connection $database, RouteProviderInterface $route_provider) { - parent::__construct($entity_type, $entity_info, $database); + public function __construct($entity_type, array $entity_info, Connection $database, LanguageManager $language_manager, FallbackManagerInterface $fallback_manager, RouteProviderInterface $route_provider) { + parent::__construct($entity_type, $entity_info, $database, $language_manager, $fallback_manager); $this->routeProvider = $route_provider; @@ -85,6 +87,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ $entity_type, $entity_info, $container->get('database'), + $container->get('language_manager'), + $container->get('language_fallback_manager'), $container->get('router.route_provider') ); } diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 77dc994..198dff8 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -139,7 +139,7 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr case 'summary': if ($items = field_get_items($node, 'body', $langcode)) { $instance = field_info_instance('node', 'body', $node->type); - $field_langcode = field_language($node, 'body', $langcode); + $field_langcode = $node->getCurrentTranslation($langcode)->language()->id; // If the summary was requested and is not empty, use it. if ($name == 'summary' && !empty($items[0]['summary'])) { diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php index 40a7e44..6610571 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -82,6 +82,7 @@ function setUp() { $language = new Language(array( 'id' => 'l' . $i, 'name' => $this->randomString(), + 'weight' => $i, )); $this->langcodes[$i] = $language->id; language_save($language); @@ -462,4 +463,71 @@ function testEntityTranslationAPI() { $this->assertEqual($translation->get($this->field_name)->value, $this->field_name . '_' . $langcode2, 'Language-aware default values correctly populated.'); } + /** + * Tests language fallback applied to field and entity translations. + */ + function testLanguageFallback() { + $default_langcode = $this->langcodes[0]; + $langcode = $this->langcodes[1]; + $langcode2 = $this->langcodes[2]; + + $values = array(); + for ($i = 0; $i < 3; $i++) { + $values[$this->langcodes[$i]]['name'] = $this->randomName(); + $values[$this->langcodes[$i]]['user_id'] = mt_rand(0, 127); + } + + $entity = $this->entityManager + ->getStorageController('entity_test_mul') + ->create(array('langcode' => $default_langcode) + $values[$default_langcode]); + $entity->save(); + + $entity->addTranslation($langcode, $values[$langcode]); + $entity->save(); + + // Check that retrieveing the current translation works as expected. + $entity = $this->reloadEntity($entity); + $translation = $entity->getCurrentTranslation($langcode2); + $this->assertEqual($translation->language()->id, $default_langcode, 'The current translation language matches the expected one.'); + + // TODO + $languages = language_list(); + $languages[$langcode]->weight = -1; + language_save($languages[$langcode]); + $translation = $entity->getCurrentTranslation($langcode2); + $this->assertEqual($translation->language()->id, $langcode, 'The current translation language matches the expected one.'); + + // TODO + $translation = $entity->getTranslation($langcode); + unset($translation->name); + $translation->save(); + $entity = $this->reloadEntity($entity); + $translation = $entity->getCurrentTranslation($langcode); + $this->assertEqual($translation->applyLanguageFallback()->name->value, $values[$default_langcode]['name'], 'Field fallback is correctly performed when a field translation is missing.'); + + // TODO + $translation2 = $entity->addTranslation($langcode2, $values[$langcode2]); + unset($translation2->name); + $translation2->save(); + $entity = $this->reloadEntity($entity); + $translation2 = $entity->getCurrentTranslation($langcode2); + $this->assertEqual($translation2->applyLanguageFallback()->name->value, $values[$default_langcode]['name'], 'Field fallback is correctly performed when a field translation is missing.'); + + // TODO + $translation = $entity->getTranslation($langcode); + $translation->name->value = $values[$langcode]['name']; + $data = $translation2->applyLanguageFallback(); + $this->assertEqual($data->name->value, $values[$langcode]['name'], 'Field fallback is correctly performed when a field translation is missing.'); + + // TODO + try { + $message = 'Trying to set a value on the fallback data causes an exception to be thrown.'; + $data->name->value = $this->randomName(); + $this->fail($message); + } + catch (\LogicException $e) { + $this->pass($message); + } + } + } diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php index acf9d5b..3bc13a5 100644 --- a/core/modules/user/lib/Drupal/user/UserStorageController.php +++ b/core/modules/user/lib/Drupal/user/UserStorageController.php @@ -7,6 +7,8 @@ namespace Drupal\user; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\Entity\EntityBCDecorator; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Password\PasswordInterface; @@ -51,8 +53,8 @@ class UserStorageController extends DatabaseStorageControllerNG implements UserS * @param \Drupal\user\UserDataInterface $user_data * The user data service. */ - public function __construct($entity_type, $entity_info, Connection $database, PasswordInterface $password, UserDataInterface $user_data) { - parent::__construct($entity_type, $entity_info, $database); + public function __construct($entity_type, $entity_info, Connection $database, LanguageManager $languageManager, FallbackManagerInterface $fallback_manager, PasswordInterface $password, UserDataInterface $user_data) { + parent::__construct($entity_type, $entity_info, $database, $languageManager, $fallback_manager); $this->password = $password; $this->userData = $user_data; @@ -66,6 +68,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ $entity_type, $entity_info, $container->get('database'), + $container->get('language_manager'), + $container->get('language_fallback_manager'), $container->get('password'), $container->get('user.data') ); diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php index 78bbeca..9b174b5 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php @@ -11,6 +11,8 @@ use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\views\ViewExecutable; use Drupal\Core\Database\Database; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\Plugin\views\query\Sql; @@ -905,6 +907,20 @@ public function setNewRevision($value = TRUE) { } /** + * {@inheritdoc} + */ + public function setLanguageManager(LanguageManager $language_manager) { + $this->storage->setLanguageManager($language_manager); + } + + /** + * {@inheritdoc} + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager) { + $this->storage->setLanguageFallbackManager($fallback_manager); + } + + /** * Implements \Drupal\Core\Entity\EntityInterface::enforceIsNew(). */ public function enforceIsNew($value = TRUE) { @@ -927,6 +943,14 @@ public function getTranslation($langcode) { } /** + * {@inheritdoc} + */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + // @todo Revisit this once config entities are converted to NG. + return $this; + } + + /** * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). */ public function getTranslationLanguages($include_default = TRUE) { @@ -1243,4 +1267,12 @@ public function mergeDefaultDisplaysOptions() { public function uriRelationships() { return $this->storage->uriRelationships(); } + + /** + * {@inheritdoc} + */ + public function applyLanguageFallback($context = array()) { + return $this->storage->applyLanguageFallback($context); + } + }