Commit c2f76e09 authored by alexpott's avatar alexpott

Issue #2015687 by smiletrl, amateescu, Berdir, fago: Convert field type to...

Issue #2015687 by smiletrl, amateescu, Berdir, fago: Convert field type to FieldType plugin for taxonomy module.
parent 65be82a1
...@@ -8,80 +8,14 @@ ...@@ -8,80 +8,14 @@
namespace Drupal\Core\Field; namespace Drupal\Core\Field;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\field\FieldInstanceInterface;
use Drupal\field\FieldInterface;
/** /**
* A common base class for configurable entity reference fields. * A common base class for configurable entity reference fields.
* *
* Extends the Core 'entity_reference' entity field item with properties for * Extends the Core 'entity_reference' entity field item with common methods
* revision ids, labels (for autocreate) and access. * used in general configurable entity reference field.
*
* Required settings (below the definition's 'settings' key) are:
* - target_type: The entity type to reference.
*/ */
class ConfigEntityReferenceItemBase extends EntityReferenceItem implements ConfigFieldItemInterface { class ConfigEntityReferenceItemBase extends EntityReferenceItem {
/**
* Definitions of the contained properties.
*
* @see ConfigurableEntityReferenceItem::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
$settings = $this->definition->getSettings();
$target_type = $settings['target_type'];
// Definitions vary by entity type and bundle, so key them accordingly.
$key = $target_type . ':';
$key .= isset($settings['target_bundle']) ? $settings['target_bundle'] : '';
if (!isset(static::$propertyDefinitions[$key])) {
// Call the parent to define the target_id and entity properties.
parent::getPropertyDefinitions();
// Only add the revision ID property if the target entity type supports
// revisions.
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
if (!empty($target_type_info['entity_keys']['revision']) && !empty($target_type_info['revision_table'])) {
static::$propertyDefinitions[$key]['revision_id'] = DataDefinition::create('integer')
->setLabel(t('Revision ID'))
->setConstraints(array('Range' => array('min' => 0)));
}
static::$propertyDefinitions[$key]['label'] = DataDefinition::create('string')
->setLabel(t('Label (auto-create)'))
->setComputed(TRUE);
static::$propertyDefinitions[$key]['access'] = DataDefinition::create('boolean')
->setLabel(t('Access'))
->setComputed(TRUE);
}
return static::$propertyDefinitions[$key];
}
/**
* {@inheritdoc}
*
* Copied from \Drupal\field\Plugin\Field\FieldType\LegacyConfigFieldItem,
* since we cannot extend it.
*/
public static function schema(FieldInterface $field) {
$definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->type);
$module = $definition['provider'];
module_load_install($module);
$callback = "{$module}_field_schema";
if (function_exists($callback)) {
return $callback($field);
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -101,75 +35,14 @@ public function isEmpty() { ...@@ -101,75 +35,14 @@ public function isEmpty() {
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* Copied from \Drupal\field\Plugin\Field\FieldType\LegacyConfigFieldItem,
* since we cannot extend it.
*/
public function settingsForm(array $form, array &$form_state, $has_data) {
if ($callback = $this->getLegacyCallback('settings_form')) {
$instance = $this->getFieldDefinition();
if (!($instance instanceof FieldInstanceInterface)) {
throw new \UnexpectedValueException('ConfigEntityReferenceItemBase::settingsForm() called for a field whose definition is not a field instance.');
}
// hook_field_settings_form() used to receive the $instance (not actually
// needed), and the value of field_has_data().
return $callback($instance->getField(), $instance, $has_data);
}
return array();
}
/**
* {@inheritdoc}
*
* Copied from \Drupal\field\Plugin\Field\FieldType\LegacyConfigFieldItem,
* since we cannot extend it.
*/ */
public function instanceSettingsForm(array $form, array &$form_state) { public function preSave() {
if ($callback = $this->getLegacyCallback('instance_settings_form')) { $entity = $this->get('entity')->getValue();
$instance = $this->getFieldDefinition(); $target_id = $this->get('target_id')->getValue();
if (!($instance instanceof FieldInstanceInterface)) {
throw new \UnexpectedValueException('ConfigEntityReferenceItemBase::instanceSettingsForm() called for a field whose definition is not a field instance.');
}
return $callback($instance->getField(), $instance, $form_state);
}
return array();
}
/** if (!$target_id && !empty($entity) && $entity->isNew()) {
* Returns options provided via the legacy callback hook_options_list(). $entity->save();
* $this->set('target_id', $entity->id());
* @todo: Convert all legacy callback implementations to methods.
*
* @see \Drupal\Core\TypedData\AllowedValuesInterface
*/
public function getSettableOptions() {
$definition = $this->getPluginDefinition();
$callback = "{$definition['provider']}_options_list";
if (function_exists($callback)) {
// We are at the field item level, so we need to go two levels up to get
// to the entity object.
return $callback($this->getFieldDefinition(), $this->getEntity());
}
}
/**
* Returns the legacy callback for a given field type "hook".
*
* Copied from \Drupal\field\Plugin\Field\FieldType\LegacyConfigFieldItem,
* since we cannot extend it.
*
* @param string $hook
* The name of the hook, e.g. 'settings_form', 'is_empty'.
*
* @return string|null
* The name of the legacy callback, or NULL if it does not exist.
*/
protected function getLegacyCallback($hook) {
$definition = $this->getPluginDefinition();
$module = $definition['provider'];
$callback = "{$module}_field_{$hook}";
if (function_exists($callback)) {
return $callback;
} }
} }
......
...@@ -211,4 +211,11 @@ public function delete() { } ...@@ -211,4 +211,11 @@ public function delete() { }
*/ */
public function deleteRevision() { } public function deleteRevision() { }
/**
* {@inheritdoc}
*/
public function getMainPropertyName() {
return 'value';
}
} }
...@@ -136,4 +136,20 @@ public function delete(); ...@@ -136,4 +136,20 @@ public function delete();
*/ */
public function deleteRevision(); public function deleteRevision();
/**
* Returns the name of the main property, if any.
*
* Some field items consist mainly of one main property, e.g. the value of a
* text field or the @code target_id @endcode of an entity reference. If the
* field item has no main property, the method returns NULL.
*
* @return string|null
* The name of the value property, or NULL if there is none.
*
* @todo: Move this to ComplexDataInterface once we improved Typed data to do
* not enforce having all methods on the data objects.
* https://drupal.org/node/2002134
*/
public function getMainPropertyName();
} }
...@@ -141,4 +141,12 @@ public function onChange($property_name) { ...@@ -141,4 +141,12 @@ public function onChange($property_name) {
} }
parent::onChange($property_name); parent::onChange($property_name);
} }
/**
* {@inheritdoc}
*/
public function getMainPropertyName() {
return 'target_id';
}
} }
...@@ -24,6 +24,11 @@ ...@@ -24,6 +24,11 @@
* as structured options arrays that can be used in an Options widget such as a * as structured options arrays that can be used in an Options widget such as a
* select box or checkboxes. * select box or checkboxes.
* *
* Note that this interface is mostly applicable for primitive data values, but
* can be used on complex data structures if a (primitive) main property is
* specified. In that case, the allowed values and options apply to the main
* property only.
*
* @see \Drupal\options\Plugin\Field\FieldWidget\OptionsWidgetBase * @see \Drupal\options\Plugin\Field\FieldWidget\OptionsWidgetBase
*/ */
interface AllowedValuesInterface { interface AllowedValuesInterface {
......
...@@ -362,7 +362,7 @@ public function getConstraints(DataDefinitionInterface $definition) { ...@@ -362,7 +362,7 @@ public function getConstraints(DataDefinitionInterface $definition) {
$class = $type_definition['class']; $class = $type_definition['class'];
} }
// Check if the class provides allowed values. // Check if the class provides allowed values.
if (array_key_exists('Drupal\Core\TypedData\AllowedValuesInterface', class_implements($class))) { if (is_subclass_of($class,'Drupal\Core\TypedData\AllowedValuesInterface')) {
$constraints[] = $validation_manager->create('AllowedValues', array()); $constraints[] = $validation_manager->create('AllowedValues', array());
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\Core\Validation\Plugin\Validation\Constraint; namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Drupal\Core\TypedData\AllowedValuesInterface; use Drupal\Core\TypedData\AllowedValuesInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\ChoiceValidator; use Symfony\Component\Validator\Constraints\ChoiceValidator;
...@@ -20,14 +21,32 @@ class AllowedValuesConstraintValidator extends ChoiceValidator { ...@@ -20,14 +21,32 @@ class AllowedValuesConstraintValidator extends ChoiceValidator {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function validate($value, Constraint $constraint) { public function validate($value, Constraint $constraint) {
if (!isset($value)) { $typed_data = $this->context->getMetadata()->getTypedData();
return;
} if ($typed_data instanceof AllowedValuesInterface) {
if ($this->context->getMetadata()->getTypedData() instanceof AllowedValuesInterface) {
$account = \Drupal::currentUser(); $account = \Drupal::currentUser();
$allowed_values = $this->context->getMetadata()->getTypedData()->getSettableValues($account); $allowed_values = $typed_data->getSettableValues($account);
$constraint->choices = $allowed_values; $constraint->choices = $allowed_values;
// If the data is complex, we have to validate its main property.
if ($typed_data instanceof ComplexDataInterface) {
$name = $typed_data->getMainPropertyName();
if (!isset($name)) {
throw new \LogicException('Cannot validate allowed values for complex data without a main property.');
}
$value = $typed_data->get($name)->getValue();
}
} }
return parent::validate($value, $constraint);
// The parent implementation ignores values that are not set, but makes
// sure some choices are available firstly. However, we want to support
// empty choices for undefined values, e.g. if a term reference field
// points to an empty vocabulary.
if (!isset($value)) {
return;
}
parent::validate($value, $constraint);
} }
} }
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\entity_reference; namespace Drupal\entity_reference;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\field\FieldInterface; use Drupal\field\FieldInterface;
use Drupal\Core\Field\ConfigEntityReferenceItemBase; use Drupal\Core\Field\ConfigEntityReferenceItemBase;
use Drupal\Core\Field\ConfigFieldItemInterface; use Drupal\Core\Field\ConfigFieldItemInterface;
...@@ -17,11 +18,51 @@ ...@@ -17,11 +18,51 @@
* Replaces the Core 'entity_reference' entity field type implementation, this * Replaces the Core 'entity_reference' entity field type implementation, this
* supports configurable fields, auto-creation of referenced entities and more. * supports configurable fields, auto-creation of referenced entities and more.
* *
* Required settings (below the definition's 'settings' key) are:
* - target_type: The entity type to reference.
*
* @see entity_reference_field_info_alter(). * @see entity_reference_field_info_alter().
* *
*/ */
class ConfigurableEntityReferenceItem extends ConfigEntityReferenceItemBase implements ConfigFieldItemInterface { class ConfigurableEntityReferenceItem extends ConfigEntityReferenceItemBase implements ConfigFieldItemInterface {
/**
* Definitions of the contained properties.
*
* @see ConfigurableEntityReferenceItem::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
$settings = $this->definition->getSettings();
$target_type = $settings['target_type'];
// Definitions vary by entity type and bundle, so key them accordingly.
$key = $target_type . ':';
$key .= isset($settings['target_bundle']) ? $settings['target_bundle'] : '';
if (!isset(static::$propertyDefinitions[$key])) {
// Call the parent to define the target_id and entity properties.
parent::getPropertyDefinitions();
// Only add the revision ID property if the target entity type supports
// revisions.
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
if (!empty($target_type_info['entity_keys']['revision']) && !empty($target_type_info['revision_table'])) {
static::$propertyDefinitions[$key]['revision_id'] = DataDefinition::create('integer')
->setLabel(t('Revision ID'))
->setConstraints(array('Range' => array('min' => 0)));
}
}
return static::$propertyDefinitions[$key];
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -65,19 +106,6 @@ public static function schema(FieldInterface $field) { ...@@ -65,19 +106,6 @@ public static function schema(FieldInterface $field) {
return $schema; return $schema;
} }
/**
* {@inheritdoc}
*/
public function preSave() {
$entity = $this->get('entity')->getValue();
$target_id = $this->get('target_id')->getValue();
if (!$target_id && !empty($entity) && $entity->isNew()) {
$entity->save();
$this->set('target_id', $entity->id());
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -27,7 +27,7 @@ public function prepareView(array $entities_items) { ...@@ -27,7 +27,7 @@ public function prepareView(array $entities_items) {
foreach ($entities_items as $items) { foreach ($entities_items as $items) {
foreach ($items as $item) { foreach ($items as $item) {
// Force the array key to prevent duplicates. // Force the array key to prevent duplicates.
if ($item->target_id !== 0) { if ($item->target_id != NULL) {
$tids[$item->target_id] = $item->target_id; $tids[$item->target_id] = $item->target_id;
} }
} }
...@@ -48,7 +48,7 @@ public function prepareView(array $entities_items) { ...@@ -48,7 +48,7 @@ public function prepareView(array $entities_items) {
$item->entity = $terms[$item->target_id]; $item->entity = $terms[$item->target_id];
} }
// Terms to be created are not in $terms, but are still legitimate. // Terms to be created are not in $terms, but are still legitimate.
elseif ($item->target_id === 0 && isset($item->entity)) { elseif ($item->target_id === NULL && isset($item->entity)) {
// Leave the item in place. // Leave the item in place.
} }
// Otherwise, unset the instance value, since the term does not exist. // Otherwise, unset the instance value, since the term does not exist.
......
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
namespace Drupal\taxonomy\Plugin\Field\FieldType; namespace Drupal\taxonomy\Plugin\Field\FieldType;
use Drupal\Core\Field\Plugin\Field\FieldType\LegacyConfigFieldItemList; use Drupal\Core\Field\ConfigFieldItemList;
/** /**
* Represents a configurable taxonomy_term_reference entity field. * Represents a configurable taxonomy_term_reference entity field item list.
*/ */
class TaxonomyTermReferenceFieldItemList extends LegacyConfigFieldItemList { class TaxonomyTermReferenceFieldItemList extends ConfigFieldItemList {
/** /**
* {@inheritdoc} * {@inheritdoc}
......
<?php
/**
* @file
* Contains \Drupal\taxonomy\Plugin\field\field_type\TaxonomyTermReferenceItem.
*/
namespace Drupal\taxonomy\Plugin\Field\FieldType;
use Drupal\Core\Field\ConfigEntityReferenceItemBase;
use Drupal\Core\Field\ConfigFieldItemInterface;
use Drupal\field\FieldInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* Plugin implementation of the 'term_reference' field type.
*
* @FieldType(
* id = "taxonomy_term_reference",
* label = @Translation("Term Reference"),
* description = @Translation("This field stores a reference to a taxonomy term."),
* settings = {
* "options_list_callback" = NULL,
* "allowed_values" = {
* {
* "vocabulary" = "",
* "parent" = "0"
* }
* }
* },
* instance_settings = { },
* default_widget = "options_select",
* default_formatter = "taxonomy_term_reference_link",
* list_class = "\Drupal\taxonomy\Plugin\Field\FieldType\TaxonomyTermReferenceFieldItemList"
* )
*/
class TaxonomyTermReferenceItem extends ConfigEntityReferenceItemBase implements ConfigFieldItemInterface, AllowedValuesInterface {
/**
* {@inheritdoc}
*/
public function getPossibleValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Possible Options may contain group
// arrays.
$flatten_options = $this->flattenOptions($this->getPossibleOptions($account));
return array_keys($flatten_options);
}
/**
* {@inheritdoc}
*/
public function getPossibleOptions(AccountInterface $account = NULL) {
return $this->getSettableOptions($account);
}
/**
* {@inheritdoc}
*/
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Settable Options may contain group
// arrays.
$flatten_options = $this->flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}
/**
* {@inheritdoc}
*/
public function getSettableOptions(AccountInterface $account = NULL) {
$instance = $this->getFieldDefinition();
$entity = $this->getParent()->getParent();
$function = $this->getFieldSetting('options_list_callback') ? $this->getFieldSetting('options_list_callback') : array($this, 'getDefaultOptions');
return call_user_func_array($function, array($instance, $entity));
}
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
$this->definition['settings']['target_type'] = 'taxonomy_term';
return parent::getPropertyDefinitions();
}
/**
* {@inheritdoc}
*/
public static function schema(FieldInterface $field) {
return array(
'columns' => array(
'target_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => FALSE,
),
),
'indexes' => array(
'target_id' => array('target_id'),
),
'foreign keys' => array(
'target_id' => array(
'table' => 'taxonomy_term_data',
'columns' => array('target_id' => 'tid'),
),
),
);
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, array &$form_state, $has_data) {
// Get proper values for 'allowed_values_function', which is a core setting.
$vocabularies = entity_load_multiple('taxonomy_vocabulary');
$options = array();
foreach ($vocabularies as $vocabulary) {
$options[$vocabulary->id()] = $vocabulary->name;
}
$settings = $this->getFieldSettings();
$element = array();
$element['#tree'] = TRUE;
foreach ($settings['allowed_values'] as $delta => $tree) {
$element['allowed_values'][$delta]['vocabulary'] = array(
'#type' => 'select',
'#title' => t('Vocabulary'),
'#default_value' => $tree['vocabulary'],
'#options' => $options,
'#required' => TRUE,
'#description' => t('The vocabulary which supplies the options for this field.'),
'#disabled' => $has_data,
);