Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1 @@
purge_batch_size: 50

View file

@ -0,0 +1,56 @@
# Schema for configuration files of the Field module.
field.settings:
type: config_object
label: 'Field settings'
mapping:
purge_batch_size:
type: integer
label: 'Maximum number of field data records to purge'
field.storage.*.*:
type: config_entity
label: 'Field'
mapping:
id:
type: string
label: 'ID'
field_name:
type: string
label: 'Field name'
entity_type:
type: string
label: 'Entity type'
type:
type: string
label: 'Type'
settings:
type: field.storage_settings.[%parent.type]
module:
type: string
label: 'Module'
locked:
type: boolean
label: 'Locked'
cardinality:
type: integer
label: 'Maximum number of values users can enter'
translatable:
type: boolean
label: 'Translatable'
indexes:
type: sequence
label: 'Indexes'
sequence:
type: sequence
label: 'Indexes'
sequence:
type: ignore
label: 'Index'
persist_with_no_fields:
type: boolean
label: 'Persist field storage with no fields'
field.field.*.*.*:
type: field_config_base
label: 'Field'

View file

@ -0,0 +1,25 @@
# Schema for the views plugins of the Field module.
views.argument.field_list:
type: views.argument.numeric
label: 'List field name'
mapping:
human:
type: boolean
label: 'Display list value as human readable'
views.argument.field_list_string:
type: views.argument.string
label: 'List field name'
mapping:
human:
type: boolean
label: 'Display list value as human readable'
views.filter.field_list:
type: views.filter.many_to_one
label: 'List field'
views.relationship.entity_reverse:
type: views_relationship
label: 'Reverse entity reference'

View file

@ -0,0 +1,311 @@
<?php
/**
* @addtogroup hooks
* @{
*/
use Drupal\Component\Utility\NestedArray;
/**
* @defgroup field_types Field Types API
* @{
* Defines field, widget, display formatter, and storage types.
*
* In the Field API, each field has a type, which determines what kind of data
* (integer, string, date, etc.) the field can hold, which settings it provides,
* and so on. The data type(s) accepted by a field are defined in
* hook_field_schema().
*
* Field types are plugins annotated with class
* \Drupal\Core\Field\Annotation\FieldType, and implement plugin interface
* \Drupal\Core\Field\FieldItemInterface. Field Type plugins are managed by the
* \Drupal\Core\Field\FieldTypePluginManager class. Field type classes usually
* extend base class \Drupal\Core\Field\FieldItemBase. Field-type plugins need
* to be in the namespace \Drupal\{your_module}\Plugin\Field\FieldType. See the
* @link plugin_api Plugin API topic @endlink for more information on how to
* define plugins.
*
* The Field Types API also defines two kinds of pluggable handlers: widgets
* and formatters. @link field_widget Widgets @endlink specify how the field
* appears in edit forms, while @link field_formatter formatters @endlink
* specify how the field appears in displayed entities.
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*
* @see field
* @see field_widget
* @see field_formatter
* @see plugin_api
*/
/**
* Perform alterations on Field API field types.
*
* @param $info
* Array of information on field types as collected by the "field type" plugin
* manager.
*/
function hook_field_info_alter(&$info) {
// Change the default widget for fields of type 'foo'.
if (isset($info['foo'])) {
$info['foo']['default widget'] = 'mymodule_widget';
}
}
/**
* Forbid a field storage update from occurring.
*
* Any module may forbid any update for any reason. For example, the
* field's storage module might forbid an update if it would change
* the storage schema while data for the field exists. A field type
* module might forbid an update if it would change existing data's
* semantics, or if there are external dependencies on field settings
* that cannot be updated.
*
* To forbid the update from occurring, throw a
* \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException.
*
* @param \Drupal\field\FieldStorageConfigInterface $field_storage
* The field storage as it will be post-update.
* @param \Drupal\field\FieldStorageConfigInterface $prior_field_storage
* The field storage as it is pre-update.
*
* @see entity_crud
*/
function hook_field_storage_config_update_forbid(\Drupal\field\FieldStorageConfigInterface $field_storage, \Drupal\field\FieldStorageConfigInterface $prior_field_storage) {
if ($field_storage->module == 'options' && $field_storage->hasData()) {
// Forbid any update that removes allowed values with actual data.
$allowed_values = $field_storage->getSetting('allowed_values');
$prior_allowed_values = $prior_field_storage->getSetting('allowed_values');
$lost_keys = array_keys(array_diff_key($prior_allowed_values,$allowed_values));
if (_options_values_in_use($field_storage->getTargetEntityTypeId(), $field_storage->getName(), $lost_keys)) {
throw new \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field_storage->getName())));
}
}
}
/**
* @} End of "defgroup field_types".
*/
/**
* @defgroup field_widget Field Widget API
* @{
* Define Field API widget types.
*
* Field API widgets specify how fields are displayed in edit forms. Fields of a
* given @link field_types field type @endlink may be edited using more than one
* widget. In this case, the Field UI module allows the site builder to choose
* which widget to use.
*
* Widgets are Plugins managed by the
* \Drupal\Core\Field\WidgetPluginManager class. A widget is a plugin annotated
* with class \Drupal\Core\Field\Annotation\FieldWidget that implements
* \Drupal\Core\Field\WidgetInterface (in most cases, by
* subclassing \Drupal\Core\Field\WidgetBase). Widget plugins need to be in the
* namespace \Drupal\{your_module}\Plugin\Field\FieldWidget.
*
* Widgets are @link forms_api_reference.html Form API @endlink
* elements with additional processing capabilities. The methods of the
* WidgetInterface object are typically called by respective methods in the
* \Drupal\Core\Entity\Entity\EntityFormDisplay class.
*
* @see field
* @see field_types
* @see field_formatter
* @see plugin_api
*/
/**
* Perform alterations on Field API widget types.
*
* @param array $info
* An array of information on existing widget types, as collected by the
* annotation discovery mechanism.
*/
function hook_field_widget_info_alter(array &$info) {
// Let a new field type re-use an existing widget.
$info['options_select']['field_types'][] = 'my_field_type';
}
/**
* Alter forms for field widgets provided by other modules.
*
* @param $element
* The field widget form element as constructed by hook_field_widget_form().
* @param $form_state
* The current state of the form.
* @param $context
* An associative array containing the following key-value pairs:
* - form: The form structure to which widgets are being attached. This may be
* a full form structure, or a sub-element of a larger form.
* - widget: The widget plugin instance.
* - items: The field values, as a
* \Drupal\Core\Field\FieldItemListInterface object.
* - delta: The order of this item in the array of subelements (0, 1, 2, etc).
* - default: A boolean indicating whether the form is being shown as a dummy
* form to set default values.
*
* @see \Drupal\Core\Field\WidgetBase::formSingleElement()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
*/
function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// Add a css class to widget form elements for all fields of type mytype.
$field_definition = $context['items']->getFieldDefinition();
if ($field_definition->getType() == 'mytype') {
// Be sure not to overwrite existing attributes.
$element['#attributes']['class'][] = 'myclass';
}
}
/**
* Alter widget forms for a specific widget provided by another module.
*
* Modules can implement hook_field_widget_WIDGET_TYPE_form_alter() to modify a
* specific widget form, rather than using hook_field_widget_form_alter() and
* checking the widget type.
*
* @param $element
* The field widget form element as constructed by hook_field_widget_form().
* @param $form_state
* The current state of the form.
* @param $context
* An associative array. See hook_field_widget_form_alter() for the structure
* and content of the array.
*
* @see \Drupal\Core\Field\WidgetBase::formSingleElement()
* @see hook_field_widget_form_alter()
*/
function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// Code here will only act on widgets of type WIDGET_TYPE. For example,
// hook_field_widget_mymodule_autocomplete_form_alter() will only act on
// widgets of type 'mymodule_autocomplete'.
$element['#autocomplete_route_name'] = 'mymodule.autocomplete_route';
}
/**
* @} End of "defgroup field_widget".
*/
/**
* @defgroup field_formatter Field Formatter API
* @{
* Define Field API formatter types.
*
* Field API formatters specify how fields are displayed when the entity to
* which the field is attached is displayed. Fields of a given
* @link field_types field type @endlink may be displayed using more than one
* formatter. In this case, the Field UI module allows the site builder to
* choose which formatter to use.
*
* Formatters are Plugins managed by the
* \Drupal\Core\Field\FormatterPluginManager class. A formatter is a plugin
* annotated with class \Drupal\Core\Field\Annotation\FieldFormatter that
* implements \Drupal\Core\Field\FormatterInterface (in most cases, by
* subclassing \Drupal\Core\Field\FormatterBase). Formatter plugins need to be
* in the namespace \Drupal\{your_module}\Plugin\Field\FieldFormatter.
*
* @see field
* @see field_types
* @see field_widget
* @see plugin_api
*/
/**
* Perform alterations on Field API formatter types.
*
* @param array $info
* An array of information on existing formatter types, as collected by the
* annotation discovery mechanism.
*/
function hook_field_formatter_info_alter(array &$info) {
// Let a new field type re-use an existing formatter.
$info['text_default']['field_types'][] = 'my_field_type';
}
/**
* @} End of "defgroup field_formatter".
*/
/**
* Returns the maximum weight for the entity components handled by the module.
*
* Field API takes care of fields and 'extra_fields'. This hook is intended for
* third-party modules adding other entity components (e.g. field_group).
*
* @param string $entity_type
* The type of entity; e.g. 'node' or 'user'.
* @param string $bundle
* The bundle name.
* @param string $context
* The context for which the maximum weight is requested. Either 'form' or
* 'display'.
* @param string $context_mode
* The view or form mode name.
*
* @return int
* The maximum weight of the entity's components, or NULL if no components
* were found.
*
* @ingroup field_info
*/
function hook_field_info_max_weight($entity_type, $bundle, $context, $context_mode) {
$weights = array();
foreach (my_module_entity_additions($entity_type, $bundle, $context, $context_mode) as $addition) {
$weights[] = $addition['weight'];
}
return $weights ? max($weights) : NULL;
}
/**
* @addtogroup field_purge
* @{
*/
/**
* Acts when a field storage definition is being purged.
*
* In field_purge_field_storage(), after the storage definition has been removed
* from the system, the entity storage has purged stored field data, and the
* field definitions cache has been cleared, this hook is invoked on all modules
* to allow them to respond to the field storage being purged.
*
* @param $field_storage \Drupal\field\Entity\FieldStorageConfig
* The field storage being purged.
*/
function hook_field_purge_field_storage(\Drupal\field\Entity\FieldStorageConfig $field_storage) {
db_delete('my_module_field_storage_info')
->condition('uuid', $field_storage->uuid())
->execute();
}
/**
* Acts when a field is being purged.
*
* In field_purge_field(), after the field definition has been removed
* from the system, the entity storage has purged stored field data, and the
* field info cache has been cleared, this hook is invoked on all modules to
* allow them to respond to the field being purged.
*
* @param $field
* The field being purged.
*/
function hook_field_purge_field(\Drupal\field\Entity\FieldConfig $field) {
db_delete('my_module_field_info')
->condition('id', $field->id())
->execute();
}
/**
* @} End of "addtogroup field_purge".
*/
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,6 @@
name: Field
type: module
description: 'Field API to add fields to entities like nodes and users.'
package: Core
version: VERSION
core: 8.x

View file

@ -0,0 +1,306 @@
<?php
/**
* @file
* Attach custom data fields to Drupal entities.
*/
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
/*
* Load all public Field API functions. Drupal currently has no
* mechanism for auto-loading core APIs, so we have to load them on
* every page request.
*/
require_once __DIR__ . '/field.purge.inc';
/**
* @defgroup field Field API
* @{
* Attaches custom data fields to Drupal entities.
*
* The Field API allows custom data fields to be attached to Drupal entities and
* takes care of storing, loading, editing, and rendering field data. Any entity
* type (node, user, etc.) can use the Field API to make itself "fieldable" and
* thus allow fields to be attached to it. Other modules can provide a user
* interface for managing custom fields via a web browser as well as a wide and
* flexible variety of data type, form element, and display format capabilities.
*
* The Field API defines two primary data structures, FieldStorage and Field,
* and the concept of a Bundle. A FieldStorage defines a particular type of data
* that can be attached to entities. A Field is attached to a single
* Bundle. A Bundle is a set of fields that are treated as a group by the Field
* Attach API and is related to a single fieldable entity type.
*
* For example, suppose a site administrator wants Article nodes to have a
* subtitle and photo. Using the Field API or Field UI module, the administrator
* creates a field named 'subtitle' of type 'text' and a field named 'photo' of
* type 'image'. The administrator (again, via a UI) creates two Field
* Instances, one attaching the field 'subtitle' to the 'node' bundle 'article'
* and one attaching the field 'photo' to the 'node' bundle 'article'. When the
* node storage loads an Article node, it loads the values of the
* 'subtitle' and 'photo' fields because they are both attached to the 'node'
* bundle 'article'.
*
* - @link field_types Field Types API @endlink: Defines field types, widget
* types, and display formatters. Field modules use this API to provide field
* types like Text and Node Reference along with the associated form elements
* and display formatters.
*
* - @link field_purge Field API bulk data deletion @endlink: Cleans up after
* bulk deletion operations such as deletion of field storage or field.
*/
/**
* Implements hook_help().
*/
function field_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.field':
$field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? \Drupal::url('help.page', array('name' => 'field_ui')) : '#';
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Field module allows custom data fields to be defined for <em>entity</em> types (see below). The Field module takes care of storing, loading, editing, and rendering field data. Most users will not interact with the Field module directly, but will instead use the <a href="!field-ui-help">Field UI module</a> user interface. Module developers can use the Field API to make new entity types "fieldable" and thus allow fields to be attached to them. For more information, see the <a href="!field">online documentation for the Field module</a>.', array('!field-ui-help' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', array('name' => 'field_ui')) :'#', '!field' => 'https://www.drupal.org/documentation/modules/field')). '</p>';
$output .= '<h3>' . t('Terminology') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Entities and entity types') . '</dt>';
$output .= '<dd>' . t('The website\'s content and configuration is managed using <em>entities</em>, which are grouped into <em>entity types</em>. <em>Content entity types</em> are the entity types for site content (such as the main site content, comments, custom blocks, taxonomy terms, and user accounts). <em>Configuration entity types</em> are used to store configuration information for your site, such as individual views in the Views module, and settings for your main site content types.') . '</dd>';
$output .= '<dt>' . t('Entity sub-types') . '</dt>';
$output .= '<dd>' . t('Some content entity types are further grouped into sub-types (for example, you could have article and page content types within the main site content entity type, and tag and category vocabularies within the taxonomy term entity type); other entity types, such as user accounts, do not have sub-types. Programmers use the term <em>bundle</em> for entity sub-types.') . '</dd>';
$output .= '<dt>' . t('Fields and field types') . '</dt>';
$output .= '<dd>' . t('Content entity types and sub-types store most of their text, file, and other information in <em>fields</em>. Fields are grouped by <em>field type</em>; field types define what type of data can be stored in that field, such as text, images, or taxonomy term references.') . '</dd>';
$output .= '<dt>' . t('Formatters and view modes') . '</dd>';
$output .= '<dd>' . t('Content entity types and sub-types can have one or more <em>view modes</em>, used for displaying the entity items. For instance, a content item could be viewed in full content mode on its own page, teaser mode in a list, or RSS mode in a feed. In each view mode, each field can be hidden or displayed, and if it is displayed, you can choose and configure the <em>formatter</em> that is used to display the field. For instance, a long text field can be displayed trimmed or full-length, and taxonomy term reference fields can be displayed in plain text or linked to the taxonomy term page.') . '</dd>';
$output .= '<dt>' . t('Widgets and form modes') . '</dd>';
$output .= '<dd>' . t('Content entity types and sub-types can have one or more <em>form modes</em>, used for editing. For instance, a content item could be edited in a compact format with only some fields editable, or a full format that allows all fields to be edited. In each form mode, each field can be hidden or displayed, and if it is displayed, you can choose and configure the <em>widget</em> that is used to edit the field. For instance, a taxonomy term reference field can be edited using a select list, radio buttons, or an autocomplete widget.') . '</dd>';
$output .= '</dl>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Enabling field types, widgets, and formatters') . '</dt>';
$output .= '<dd>' . t('The Field module provides the infrastructure for fields; the field types, formatters, and widgets are provided by Drupal core or additional modules. Some of the modules are required; the optional modules can be enabled from the <a href="!modules">Extend administration page</a>. Additional fields, formatters, and widgets may be provided by contributed modules, which you can find in the <a href="!contrib">contributed module section of Drupal.org</a>.', array('!modules' => \Drupal::url('system.modules_list'), '!contrib' => 'https://www.drupal.org/project/modules')) . '</dd>';
$output .= '<h3>' . t('Field, widget, and formatter information') . '</h3>';
// Make a list of all widget, formatter, and field modules currently
// enabled, ordered by displayed module name (module names are not
// translated).
$items = array();
$modules = \Drupal::moduleHandler()->getModuleList();
$widgets = \Drupal::service('plugin.manager.field.widget')->getDefinitions();
$field_types = \Drupal::service('plugin.manager.field.field_type')->getUiDefinitions();
$formatters = \Drupal::service('plugin.manager.field.formatter')->getDefinitions();
$providers = array();
foreach (array_merge($field_types, $widgets, $formatters) as $plugin) {
$providers[] = $plugin['provider'];
}
$providers = array_unique($providers);
sort($providers);
foreach ($providers as $provider) {
// Skip plugins provided by core components as they do not implement
// hook_help().
if (isset($modules[$provider])) {
$display = \Drupal::moduleHandler()->getName($provider);
if (\Drupal::moduleHandler()->implementsHook($provider, 'help')) {
$items[] = \Drupal::l($display, new Url('help.page', array('name' => $provider)));
}
else {
$items[] = $display;
}
}
}
if ($items) {
$output .= '<dt>' . t('Provided by modules') . '</dt>';
$output .= '<dd>' . t('Here is a list of the currently enabled field, formatter, and widget modules:');
$item_list = array(
'#theme' => 'item_list',
'#items' => $items,
);
$output .= \Drupal::service('renderer')->renderPlain($item_list);
$output .= '</dd>';
}
$output .= '<dt>' . t('Provided by Drupal core') . '</dt>';
$output .= '<dd>' . t('As mentioned previously, some field types, widgets, and formatters are provided by Drupal core. Here are some notes on how to use some of these:');
$output .= '<ul>';
$output .= '<li>' . t('<strong>Number fields</strong>: When you add a number field you can choose from three types: <em>decimal</em>, <em>float</em>, and <em>integer</em>. The <em>decimal</em> number field type allows users to enter exact decimal values, with fixed numbers of decimal places. The <em>float</em> number field type allows users to enter approximate decimal values. The <em>integer</em> number field type allows users to enter whole numbers, such as years (for example, 2012) or values (for example, 1, 2, 5, 305). It does not allow decimals.') . '</li>';
$output .= '</ul></dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_cron().
*/
function field_cron() {
// Do a pass of purging on deleted Field API data, if any exists.
$limit = \Drupal::config('field.settings')->get('purge_batch_size');
field_purge_batch($limit);
}
/**
* Implements hook_entity_field_storage_info().
*/
function field_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
if (\Drupal::entityManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
// Query by filtering on the ID as this is more efficient than filtering
// on the entity_type property directly.
$ids = \Drupal::entityQuery('field_storage_config')
->condition('id', $entity_type->id() . '.', 'STARTS_WITH')
->execute();
// Fetch all fields and key them by field name.
$field_storages = FieldStorageConfig::loadMultiple($ids);
$result = array();
foreach ($field_storages as $field_storage) {
$result[$field_storage->getName()] = $field_storage;
}
return $result;
}
}
/**
* Implements hook_entity_bundle_field_info().
*/
function field_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
if (\Drupal::entityManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
// Query by filtering on the ID as this is more efficient than filtering
// on the entity_type property directly.
$ids = \Drupal::entityQuery('field_config')
->condition('id', $entity_type->id() . '.' . $bundle . '.', 'STARTS_WITH')
->execute();
// Fetch all fields and key them by field name.
$field_configs = FieldConfig::loadMultiple($ids);
$result = array();
foreach ($field_configs as $field_instance) {
$result[$field_instance->getName()] = $field_instance;
}
return $result;
}
}
/**
* Implements hook_entity_bundle_rename().
*/
function field_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) {
$fields = entity_load_multiple_by_properties('field_config', array('entity_type' => $entity_type, 'bundle' => $bundle_old, 'include_deleted' => TRUE));
foreach ($fields as $field) {
$id_new = $field->getTargetEntityTypeId() . '.' . $bundle_new . '.' . $field->getName();
$field->set('id', $id_new);
$field->set('bundle', $bundle_new);
// Save non-deleted fields.
if (!$field->isDeleted()) {
$field->allowBundleRename();
$field->save();
}
// Update deleted fields directly in the state storage.
else {
$state = \Drupal::state();
$deleted_fields = $state->get('field.field.deleted') ?: array();
$deleted_fields[$field->uuid] = $field->toArray();
$state->set('field.field.deleted', $deleted_fields);
}
}
}
/**
* Implements hook_entity_bundle_delete().
*
* This deletes the data for the field as well as the field themselves. This
* function actually just marks the data and fields as deleted, leaving the
* garbage collection for a separate process, because it is not always
* possible to delete this much data in a single page request (particularly
* since for some field types, the deletion is more than just a simple DELETE
* query).
*/
function field_entity_bundle_delete($entity_type, $bundle) {
// Get the fields on the bundle.
$fields = entity_load_multiple_by_properties('field_config', array('entity_type' => $entity_type, 'bundle' => $bundle));
foreach ($fields as $field) {
$field->delete();
}
}
/**
* @} End of "defgroup field".
*/
/**
* Assembles a partial entity structure with initial IDs.
*
* @param object $ids
* An object with the properties entity_type (required), entity_id (required),
* revision_id (optional) and bundle (optional).
*
* @return \Drupal\Core\Entity\EntityInterface
* An entity, initialized with the provided IDs.
*/
function _field_create_entity_from_ids($ids) {
$id_properties = array();
$entity_type = \Drupal::entityManager()->getDefinition($ids->entity_type);
if ($id_key = $entity_type->getKey('id')) {
$id_properties[$id_key] = $ids->entity_id;
}
if (isset($ids->revision_id) && $revision_key = $entity_type->getKey('revision')) {
$id_properties[$revision_key] = $ids->revision_id;
}
if (isset($ids->bundle) && $bundle_key = $entity_type->getKey('bundle')) {
$id_properties[$bundle_key] = $ids->bundle;
}
return entity_create($ids->entity_type, $id_properties);
}
/**
* Implements hook_config_import_steps_alter().
*/
function field_config_import_steps_alter(&$sync_steps, ConfigImporter $config_importer) {
$field_storages = \Drupal\field\ConfigImporterFieldPurger::getFieldStoragesToPurge(
$config_importer->getStorageComparer()->getSourceStorage()->read('core.extension'),
$config_importer->getStorageComparer()->getChangelist('delete')
);
if ($field_storages) {
// Add a step to the beginning of the configuration synchronization process
// to purge field data where the module that provides the field is being
// uninstalled.
array_unshift($sync_steps, array('\Drupal\field\ConfigImporterFieldPurger', 'process'));
};
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Adds a warning if field data will be permanently removed by the configuration
* synchronization.
*
* @see \Drupal\field\ConfigImporterFieldPurger
*/
function field_form_config_admin_import_form_alter(&$form, FormStateInterface $form_state) {
// Only display the message when there is a storage comparer available and the
// form is not submitted.
$user_input = $form_state->getUserInput();
$storage_comparer = $form_state->get('storage_comparer');
if ($storage_comparer && empty($user_input)) {
$field_storages = \Drupal\field\ConfigImporterFieldPurger::getFieldStoragesToPurge(
$storage_comparer->getSourceStorage()->read('core.extension'),
$storage_comparer->getChangelist('delete')
);
if ($field_storages) {
foreach ($field_storages as $field) {
$field_labels[] = $field->label();
}
drupal_set_message(\Drupal::translation()->formatPlural(
count($field_storages),
'This synchronization will delete data from the field %fields.',
'This synchronization will delete data from the fields: %fields.',
array('%fields' => implode(', ', $field_labels))
), 'warning');
}
}
}

View file

@ -0,0 +1,181 @@
<?php
/**
* @file
* Provides support for field data purge after mass deletion.
*/
use Drupal\Core\Field\FieldException;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field\FieldConfigInterface;
/**
* @defgroup field_purge Field API bulk data deletion
* @{
* Cleans up after Field API bulk deletion operations.
*
* Field API provides functions for deleting data attached to individual
* entities as well as deleting entire fields or field storages in a single
* operation.
*
* When a single entity is deleted, the Entity storage performs the
* following operations:
* - Invoking the FieldItemListInterface delete() method for each field on the
* entity. A file field type might use this method to delete uploaded files
* from the filesystem.
* - Removing the data from storage.
* - Invoking the global hook_entity_delete() for all modules that implement it.
* Each hook implementation receives the entity being deleted and can operate
* on whichever subset of the entity's bundle's fields it chooses to.
*
* Similar operations are performed on deletion of a single entity revision.
*
* When a bundle, field or field storage is deleted, it is not practical to
* perform those operations immediately on every affected entity in a single
* page request; there could be thousands or millions of them. Instead, the
* appropriate field data items, fields, and/or field storages are marked as
* deleted so that subsequent load or query operations will not return them.
* Later, a separate process cleans up, or "purges", the marked-as-deleted data
* by going through the three-step process described above and, finally,
* removing deleted field storage and field records.
*
* Purging field data is made somewhat tricky by the fact that, while
* $entity->delete() has a complete entity to pass to the various deletion
* steps, the Field API purge process only has the field data it has previously
* stored. It cannot reconstruct complete original entities to pass to the
* deletion operations. It is even possible that the original entity to which
* some Field API data was attached has been itself deleted before the field
* purge operation takes place.
*
* Field API resolves this problem by using stub entities during purge
* operations, containing only the information from the original entity that
* Field API knows about: entity type, ID, revision ID, and bundle. It also
* contains the field data for whichever field is currently being purged.
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Purges a batch of deleted Field API data, field storages, or fields.
*
* This function will purge deleted field data in batches. The batch size
* is defined as an argument to the function, and once each batch is finished,
* it continues with the next batch until all have completed. If a deleted field
* with no remaining data records is found, the field itself will
* be purged. If a deleted field storage with no remaining fields is found, the
* field storage itself will be purged.
*
* @param $batch_size
* The maximum number of field data records to purge before returning.
* @param string $field_storage_uuid
* (optional) Limit the purge to a specific field storage.
*/
function field_purge_batch($batch_size, $field_storage_uuid = NULL) {
$properties = array(
'deleted' => TRUE,
'include_deleted' => TRUE,
);
if ($field_storage_uuid) {
$properties['field_storage_uuid'] = $field_storage_uuid;
}
$fields = entity_load_multiple_by_properties('field_config', $properties);
$info = \Drupal::entityManager()->getDefinitions();
foreach ($fields as $field) {
$entity_type = $field->getTargetEntityTypeId();
// We cannot purge anything if the entity type is unknown (e.g. the
// providing module was uninstalled).
// @todo Revisit after https://www.drupal.org/node/2080823.
if (!isset($info[$entity_type])) {
continue;
}
$count_purged = \Drupal::entityManager()->getStorage($entity_type)->purgeFieldData($field, $batch_size);
if ($count_purged < $batch_size || $count_purged == 0) {
// No field data remains for the field, so we can remove it.
field_purge_field($field);
}
$batch_size -= $count_purged;
// Only delete up to the maximum number of records.
if ($batch_size == 0) {
break;
}
}
// Retrieve all deleted field storages. Any that have no fields can be purged.
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
foreach ($deleted_storages as $field_storage) {
$field_storage = new FieldStorageConfig($field_storage);
if ($field_storage_uuid && $field_storage->uuid() != $field_storage_uuid) {
// If a specific UUID is provided, only purge the corresponding field.
continue;
}
// We cannot purge anything if the entity type is unknown (e.g. the
// providing module was uninstalled).
// @todo Revisit after https://www.drupal.org/node/2080823.
if (!isset($info[$field_storage->getTargetEntityTypeId()])) {
continue;
}
$fields = entity_load_multiple_by_properties('field_config', array('field_storage_uuid' => $field_storage->uuid(), 'include_deleted' => TRUE));
if (empty($fields)) {
field_purge_field_storage($field_storage);
}
}
}
/**
* Purges a field record from the database.
*
* This function assumes all data for the field has already been purged and
* should only be called by field_purge_batch().
*
* @param $field
* The field record to purge.
*/
function field_purge_field(FieldConfigInterface $field) {
$state = \Drupal::state();
$deleted_fields = $state->get('field.field.deleted');
unset($deleted_fields[$field->uuid()]);
$state->set('field.field.deleted', $deleted_fields);
// Invoke external hooks after the cache is cleared for API consistency.
\Drupal::moduleHandler()->invokeAll('field_purge_field', array($field));
}
/**
* Purges a field record from the database.
*
* This function assumes all fields for the field storage has already been
* purged, and should only be called by field_purge_batch().
*
* @param \Drupal\field\FieldStorageConfigInterface $field_storage
* The field storage to purge.
*
* @throws Drupal\field\FieldException
*/
function field_purge_field_storage(FieldStorageConfigInterface $field_storage) {
$fields = entity_load_multiple_by_properties('field_config', array('field_storage_uuid' => $field_storage->uuid(), 'include_deleted' => TRUE));
if (count($fields) > 0) {
throw new FieldException(t('Attempt to purge a field storage @field_name that still has fields.', array('@field_name' => $field_storage->getName())));
}
$state = \Drupal::state();
$deleted_storages = $state->get('field.storage.deleted');
unset($deleted_storages[$field_storage->uuid()]);
$state->set('field.storage.deleted', $deleted_storages);
// Notify the storage layer.
\Drupal::entityManager()->getStorage($field_storage->getTargetEntityTypeId())->finalizePurge($field_storage);
// Invoke external hooks after the cache is cleared for API consistency.
\Drupal::moduleHandler()->invokeAll('field_purge_field_storage', array($field_storage));
}
/**
* @} End of "defgroup field_purge".
*/

View file

@ -0,0 +1,7 @@
services:
field.uninstall_validator:
class: Drupal\field\FieldUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@entity.manager', '@string_translation']
lazy: true

View file

@ -0,0 +1,151 @@
<?php
/**
* @file
* Contains \Drupal\field\ConfigImporterFieldPurger.
*/
namespace Drupal\field;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Processes field purges before a configuration synchronization.
*/
class ConfigImporterFieldPurger {
/**
* Processes fields targeted for purge as part of a configuration sync.
*
* This takes care of deleting the field if necessary, and purging the data on
* the fly.
*
* @param array $context
* The batch context.
* @param \Drupal\Core\Config\ConfigImporter $config_importer
* The config importer.
*/
public static function process(array &$context, ConfigImporter $config_importer) {
if (!isset($context['sandbox']['field'])) {
static::initializeSandbox($context, $config_importer);
}
// Get the list of field storages to purge.
$field_storages = static::getFieldStoragesToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete'));
// Get the first field storage to process.
$field_storage = reset($field_storages);
if (!isset($context['sandbox']['field']['current_storage_id']) || $context['sandbox']['field']['current_storage_id'] != $field_storage->id()) {
$context['sandbox']['field']['current_storage_id'] = $field_storage->id();
// If the storage has not been deleted yet we need to do that. This is the
// case when the storage deletion is staged.
if (!$field_storage->isDeleted()) {
$field_storage->delete();
}
}
field_purge_batch($context['sandbox']['field']['purge_batch_size'], $field_storage->uuid());
$context['sandbox']['field']['current_progress']++;
$fields_to_delete_count = count(static::getFieldStoragesToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete')));
if ($fields_to_delete_count == 0) {
$context['finished'] = 1;
}
else {
$context['finished'] = $context['sandbox']['field']['current_progress'] / $context['sandbox']['field']['steps_to_delete'];
$context['message'] = \Drupal::translation()->translate('Purging field @field_label', array('@field_label' => $field_storage->label()));
}
}
/**
* Initializes the batch context sandbox for processing field deletions.
*
* This calculates the number of steps necessary to purge all the field data
* and saves data for later use.
*
* @param array $context
* The batch context.
* @param \Drupal\Core\Config\ConfigImporter $config_importer
* The config importer.
*/
protected static function initializeSandbox(array &$context, ConfigImporter $config_importer) {
$context['sandbox']['field']['purge_batch_size'] = \Drupal::config('field.settings')->get('purge_batch_size');
// Save the future list of installed extensions to limit the amount of times
// the configuration is read from disk.
$context['sandbox']['field']['extensions'] = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
$context['sandbox']['field']['steps_to_delete'] = 0;
$fields = static::getFieldStoragesToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete'));
foreach ($fields as $field) {
$row_count = \Drupal::entityManager()->getStorage($field->getTargetEntityTypeId())
->countFieldData($field);
if ($row_count > 0) {
// The number of steps to delete each field is determined by the
// purge_batch_size setting. For example if the field has 9 rows and the
// batch size is 10 then this will add 1 step to $number_of_steps.
$how_many_steps = ceil($row_count / $context['sandbox']['field']['purge_batch_size']);
$context['sandbox']['field']['steps_to_delete'] += $how_many_steps;
}
}
// Each field possibly needs one last field_purge_batch() call to remove the
// last field and the field storage itself.
$context['sandbox']['field']['steps_to_delete'] += count($fields);
$context['sandbox']['field']['current_progress'] = 0;
}
/**
* Gets the list of fields to purge before configuration synchronization.
*
* If, during a configuration synchronization, a field is being deleted and
* the module that provides the field type is being uninstalled then the field
* data must be purged before the module is uninstalled. Also, if deleted
* fields exist whose field types are provided by modules that are being
* uninstalled their data need to be purged too.
*
* @param array $extensions
* The list of extensions that will be enabled after the configuration
* synchronization has finished.
* @param array $deletes
* The configuration that will be deleted by the configuration
* synchronization.
*
* @return \Drupal\field\Entity\FieldStorageConfig[]
* An array of field storages that need purging before configuration can be
* synchronized.
*/
public static function getFieldStoragesToPurge(array $extensions, array $deletes) {
$providers = array_keys($extensions['module']);
$providers[] = 'core';
$storages_to_delete = array();
// Gather fields that will be deleted during configuration synchronization
// where the module that provides the field type is also being uninstalled.
$field_storage_ids = array();
foreach ($deletes as $config_name) {
$field_storage_config_prefix = \Drupal::entityManager()->getDefinition('field_storage_config')->getConfigPrefix();
if (strpos($config_name, $field_storage_config_prefix . '.') === 0) {
$field_storage_ids[] = ConfigEntityStorage::getIDFromConfigName($config_name, $field_storage_config_prefix);
}
}
if (!empty($field_storage_ids)) {
$field_storages = \Drupal::entityQuery('field_storage_config')
->condition('id', $field_storage_ids, 'IN')
->condition('module', $providers, 'NOT IN')
->execute();
if (!empty($field_storages)) {
$storages_to_delete = FieldStorageConfig::loadMultiple($field_storages);
}
}
// Gather deleted fields from modules that are being uninstalled.
/** @var \Drupal\field\FieldStorageConfigInterface[] $field_storages */
$field_storages = entity_load_multiple_by_properties('field_storage_config', array('deleted' => TRUE, 'include_deleted' => TRUE));
foreach ($field_storages as $field_storage) {
if (!in_array($field_storage->getTypeProvider(), $providers)) {
$storages_to_delete[$field_storage->id()] = $field_storage;
}
}
return $storages_to_delete;
}
}

View file

@ -0,0 +1,348 @@
<?php
/**
* @file
* Contains \Drupal\field\Entity\FieldConfig.
*/
namespace Drupal\field\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldConfigBase;
use Drupal\Core\Field\FieldException;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field\FieldConfigInterface;
/**
* Defines the Field entity.
*
* @ConfigEntityType(
* id = "field_config",
* label = @Translation("Field"),
* handlers = {
* "access" = "Drupal\field\FieldConfigAccessControlHandler",
* "storage" = "Drupal\field\FieldConfigStorage"
* },
* config_prefix = "field",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "field_name",
* "entity_type",
* "bundle",
* "label",
* "description",
* "required",
* "translatable",
* "default_value",
* "default_value_callback",
* "settings",
* "field_type",
* }
* )
*/
class FieldConfig extends FieldConfigBase implements FieldConfigInterface {
/**
* Flag indicating whether the field is deleted.
*
* The delete() method marks the field as "deleted" and removes the
* corresponding entry from the config storage, but keeps its definition in
* the state storage while field data is purged by a separate
* garbage-collection process.
*
* Deleted fields stay out of the regular entity lifecycle (notably, their
* values are not populated in loaded entities, and are not saved back).
*
* @var bool
*/
protected $deleted = FALSE;
/**
* The associated FieldStorageConfig entity.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* Constructs a FieldConfig object.
*
* In most cases, Field entities are created via
* entity_create('field_config', $values), where $values is the same
* parameter as in this constructor.
*
* @param array $values
* An array of field properties, keyed by property name. The
* storage associated to the field can be specified either with:
* - field_storage: the FieldStorageConfigInterface object,
* or by referring to an existing field storage in the current configuration
* with:
* - field_name: The field name.
* - entity_type: The entity type.
* Additionally, a 'bundle' property is required to indicate the entity
* bundle to which the field is attached to. Other array elements will be
* used to set the corresponding properties on the class; see the class
* property documentation for details.
*
* @see entity_create()
*/
public function __construct(array $values, $entity_type = 'field_config') {
// Allow either an injected FieldStorageConfig object, or a field_name and
// entity_type.
if (isset($values['field_storage'])) {
if (!$values['field_storage'] instanceof FieldStorageConfigInterface) {
throw new FieldException('Attempt to create a configurable field for a non-configurable field storage.');
}
$field_storage = $values['field_storage'];
$values['field_name'] = $field_storage->getName();
$values['entity_type'] = $field_storage->getTargetEntityTypeId();
// The internal property is fieldStorage, not field_storage.
unset($values['field_storage']);
$values['fieldStorage'] = $field_storage;
}
else {
if (empty($values['field_name'])) {
throw new FieldException('Attempt to create a field without a field_name.');
}
if (empty($values['entity_type'])) {
throw new FieldException(SafeMarkup::format('Attempt to create a field @field_name without an entity_type.', array('@field_name' => $values['field_name'])));
}
}
// 'bundle' is required in either case.
if (empty($values['bundle'])) {
throw new FieldException(SafeMarkup::format('Attempt to create a field @field_name without a bundle.', array('@field_name' => $values['field_name'])));
}
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageInterface $storage) {
parent::postCreate($storage);
// Validate that we have a valid storage for this field. This throws an
// exception if the storage is invalid.
$this->getFieldStorageDefinition();
// 'Label' defaults to the field name (mostly useful for fields created in
// tests).
if (empty($this->label)) {
$this->label = $this->getName();
}
}
/**
* Overrides \Drupal\Core\Entity\Entity::preSave().
*
* @throws \Drupal\Core\Field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
public function preSave(EntityStorageInterface $storage) {
$entity_manager = \Drupal::entityManager();
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$storage_definition = $this->getFieldStorageDefinition();
// Filter out unknown settings and make sure all settings are present, so
// that a complete field definition is passed to the various hooks and
// written to config.
$default_settings = $field_type_manager->getDefaultFieldSettings($storage_definition->getType());
$this->settings = array_intersect_key($this->settings, $default_settings) + $default_settings;
if ($this->isNew()) {
// Notify the entity storage.
$entity_manager->onFieldDefinitionCreate($this);
}
else {
// Some updates are always disallowed.
if ($this->entity_type != $this->original->entity_type) {
throw new FieldException("Cannot change an existing field's entity_type.");
}
if ($this->bundle != $this->original->bundle && empty($this->bundleRenameAllowed)) {
throw new FieldException("Cannot change an existing field's bundle.");
}
if ($storage_definition->uuid() != $this->original->getFieldStorageDefinition()->uuid()) {
throw new FieldException("Cannot change an existing field's storage.");
}
// Notify the entity storage.
$entity_manager->onFieldDefinitionUpdate($this, $this->original);
}
parent::preSave($storage);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Mark the field_storage_config as a a dependency.
$this->addDependency('config', $this->getFieldStorageDefinition()->getConfigDependencyName());
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $fields) {
$state = \Drupal::state();
parent::preDelete($storage, $fields);
// Keep the field definitions in the state storage so we can use them
// later during field_purge_batch().
$deleted_fields = $state->get('field.field.deleted') ?: array();
foreach ($fields as $field) {
if (!$field->deleted) {
$config = $field->toArray();
$config['deleted'] = TRUE;
$config['field_storage_uuid'] = $field->getFieldStorageDefinition()->uuid();
$deleted_fields[$field->uuid()] = $config;
}
}
$state->set('field.field.deleted', $deleted_fields);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $fields) {
// Clear the cache upfront, to refresh the results of getBundles().
\Drupal::entityManager()->clearCachedFieldDefinitions();
// Notify the entity storage.
foreach ($fields as $field) {
if (!$field->deleted) {
\Drupal::entityManager()->onFieldDefinitionDelete($field);
}
}
// If this is part of a configuration synchronization then the following
// configuration updates are not necessary.
$entity = reset($fields);
if ($entity->isSyncing()) {
return;
}
// Delete the associated field storages if they are not used anymore and are
// not persistent.
$storages_to_delete = array();
foreach ($fields as $field) {
$storage_definition = $field->getFieldStorageDefinition();
if (!$field->deleted && !$field->isUninstalling() && $storage_definition->isDeletable()) {
// Key by field UUID to avoid deleting the same storage twice.
$storages_to_delete[$storage_definition->uuid()] = $storage_definition;
}
}
if ($storages_to_delete) {
\Drupal::entityManager()->getStorage('field_storage_config')->delete($storages_to_delete);
}
}
/**
* {@inheritdoc}
*/
protected function linkTemplates() {
$link_templates = parent::linkTemplates();
if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
$link_templates["{$this->entity_type}-field-edit-form"] = 'entity.field_config.' . $this->entity_type . '_field_edit_form';
$link_templates["{$this->entity_type}-storage-edit-form"] = 'entity.field_config.' . $this->entity_type . '_storage_edit_form';
$link_templates["{$this->entity_type}-field-delete-form"] = 'entity.field_config.' . $this->entity_type . '_field_delete_form';
if (isset($link_templates['config-translation-overview'])) {
$link_templates["config-translation-overview.{$this->entity_type}"] = "entity.field_config.config_translation_overview.{$this->entity_type}";
}
}
return $link_templates;
}
/**
* {@inheritdoc}
*/
protected function urlRouteParameters($rel) {
$parameters = parent::urlRouteParameters($rel);
$entity_type = \Drupal::entityManager()->getDefinition($this->entity_type);
$parameters[$entity_type->getBundleEntityType()] = $this->bundle;
return $parameters;
}
/**
* {@inheritdoc}
*/
public function isDeleted() {
return $this->deleted;
}
/**
* {@inheritdoc}
*/
public function getFieldStorageDefinition() {
if (!$this->fieldStorage) {
$fields = $this->entityManager()->getFieldStorageDefinitions($this->entity_type);
if (!isset($fields[$this->field_name])) {
throw new FieldException(SafeMarkup::format('Attempt to create a field @field_name that does not exist on entity type @entity_type.', array('@field_name' => $this->field_name, '@entity_type' => $this->entity_type))); }
if (!$fields[$this->field_name] instanceof FieldStorageConfigInterface) {
throw new FieldException(SafeMarkup::format('Attempt to create a configurable field of non-configurable field storage @field_name.', array('@field_name' => $this->field_name, '@entity_type' => $this->entity_type)));
}
$this->fieldStorage = $fields[$this->field_name];
}
return $this->fieldStorage;
}
/**
* {@inheritdoc}
*/
public function isDisplayConfigurable($context) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getDisplayOptions($display_context) {
// Hide configurable fields by default.
return array('type' => 'hidden');
}
/**
* {@inheritdoc}
*/
public function isReadOnly() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isComputed() {
return FALSE;
}
/**
* Loads a field config entity based on the entity type and field name.
*
* @param string $entity_type_id
* ID of the entity type.
* @param string $bundle
* Bundle name.
* @param string $field_name
* Name of the field.
*
* @return static
* The field config entity if one exists for the provided field
* name, otherwise NULL.
*/
public static function loadByName($entity_type_id, $bundle, $field_name) {
return \Drupal::entityManager()->getStorage('field_config')->load($entity_type_id . '.' . $bundle . '.' . $field_name);
}
}

View file

@ -0,0 +1,823 @@
<?php
/**
* @file
* Contains \Drupal\field\Entity\FieldStorageConfig.
*/
namespace Drupal\field\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\OptionsProviderInterface;
use Drupal\field\FieldStorageConfigInterface;
/**
* Defines the Field storage configuration entity.
*
* @ConfigEntityType(
* id = "field_storage_config",
* label = @Translation("Field storage"),
* handlers = {
* "storage" = "Drupal\field\FieldStorageConfigStorage"
* },
* config_prefix = "storage",
* entity_keys = {
* "id" = "id",
* "label" = "id"
* },
* config_export = {
* "id",
* "field_name",
* "entity_type",
* "type",
* "settings",
* "module",
* "locked",
* "cardinality",
* "translatable",
* "indexes",
* "persist_with_no_fields",
* }
* )
*/
class FieldStorageConfig extends ConfigEntityBase implements FieldStorageConfigInterface {
/**
* The maximum length of the field name, in characters.
*
* For fields created through Field UI, this includes the 'field_' prefix.
*/
const NAME_MAX_LENGTH = 32;
/**
* The field ID.
*
* The ID consists of 2 parts: the entity type and the field name.
*
* Example: node.body, user.field_main_image.
*
* @var string
*/
protected $id;
/**
* The field name.
*
* This is the name of the property under which the field values are placed in
* an entity: $entity->{$field_name}. The maximum length is
* Field:NAME_MAX_LENGTH.
*
* Example: body, field_main_image.
*
* @var string
*/
protected $field_name;
/**
* The name of the entity type the field can be attached to.
*
* @var string
*/
protected $entity_type;
/**
* The field type.
*
* Example: text, integer.
*
* @var string
*/
protected $type;
/**
* The name of the module that provides the field type.
*
* @var string
*/
protected $module;
/**
* Field-type specific settings.
*
* An array of key/value pairs, The keys and default values are defined by the
* field type.
*
* @var array
*/
protected $settings = [];
/**
* The field cardinality.
*
* The maximum number of values the field can hold. Possible values are
* positive integers or
* FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED. Defaults to 1.
*
* @var int
*/
protected $cardinality = 1;
/**
* Flag indicating whether the field is translatable.
*
* Defaults to TRUE.
*
* @var bool
*/
protected $translatable = TRUE;
/**
* Flag indicating whether the field is available for editing.
*
* If TRUE, some actions not available though the UI (but are still possible
* through direct API manipulation):
* - field settings cannot be changed,
* - new fields cannot be created
* - existing fields cannot be deleted.
* Defaults to FALSE.
*
* @var bool
*/
protected $locked = FALSE;
/**
* Flag indicating whether the field storage should be deleted when orphaned.
*
* By default field storages for configurable fields are removed when there
* are no remaining fields using them. If multiple modules provide bundles
* which need to use the same field storage then setting this to TRUE will
* preserve the field storage regardless of what happens to the bundles. The
* classic use case for this is node body field storage since Book, Forum, the
* Standard profile and bundle (node type) creation through the UI all use
* same field storage.
*
* @var bool
*/
protected $persist_with_no_fields = FALSE;
/**
* The custom storage indexes for the field data storage.
*
* This set of indexes is merged with the "default" indexes specified by the
* field type in hook_field_schema() to determine the actual set of indexes
* that get created.
*
* The indexes are defined using the same definition format as Schema API
* index specifications. Only columns that are part of the field schema, as
* defined by the field type in hook_field_schema(), are allowed.
*
* Some storage backends might not support indexes, and discard that
* information.
*
* @var array
*/
protected $indexes = [];
/**
* Flag indicating whether the field is deleted.
*
* The delete() method marks the field as "deleted" and removes the
* corresponding entry from the config storage, but keeps its definition in
* the state storage while field data is purged by a separate
* garbage-collection process.
*
* Deleted fields stay out of the regular entity lifecycle (notably, their
* values are not populated in loaded entities, and are not saved back).
*
* @var bool
*/
protected $deleted = FALSE;
/**
* The field schema.
*
* @var array
*/
protected $schema;
/**
* An array of field property definitions.
*
* @var \Drupal\Core\TypedData\DataDefinitionInterface[]
*
* @see \Drupal\Core\TypedData\ComplexDataDefinitionInterface::getPropertyDefinitions()
*/
protected $propertyDefinitions;
/**
* Static flag set to prevent recursion during field deletes.
*
* @var bool
*/
protected static $inDeletion = FALSE;
/**
* Constructs a FieldStorageConfig object.
*
* @param array $values
* An array of field properties, keyed by property name. Most array
* elements will be used to set the corresponding properties on the class;
* see the class property documentation for details. Some array elements
* have special meanings and a few are required. Special elements are:
* - name: required. As a temporary Backwards Compatibility layer right now,
* a 'field_name' property can be accepted in place of 'id'.
* - entity_type: required.
* - type: required.
*
* In most cases, Field entities are created via
* entity_create('field_storage_config', $values)), where $values is the same
* parameter as in this constructor.
*
* @see entity_create()
*/
public function __construct(array $values, $entity_type = 'field_storage_config') {
// Check required properties.
if (empty($values['field_name'])) {
throw new FieldException('Attempt to create a field storage without a field name.');
}
if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $values['field_name'])) {
throw new FieldException(SafeMarkup::format('Attempt to create a field storage @field_name with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character', array('@field_name' => $values['field_name'])));
}
if (empty($values['type'])) {
throw new FieldException(SafeMarkup::format('Attempt to create a field storage @field_name with no type.', array('@field_name' => $values['field_name'])));
}
if (empty($values['entity_type'])) {
throw new FieldException(SafeMarkup::format('Attempt to create a field storage @field_name with no entity_type.', array('@field_name' => $values['field_name'])));
}
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->getTargetEntityTypeId() . '.' . $this->getName();
}
/**
* Overrides \Drupal\Core\Entity\Entity::preSave().
*
* @throws \Drupal\Core\Field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
public function preSave(EntityStorageInterface $storage) {
// Clear the derived data about the field.
unset($this->schema);
// Filter out unknown settings and make sure all settings are present, so
// that a complete field definition is passed to the various hooks and
// written to config.
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$default_settings = $field_type_manager->getDefaultStorageSettings($this->type);
$this->settings = array_intersect_key($this->settings, $default_settings) + $default_settings;
if ($this->isNew()) {
$this->preSaveNew($storage);
}
else {
$this->preSaveUpdated($storage);
}
parent::preSave($storage);
}
/**
* Prepares saving a new field definition.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage.
*
* @throws \Drupal\Core\Field\FieldException If the field definition is invalid.
*/
protected function preSaveNew(EntityStorageInterface $storage) {
$entity_manager = \Drupal::entityManager();
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
// Assign the ID.
$this->id = $this->id();
// Field name cannot be longer than FieldStorageConfig::NAME_MAX_LENGTH characters.
// We use Unicode::strlen() because the DB layer assumes that column widths
// are given in characters rather than bytes.
if (Unicode::strlen($this->getName()) > static::NAME_MAX_LENGTH) {
throw new FieldException(SafeMarkup::format(
'Attempt to create a field storage with an name longer than @max characters: %name', array(
'@max' => static::NAME_MAX_LENGTH,
'%name' => $this->getName(),
)
));
}
// Disallow reserved field names.
$disallowed_field_names = array_keys($entity_manager->getBaseFieldDefinitions($this->getTargetEntityTypeId()));
if (in_array($this->getName(), $disallowed_field_names)) {
throw new FieldException(SafeMarkup::format('Attempt to create field storage %name which is reserved by entity type %type.', array('%name' => $this->getName(), '%type' => $this->getTargetEntityTypeId())));
}
// Check that the field type is known.
$field_type = $field_type_manager->getDefinition($this->getType(), FALSE);
if (!$field_type) {
throw new FieldException(SafeMarkup::format('Attempt to create a field storage of unknown type %type.', array('%type' => $this->getType())));
}
$this->module = $field_type['provider'];
// Notify the entity manager.
$entity_manager->onFieldStorageDefinitionCreate($this);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Ensure the field is dependent on the providing module.
$this->addDependency('module', $this->getTypeProvider());
// Ensure the field is dependent on the provider of the entity type.
$entity_type = \Drupal::entityManager()->getDefinition($this->entity_type);
$this->addDependency('module', $entity_type->getProvider());
return $this->dependencies;
}
/**
* Prepares saving an updated field definition.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage.
*/
protected function preSaveUpdated(EntityStorageInterface $storage) {
$module_handler = \Drupal::moduleHandler();
$entity_manager = \Drupal::entityManager();
// Some updates are always disallowed.
if ($this->getType() != $this->original->getType()) {
throw new FieldException("Cannot change the field type for an existing field storage.");
}
if ($this->getTargetEntityTypeId() != $this->original->getTargetEntityTypeId()) {
throw new FieldException("Cannot change the entity type for an existing field storage.");
}
// See if any module forbids the update by throwing an exception. This
// invokes hook_field_storage_config_update_forbid().
$module_handler->invokeAll('field_storage_config_update_forbid', array($this, $this->original));
// Notify the entity manager. A listener can reject the definition
// update as invalid by raising an exception, which stops execution before
// the definition is written to config.
$entity_manager->onFieldStorageDefinitionUpdate($this, $this->original);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
if ($update) {
// Invalidate the render cache for all affected entities.
$entity_manager = \Drupal::entityManager();
$entity_type = $this->getTargetEntityTypeId();
if ($entity_manager->hasHandler($entity_type, 'view_builder')) {
$entity_manager->getViewBuilder($entity_type)->resetCache();
}
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $field_storages) {
$state = \Drupal::state();
// Set the static flag so that we don't delete field storages whilst
// deleting fields.
static::$inDeletion = TRUE;
// Delete or fix any configuration that is dependent, for example, fields.
parent::preDelete($storage, $field_storages);
// Keep the field definitions in the state storage so we can use them later
// during field_purge_batch().
$deleted_storages = $state->get('field.storage.deleted') ?: array();
foreach ($field_storages as $field_storage) {
if (!$field_storage->deleted) {
$config = $field_storage->toArray();
$config['deleted'] = TRUE;
$config['bundles'] = $field_storage->getBundles();
$deleted_storages[$field_storage->uuid()] = $config;
}
}
$state->set('field.storage.deleted', $deleted_storages);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $fields) {
// Notify the storage.
foreach ($fields as $field) {
if (!$field->deleted) {
\Drupal::entityManager()->onFieldStorageDefinitionDelete($field);
$field->deleted = TRUE;
}
}
// Unset static flag.
static::$inDeletion = FALSE;
}
/**
* {@inheritdoc}
*/
public function getSchema() {
if (!isset($this->schema)) {
// Get the schema from the field item class.
$class = $this->getFieldItemClass();
$schema = $class::schema($this);
// Fill in default values for optional entries.
$schema += array(
'unique keys' => array(),
'indexes' => array(),
'foreign keys' => array(),
);
// Merge custom indexes with those specified by the field type. Custom
// indexes prevail.
$schema['indexes'] = $this->indexes + $schema['indexes'];
$this->schema = $schema;
}
return $this->schema;
}
/**
* {@inheritdoc}
*/
public function hasCustomStorage() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isBaseField() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getColumns() {
$schema = $this->getSchema();
// A typical use case for the method is to iterate on the columns, while
// some other use cases rely on identifying the first column with the key()
// function. Since the schema is persisted in the Field object, we take care
// of resetting the array pointer so that the former does not interfere with
// the latter.
reset($schema['columns']);
return $schema['columns'];
}
/**
* {@inheritdoc}
*/
public function getBundles() {
if (!$this->isDeleted()) {
$map = \Drupal::entityManager()->getFieldMap();
if (isset($map[$this->getTargetEntityTypeId()][$this->getName()]['bundles'])) {
return $map[$this->getTargetEntityTypeId()][$this->getName()]['bundles'];
}
}
return array();
}
/**
* {@inheritdoc}
*/
public function getName() {
return $this->field_name;
}
/**
* {@inheritdoc}
*/
public function isDeleted() {
return $this->deleted;
}
/**
* {@inheritdoc}
*/
public function getTypeProvider() {
return $this->module;
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->type;
}
/**
* {@inheritdoc}
*/
public function getSettings() {
// @todo FieldTypePluginManager maintains its own static cache. However, do
// some CPU and memory profiling to see if it's worth statically caching
// $field_type_info, or the default field storage and field settings,
// within $this.
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$settings = $field_type_manager->getDefaultStorageSettings($this->getType());
return $this->settings + $settings;
}
/**
* {@inheritdoc}
*/
public function getSetting($setting_name) {
// @todo See getSettings() about potentially statically caching this.
// We assume here that one call to array_key_exists() is more efficient
// than calling getSettings() when all we need is a single setting.
if (array_key_exists($setting_name, $this->settings)) {
return $this->settings[$setting_name];
}
$settings = $this->getSettings();
if (array_key_exists($setting_name, $settings)) {
return $settings[$setting_name];
}
else {
return NULL;
}
}
/**
* {@inheritdoc}
*/
public function setSetting($setting_name, $value) {
$this->settings[$setting_name] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function setSettings(array $settings) {
$this->settings = $settings;
return $this;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
return $this->translatable;
}
/**
* {@inheritdoc}
*/
public function isRevisionable() {
// All configurable fields are revisionable.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function setTranslatable($translatable) {
$this->translatable = $translatable;
return $this;
}
/**
* {@inheritdoc}
*/
public function getProvider() {
return 'field';
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->label();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getCardinality() {
return $this->cardinality;
}
/**
* {@inheritdoc}
*/
public function setCardinality($cardinality) {
$this->cardinality = $cardinality;
return $this;
}
/**
* {@inheritdoc}
*/
public function getOptionsProvider($property_name, FieldableEntityInterface $entity) {
// If the field item class implements the interface, create an orphaned
// runtime item object, so that it can be used as the options provider
// without modifying the entity being worked on.
if (is_subclass_of($this->getFieldItemClass(), '\Drupal\Core\TypedData\OptionsProviderInterface')) {
$items = $entity->get($this->getName());
return \Drupal::service('plugin.manager.field.field_type')->createFieldItem($items, 0);
}
// @todo: Allow setting custom options provider, see
// https://www.drupal.org/node/2002138.
}
/**
* {@inheritdoc}
*/
public function isMultiple() {
$cardinality = $this->getCardinality();
return ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) || ($cardinality > 1);
}
/**
* {@inheritdoc}
*/
public function isLocked() {
return $this->locked;
}
/**
* {@inheritdoc}
*/
public function setLocked($locked) {
$this->locked = $locked;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTargetEntityTypeId() {
return $this->entity_type;
}
/**
* {@inheritdoc}
*/
public function isQueryable() {
return TRUE;
}
/**
* Determines whether a field has any data.
*
* @return bool
* TRUE if the field has data for any entity; FALSE otherwise.
*/
public function hasData() {
return \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE);
}
/**
* Implements the magic __sleep() method.
*
* Using the Serialize interface and serialize() / unserialize() methods
* breaks entity forms in PHP 5.4.
* @todo Investigate in https://www.drupal.org/node/2074253.
*/
public function __sleep() {
// Only serialize necessary properties, excluding those that can be
// recalculated.
$properties = get_object_vars($this);
unset($properties['schema'], $properties['propertyDefinitions'], $properties['original']);
return array_keys($properties);
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
return array();
}
/**
* {@inheritdoc}
*/
public function getConstraint($constraint_name) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getPropertyDefinition($name) {
if (!isset($this->propertyDefinitions)) {
$this->getPropertyDefinitions();
}
if (isset($this->propertyDefinitions[$name])) {
return $this->propertyDefinitions[$name];
}
}
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
if (!isset($this->propertyDefinitions)) {
$class = $this->getFieldItemClass();
$this->propertyDefinitions = $class::propertyDefinitions($this);
}
return $this->propertyDefinitions;
}
/**
* {@inheritdoc}
*/
public function getPropertyNames() {
return array_keys($this->getPropertyDefinitions());
}
/**
* {@inheritdoc}
*/
public function getMainPropertyName() {
$class = $this->getFieldItemClass();
return $class::mainPropertyName();
}
/**
* {@inheritdoc}
*/
public function getUniqueStorageIdentifier() {
return $this->uuid();
}
/**
* Helper to retrieve the field item class.
*/
protected function getFieldItemClass() {
$type_definition = \Drupal::typedDataManager()
->getDefinition('field_item:' . $this->getType());
return $type_definition['class'];
}
/**
* Loads a field config entity based on the entity type and field name.
*
* @param string $entity_type_id
* ID of the entity type.
* @param string $field_name
* Name of the field.
*
* @return static
* The field config entity if one exists for the provided field name,
* otherwise NULL.
*/
public static function loadByName($entity_type_id, $field_name) {
return \Drupal::entityManager()->getStorage('field_storage_config')->load($entity_type_id . '.' . $field_name);
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
// The field storage is not deleted, is configured to be removed when there
// are no fields, the field storage has no bundles, and field storages are
// not in the process of being deleted.
return !$this->deleted && !$this->persist_with_no_fields && count($this->getBundles()) == 0 && !static::$inDeletion;
}
/**
* {@inheritdoc}
*/
public function getIndexes() {
return $this->indexes;
}
/**
* {@inheritdoc}
*/
public function setIndexes(array $indexes) {
$this->indexes = $indexes;
return $this;
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldConfigAccessControlHandler.
*/
namespace Drupal\field;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the field entity type.
*
* @see \Drupal\field\Entity\FieldConfig
*/
class FieldConfigAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
if ($operation == 'delete') {
$field_storage_entity = $entity->getFieldStorageDefinition();
if ($field_storage_entity->isLocked()) {
return AccessResult::forbidden()->cacheUntilEntityChanges($field_storage_entity);
}
else {
return AccessResult::allowedIfHasPermission($account, 'administer ' . $entity->getTargetEntityTypeId() . ' fields')->cacheUntilEntityChanges($field_storage_entity);
}
}
return AccessResult::allowedIfHasPermission($account, 'administer ' . $entity->getTargetEntityTypeId() . ' fields');
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldConfigInterface.
*/
namespace Drupal\field;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
* Provides an interface defining a field entity.
*/
interface FieldConfigInterface extends ConfigEntityInterface, FieldDefinitionInterface {
/**
* Gets the deleted flag of the field.
*
* @return bool
* Returns TRUE if the field is deleted.
*/
public function isDeleted();
}

View file

@ -0,0 +1,185 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldConfigStorage.
*/
namespace Drupal\field;
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldConfigStorageBase;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\State\StateInterface;
/**
* Controller class for fields.
*/
class FieldConfigStorage extends FieldConfigStorageBase {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The state keyvalue collection.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a FieldConfigStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, StateInterface $state, FieldTypePluginManagerInterface $field_type_manager) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
$this->entityManager = $entity_manager;
$this->state = $state;
$this->fieldTypeManager = $field_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('entity.manager'),
$container->get('state'),
$container->get('plugin.manager.field.field_type')
);
}
/**
* {@inheritdoc}
*/
public function importDelete($name, Config $new_config, Config $old_config) {
// If the field storage has been deleted in the same import, the field will
// be deleted by then, and there is nothing left to do. Just return TRUE so
// that the file does not get written to active store.
if (!$old_config->get()) {
return TRUE;
}
return parent::importDelete($name, $new_config, $old_config);
}
/**
* {@inheritdoc}
*/
public function loadByProperties(array $conditions = array()) {
// Include deleted fields if specified in the $conditions parameters.
$include_deleted = isset($conditions['include_deleted']) ? $conditions['include_deleted'] : FALSE;
unset($conditions['include_deleted']);
$fields = array();
// Get fields stored in configuration. If we are explicitly looking for
// deleted fields only, this can be skipped, because they will be
// retrieved from state below.
if (empty($conditions['deleted'])) {
if (isset($conditions['entity_type']) && isset($conditions['bundle']) && isset($conditions['field_name'])) {
// Optimize for the most frequent case where we do have a specific ID.
$id = $conditions['entity_type'] . '.' . $conditions['bundle'] . '.' . $conditions['field_name'];
$fields = $this->loadMultiple(array($id));
}
else {
// No specific ID, we need to examine all existing fields.
$fields = $this->loadMultiple();
}
}
// Merge deleted fields (stored in state) if needed.
if ($include_deleted || !empty($conditions['deleted'])) {
$deleted_fields = $this->state->get('field.field.deleted') ?: array();
$deleted_storages = $this->state->get('field.storage.deleted') ?: array();
foreach ($deleted_fields as $id => $config) {
// If the field storage itself is deleted, inject it directly in the field.
if (isset($deleted_storages[$config['field_storage_uuid']])) {
$config['field_storage'] = $this->entityManager->getStorage('field_storage_config')->create($deleted_storages[$config['field_storage_uuid']]);
}
$fields[$id] = $this->create($config);
}
}
// Collect matching fields.
$matching_fields = array();
foreach ($fields as $field) {
// Some conditions are checked against the field storage.
$field_storage = $field->getFieldStorageDefinition();
// Only keep the field if it matches all conditions.
foreach ($conditions as $key => $value) {
// Extract the actual value against which the condition is checked.
switch ($key) {
case 'field_name':
$checked_value = $field_storage->getName();
break;
case 'field_id':
case 'field_storage_uuid':
$checked_value = $field_storage->uuid();
break;
case 'uuid';
$checked_value = $field->uuid();
break;
case 'deleted';
$checked_value = $field->isDeleted();
break;
default:
$checked_value = $field->get($key);
break;
}
// Skip to the next field as soon as one condition does not match.
if ($checked_value != $value) {
continue 2;
}
}
// When returning deleted fields, key the results by UUID since they
// can include several fields with the same ID.
$key = $include_deleted ? $field->uuid() : $field->id();
$matching_fields[$key] = $field;
}
return $matching_fields;
}
}

View file

@ -0,0 +1,136 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldStorageConfigInterface.
*/
namespace Drupal\field;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Provides an interface defining a field storage entity.
*/
interface FieldStorageConfigInterface extends ConfigEntityInterface, FieldStorageDefinitionInterface {
/**
* Returns the field type.
*
* @return string
* The field type, i.e. the id of a field type plugin. For example 'text'.
*/
public function getType();
/**
* Returns the name of the module providing the field type.
*
* @return string
* The name of the module that provides the field type.
*/
public function getTypeProvider();
/**
* Returns the list of bundles where the field storage has fields.
*
* @return array
* An array of bundle names.
*/
public function getBundles();
/**
* Returns whether the field is deleted or not.
*
* @return bool
* TRUE if the field is deleted.
*/
public function isDeleted();
/**
* Checks if the field storage can be deleted.
*
* @return bool
* TRUE if the field storage can be deleted.
*/
public function isDeletable();
/**
* Returns whether the field storage is locked or not.
*
* @return bool
* TRUE if the field storage is locked.
*/
public function isLocked();
/**
* Sets the locked flag.
*
* @param bool $locked
* Sets value of locked flag.
*
* @return $this
*/
public function setLocked($locked);
/**
* Sets the maximum number of items allowed for the field.
*
* @param int $cardinality
* The cardinality value.
*
* @return $this
*/
public function setCardinality($cardinality);
/**
* Sets the value for a field setting by name.
*
* @param string $setting_name
* The name of the setting.
* @param mixed $value
* The value of the setting.
*
* @return $this
*/
public function setSetting($setting_name, $value);
/**
* Sets field settings (overwrites existing settings).
*
* @param array $settings
* The array of field settings.
*
* @return $this
*/
public function setSettings(array $settings);
/**
* Sets whether the field is translatable.
*
* @param bool $translatable
* Whether the field is translatable.
*
* @return $this
*/
public function setTranslatable($translatable);
/**
* Returns the custom storage indexes for the field data storage.
*
* @return array
* An array of custom indexes.
*/
public function getIndexes();
/**
* Sets the custom storage indexes for the field data storage..
*
* @param array $indexes
* The array of custom indexes.
*
* @return $this
*/
public function setIndexes(array $indexes);
}

View file

@ -0,0 +1,175 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldStorageConfigStorage.
*/
namespace Drupal\field;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\State\StateInterface;
/**
* Controller class for "field storage" configuration entities.
*/
class FieldStorageConfigStorage extends ConfigEntityStorage {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The state keyvalue collection.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a FieldStorageConfigStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
* @param \Drupal\Component\Plugin\PluginManagerInterface\FieldTypePluginManagerInterface
* The field type plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, StateInterface $state, FieldTypePluginManagerInterface $field_type_manager) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
$this->entityManager = $entity_manager;
$this->moduleHandler = $module_handler;
$this->state = $state;
$this->fieldTypeManager = $field_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('entity.manager'),
$container->get('module_handler'),
$container->get('state'),
$container->get('plugin.manager.field.field_type')
);
}
/**
* {@inheritdoc}
*/
public function loadByProperties(array $conditions = array()) {
// Include deleted fields if specified in the $conditions parameters.
$include_deleted = isset($conditions['include_deleted']) ? $conditions['include_deleted'] : FALSE;
unset($conditions['include_deleted']);
/** @var \Drupal\field\FieldStorageConfigInterface[] $storages */
$storages = array();
// Get field storages living in configuration. If we are explicitly looking
// for deleted storages only, this can be skipped, because they will be
// retrieved from state below.
if (empty($conditions['deleted'])) {
if (isset($conditions['entity_type']) && isset($conditions['field_name'])) {
// Optimize for the most frequent case where we do have a specific ID.
$id = $conditions['entity_type'] . $conditions['field_name'];
$storages = $this->loadMultiple(array($id));
}
else {
// No specific ID, we need to examine all existing storages.
$storages = $this->loadMultiple();
}
}
// Merge deleted field storages (living in state) if needed.
if ($include_deleted || !empty($conditions['deleted'])) {
$deleted_storages = $this->state->get('field.storage.deleted') ?: array();
foreach ($deleted_storages as $id => $config) {
$storages[$id] = $this->create($config);
}
}
// Collect matching fields.
$matches = array();
foreach ($storages as $field) {
foreach ($conditions as $key => $value) {
// Extract the actual value against which the condition is checked.
$checked_value = $field->get($key);
// Skip to the next field as soon as one condition does not match.
if ($checked_value != $value) {
continue 2;
}
}
// When returning deleted fields, key the results by UUID since they can
// include several fields with the same ID.
$key = $include_deleted ? $field->uuid() : $field->id();
$matches[$key] = $field;
}
return $matches;
}
/**
* {@inheritdoc}
*/
protected function mapFromStorageRecords(array $records) {
foreach ($records as &$record) {
$class = $this->fieldTypeManager->getPluginClass($record['type']);
$record['settings'] = $class::storageSettingsFromConfigData($record['settings']);
}
return parent::mapFromStorageRecords($records);
}
/**
* {@inheritdoc}
*/
protected function mapToStorageRecord(EntityInterface $entity) {
$record = parent::mapToStorageRecord($entity);
$class = $this->fieldTypeManager->getPluginClass($record['type']);
$record['settings'] = $class::storageSettingsToConfigData($record['settings']);
return $record;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldStorageConfigUpdateForbiddenException.
*/
namespace Drupal\field;
use Drupal\Core\Field\FieldException;
/**
* Exception class thrown by hook_field_storage_config_update_forbid().
*/
class FieldStorageConfigUpdateForbiddenException extends FieldException {}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\field\FieldUninstallValidator.
*/
namespace Drupal\field;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Prevents uninstallation of modules providing active field storage.
*/
class FieldUninstallValidator implements ModuleUninstallValidatorInterface {
use StringTranslationTrait;
/**
* The field storage config storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $fieldStorageConfigStorage;
/**
* Constructs a new FieldUninstallValidator.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
$this->fieldStorageConfigStorage = $entity_manager->getStorage('field_storage_config');
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function validate($module) {
$reasons = [];
if ($field_storages = $this->getFieldStoragesByModule($module)) {
// Provide an explanation message (only mention pending deletions if there
// remain no actual, non-deleted fields.)
$non_deleted = FALSE;
foreach ($field_storages as $field_storage) {
if (!$field_storage->isDeleted()) {
$non_deleted = TRUE;
break;
}
}
if ($non_deleted) {
$reasons[] = $this->t('Fields type(s) in use');
}
else {
$reasons[] = $this->t('Fields pending deletion');
}
}
return $reasons;
}
/**
* Returns all field storages for a specified module.
*
* @param string $module
* The module to filter field storages by.
*
* @return \Drupal\field\FieldStorageConfigInterface[]
* An array of field storages for a specified module.
*/
protected function getFieldStoragesByModule($module) {
return $this->fieldStorageConfigStorage->loadByProperties(['module' => $module, 'include_deleted' => TRUE]);
}
}

View file

@ -0,0 +1,185 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Boolean\BooleanFieldTest.
*/
namespace Drupal\field\Tests\Boolean;
use Drupal\Component\Utility\Unicode;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\simpletest\WebTestBase;
/**
* Tests boolean field functionality.
*
* @group field
*/
class BooleanFieldTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test', 'field_ui', 'options');
/**
* A field to use in this test class.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* The field used in this test class.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(array(
'view test entity',
'administer entity_test content',
'administer entity_test form display',
'administer entity_test fields',
)));
}
/**
* Tests boolean field.
*/
function testBooleanField() {
$on = $this->randomMachineName();
$off = $this->randomMachineName();
$label = $this->randomMachineName();
// Create a field with settings to validate.
$field_name = Unicode::strtolower($this->randomMachineName());
$this->fieldStorage = FieldStorageConfig::create(array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'boolean',
));
$this->fieldStorage->save();
$this->field = FieldConfig::create(array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'label' => $label,
'required' => TRUE,
'settings' => array(
'on_label' => $on,
'off_label' => $off,
),
));
$this->field->save();
// Create a form display for the default form mode.
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'boolean_checkbox',
))
->save();
// Create a display for the full view mode.
entity_get_display('entity_test', 'entity_test', 'full')
->setComponent($field_name, array(
'type' => 'boolean',
))
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[value]", '', 'Widget found.');
$this->assertRaw($on);
// Submit and ensure it is accepted.
$edit = array(
"{$field_name}[value]" => 1,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
// Verify that boolean value is displayed.
$entity = entity_load('entity_test', $id);
$display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$this->setRawContent(\Drupal::service('renderer')->renderRoot($content));
$this->assertRaw('<div class="field-item">' . $on . '</div>');
// Test if we can change the on label.
$on = $this->randomMachineName();
$edit = array(
'settings[on_label]' => $on,
);
$this->drupalPostForm('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name, $edit, t('Save settings'));
// Check if we see the updated labels in the creation form.
$this->drupalGet('entity_test/add');
$this->assertRaw($on);
// Test the display_label option.
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'boolean_checkbox',
'settings' => array(
'display_label' => TRUE,
)
))
->save();
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[value]", '', 'Widget found.');
$this->assertNoRaw($on);
$this->assertText($this->field->label());
// Go to the form display page and check if the default settings works as
// expected.
$fieldEditUrl = 'entity_test/structure/entity_test/form-display';
$this->drupalGet($fieldEditUrl);
// Click on the widget settings button to open the widget settings form.
$this->drupalPostAjaxForm(NULL, array(), $field_name . "_settings_edit");
$this->assertText(
'Use field label instead of the "On label" as label',
t('Display setting checkbox available.')
);
// Enable setting.
$edit = array('fields[' . $field_name . '][settings_edit_form][settings][display_label]' => 1);
$this->drupalPostAjaxForm(NULL, $edit, $field_name . "_plugin_settings_update");
$this->drupalPostForm(NULL, NULL, 'Save');
// Go again to the form display page and check if the setting
// is stored and has the expected effect.
$this->drupalGet($fieldEditUrl);
$this->assertText('Use field label: Yes', 'Checking the display settings checkbox updated the value.');
$this->drupalPostAjaxForm(NULL, array(), $field_name . "_settings_edit");
$this->assertText(
'Use field label instead of the "On label" as label',
t('Display setting checkbox is available')
);
$this->assertFieldByXPath(
'*//input[starts-with(@id, "edit-fields-' . $field_name . '-settings-edit-form-settings-display-label") and @value="1"]',
TRUE,
t('Display label changes label of the checkbox')
);
// Test the boolean field settings.
$this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name);
$this->assertFieldById('edit-settings-on-label', $on);
$this->assertFieldById('edit-settings-off-label', $off);
}
}

View file

@ -0,0 +1,132 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Boolean\BooleanFormatterSettingsTest.
*/
namespace Drupal\field\Tests\Boolean;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\WebTestBase;
/**
* Tests the Boolean field formatter settings.
*
* @group field
*/
class BooleanFormatterSettingsTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['field', 'field_ui', 'text', 'node', 'user'];
/**
* The name of the entity bundle that is created in the test.
*
* @var string
*/
protected $bundle;
/**
* The name of the Boolean field to use for testing.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a content type. Use Node because it has Field UI pages that work.
$type_name = Unicode::strtolower($this->randomMachineName(8)) . '_test';
$type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name));
$this->bundle = $type->id();
$admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer node fields', 'administer node display', 'bypass node access', 'administer nodes'));
$this->drupalLogin($admin_user);
$this->fieldName = Unicode::strtolower($this->randomMachineName(8));
$field_storage = FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => 'boolean',
]);
$field_storage->save();
$instance = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $this->bundle,
'label' => $this->randomMachineName(),
]);
$instance->save();
$display = entity_get_display('node', $this->bundle, 'default')
->setComponent($this->fieldName, [
'type' => 'boolean',
'settings' => [],
]);
$display->save();
}
/**
* Tests the formatter settings page for the Boolean formatter.
*/
function testBooleanFormatterSettings() {
// List the options we expect to see on the settings form. Omit the one
// with the Unicode check/x characters, which does not appear to work
// well in WebTestBase.
$options = array(
'Yes / No',
'True / False',
'On / Off',
'Enabled / Disabled',
'1 / 0',
'Custom',
);
// Define what the "default" option should look like, depending on the
// field settings.
$default = 'Field settings (@on / @off)';
// For several different values of the field settings, test that the
// options, including default, are shown correctly.
$settings = array(
array('Yes', 'No'),
array('On', 'Off'),
array('TRUE', 'FALSE'),
);
foreach ($settings as $values) {
// Set up the field settings.
$this->drupalGet('admin/structure/types/manage/' . $this->bundle . '/fields/node.' . $this->bundle . '.' . $this->fieldName);
$this->drupalPostForm(NULL, array(
'settings[on_label]' => $values[0],
'settings[off_label]' => $values[1],
), 'Save settings');
// Open the Manage Display page and trigger the field settings form.
$this->drupalGet('admin/structure/types/manage/' . $this->bundle . '/display');
$this->drupalPostAjaxForm(NULL, array(), $this->fieldName . '_settings_edit');
// Test that the settings options are present in the correct format.
foreach ($options as $string) {
$this->assertText($string);
}
$this->assertText(SafeMarkup::format($default, array('@on' => $values[0], '@off' => $values[1])));
}
}
}

View file

@ -0,0 +1,144 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Boolean\BooleanFormatterTest.
*/
namespace Drupal\field\Tests\Boolean;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the boolean formatter.
*
* @group field
*/
class BooleanFormatterTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['field', 'text', 'entity_test', 'user'];
/**
* @var string
*/
protected $entityType;
/**
* @var string
*/
protected $bundle;
/**
* @var string
*/
protected $fieldName;
/**
* @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface
*/
protected $display;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['field']);
$this->installEntitySchema('entity_test');
$this->entityType = 'entity_test';
$this->bundle = $this->entityType;
$this->fieldName = Unicode::strtolower($this->randomMachineName());
$field_storage = FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => $this->entityType,
'type' => 'boolean',
]);
$field_storage->save();
$instance = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $this->bundle,
'label' => $this->randomMachineName(),
]);
$instance->save();
$this->display = entity_get_display($this->entityType, $this->bundle, 'default')
->setComponent($this->fieldName, [
'type' => 'boolean',
'settings' => [],
]);
$this->display->save();
}
/**
* Renders fields of a given entity with a given display.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object with attached fields to render.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The display to render the fields in.
*
* @return string
* The rendered entity fields.
*/
protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) {
$content = $display->build($entity);
$content = $this->render($content);
return $content;
}
/**
* Tests boolean formatter output.
*/
public function testBooleanFormatter() {
$data = [];
$data[] = [0, [], 'Off'];
$data[] = [1, [], 'On'];
$format = ['format' => 'enabled-disabled'];
$data[] = [0, $format, 'Disabled'];
$data[] = [1, $format, 'Enabled'];
$format = ['format' => 'unicode-yes-no'];
$data[] = [1, $format, '✔'];
$data[] = [0, $format, '✖'];
$format = [
'format' => 'custom',
'format_custom_false' => 'FALSE',
'format_custom_true' => 'TRUE'
];
$data[] = [0, $format, 'FALSE'];
$data[] = [1, $format, 'TRUE'];
foreach ($data as $test_data) {
list($value, $settings, $expected) = $test_data;
$component = $this->display->getComponent($this->fieldName);
$component['settings'] = $settings;
$this->display->setComponent($this->fieldName, $component);
$entity = EntityTest::create([]);
$entity->{$this->fieldName}->value = $value;
// Verify that all HTML is escaped and newlines are retained.
$this->renderEntityFields($entity, $this->display);
$this->assertRaw($expected);
}
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Boolean\BooleanItemTest.
*/
namespace Drupal\field\Tests\Boolean;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\field\Tests\FieldUnitTestBase;
/**
* Tests the new entity API for the boolean field type.
*
* @group field
*/
class BooleanItemTest extends FieldUnitTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a boolean field and storage for validation.
entity_create('field_storage_config', array(
'field_name' => 'field_boolean',
'entity_type' => 'entity_test',
'type' => 'boolean',
))->save();
entity_create('field_config', array(
'entity_type' => 'entity_test',
'field_name' => 'field_boolean',
'bundle' => 'entity_test',
))->save();
// Create a form display for the default form mode.
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent('field_boolean', array(
'type' => 'boolean_checkbox',
))
->save();
}
/**
* Tests using entity fields of the boolean field type.
*/
public function testBooleanItem() {
// Verify entity creation.
$entity = entity_create('entity_test');
$value = '1';
$entity->field_boolean = $value;
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->field_boolean instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_boolean[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_boolean->value, $value);
$this->assertEqual($entity->field_boolean[0]->value, $value);
// Verify changing the boolean value.
$new_value = 0;
$entity->field_boolean->value = $new_value;
$this->assertEqual($entity->field_boolean->value, $new_value);
// Read changed entity and assert changed values.
$entity->save();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->field_boolean->value, $new_value);
// Test sample item generation.
$entity = entity_create('entity_test');
$entity->field_boolean->generateSampleItems();
$this->entityValidateAndSave($entity);
}
}

View file

@ -0,0 +1,361 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\BulkDeleteTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Entity\FieldConfig;
/**
* Bulk delete storages and fields, and clean up afterwards.
*
* @group field
*/
class BulkDeleteTest extends FieldUnitTestBase {
/**
* The fields to use in this test.
*
* @var array
*/
protected $fieldStorages;
/**
* The entities to use in this test.
*
* @var array
*/
protected $entities;
/**
* The entities to use in this test, keyed by bundle.
*
* @var array
*/
protected $entitiesByBundles;
/**
* The bundles for the entities used in this test.
*
* @var array
*/
protected $bundles;
/**
* The entity type to be used in the test classes.
*
* @var string
*/
protected $entityTypeId = 'entity_test';
/**
* Tests that the expected hooks have been invoked on the expected entities.
*
* @param $expected_hooks
* An array keyed by hook name, with one entry per expected invocation.
* Each entry is the value of the "$entity" parameter the hook is expected
* to have been passed.
* @param $actual_hooks
* The array of actual hook invocations recorded by field_test_memorize().
*/
function checkHooksInvocations($expected_hooks, $actual_hooks) {
foreach ($expected_hooks as $hook => $invocations) {
$actual_invocations = $actual_hooks[$hook];
// Check that the number of invocations is correct.
$this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times.");
// Check that the hook was called for each expected argument.
foreach ($invocations as $argument) {
$found = FALSE;
foreach ($actual_invocations as $actual_arguments) {
// The argument we are looking for is either an array of entities as
// the second argument or a single entity object as the first.
if ($argument instanceof EntityInterface && $actual_arguments[0]->id() == $argument->id()) {
$found = TRUE;
break;
}
// In case of an array, compare the array size and make sure it
// contains the same elements.
elseif (is_array($argument) && count($actual_arguments[1]) == count($argument) && count(array_diff_key($actual_arguments[1], $argument)) == 0) {
$found = TRUE;
break;
}
}
$this->assertTrue($found, "$hook() was called on expected argument");
}
}
}
protected function setUp() {
parent::setUp();
$this->fieldStorages = array();
$this->entities = array();
$this->entitiesByBundles = array();
// Create two bundles.
$this->bundles = array('bb_1' => 'bb_1', 'bb_2' => 'bb_2');
foreach ($this->bundles as $name => $desc) {
entity_test_create_bundle($name, $desc);
}
// Create two field storages.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'bf_1',
'entity_type' => $this->entityTypeId,
'type' => 'test_field',
'cardinality' => 1
));
$field_storage->save();
$this->fieldStorages[] = $field_storage;
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'bf_2',
'entity_type' => $this->entityTypeId,
'type' => 'test_field',
'cardinality' => 4
));
$field_storage->save();
$this->fieldStorages[] = $field_storage;
// For each bundle, create each field, and 10 entities with values for the
// fields.
foreach ($this->bundles as $bundle) {
foreach ($this->fieldStorages as $field_storage) {
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => $bundle,
))->save();
}
for ($i = 0; $i < 10; $i++) {
$entity = entity_create($this->entityTypeId, array('type' => $bundle));
foreach ($this->fieldStorages as $field_storage) {
$entity->{$field_storage->getName()}->setValue($this->_generateTestFieldValues($field_storage->getCardinality()));
}
$entity->save();
}
}
$this->entities = entity_load_multiple($this->entityTypeId);
foreach ($this->entities as $entity) {
// This test relies on the entities having stale field definitions
// so that the deleted field can be accessed on them. Access the field
// now, so that they are always loaded.
$entity->bf_1->value;
// Also keep track of the entities per bundle.
$this->entitiesByBundles[$entity->bundle()][$entity->id()] = $entity;
}
}
/**
* Verify that deleting a field leaves the field data items in the database
* and that the appropriate Field API functions can operate on the deleted
* data and field definition.
*
* This tests how EntityFieldQuery interacts with field deletion and could be
* moved to FieldCrudTestCase, but depends on this class's setUp().
*/
function testDeleteField() {
$bundle = reset($this->bundles);
$field_storage = reset($this->fieldStorages);
$field_name = $field_storage->getName();
$factory = \Drupal::service('entity.query');
// There are 10 entities of this bundle.
$found = $factory->get('entity_test')
->condition('type', $bundle)
->execute();
$this->assertEqual(count($found), 10, 'Correct number of entities found before deleting');
// Delete the field.
$field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
$field->delete();
// The field still exists, deleted.
$fields = entity_load_multiple_by_properties('field_config', array('field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE));
$this->assertEqual(count($fields), 1, 'There is one deleted field');
$field = $fields[$field->uuid()];
$this->assertEqual($field->getTargetBundle(), $bundle, 'The deleted field is for the correct bundle');
// Check that the actual stored content did not change during delete.
$storage = \Drupal::entityManager()->getStorage($this->entityTypeId);
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $storage->getTableMapping();
$table = $table_mapping->getDedicatedDataTableName($field_storage);
$column = $table_mapping->getFieldColumnName($field_storage, 'value');
$result = db_select($table, 't')
->fields('t')
->execute();
foreach ($result as $row) {
$this->assertEqual($this->entities[$row->entity_id]->{$field_name}->value, $row->$column);
}
// There are 0 entities of this bundle with non-deleted data.
$found = $factory->get('entity_test')
->condition('type', $bundle)
->condition("$field_name.deleted", 0)
->execute();
$this->assertFalse($found, 'No entities found after deleting');
// There are 10 entities of this bundle when deleted fields are allowed, and
// their values are correct.
$found = $factory->get('entity_test')
->condition('type', $bundle)
->condition("$field_name.deleted", 1)
->sort('id')
->execute();
$this->assertEqual(count($found), 10, 'Correct number of entities found after deleting');
$this->assertFalse(array_diff($found, array_keys($this->entities)));
}
/**
* Verify that field data items and fields are purged when a field storage is
* deleted.
*/
function testPurgeField() {
// Start recording hook invocations.
field_test_memorize();
$bundle = reset($this->bundles);
$field_storage = reset($this->fieldStorages);
$field_name = $field_storage->getName();
// Delete the field.
$field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
$field->delete();
// No field hooks were called.
$mem = field_test_memorize();
$this->assertEqual(count($mem), 0, 'No field hooks were called');
$batch_size = 2;
for ($count = 8; $count >= 0; $count -= $batch_size) {
// Purge two entities.
field_purge_batch($batch_size);
// There are $count deleted entities left.
$found = \Drupal::entityQuery('entity_test')
->condition('type', $bundle)
->condition($field_name . '.deleted', 1)
->execute();
$this->assertEqual(count($found), $count, 'Correct number of entities found after purging 2');
}
// Check hooks invocations.
// FieldItemInterface::delete() should have been called once for each entity in the
// bundle.
$actual_hooks = field_test_memorize();
$hooks = array();
$entities = $this->entitiesByBundles[$bundle];
foreach ($entities as $id => $entity) {
$hooks['field_test_field_delete'][] = $entity;
}
$this->checkHooksInvocations($hooks, $actual_hooks);
// The field still exists, deleted.
$fields = entity_load_multiple_by_properties('field_config', array('field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE));
$this->assertEqual(count($fields), 1, 'There is one deleted field');
// Purge the field.
field_purge_batch($batch_size);
// The field is gone.
$fields = entity_load_multiple_by_properties('field_config', array('field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE));
$this->assertEqual(count($fields), 0, 'The field is gone');
// The field storage still exists, not deleted, because it has a second
// field.
$storages = entity_load_multiple_by_properties('field_storage_config', array('uuid' => $field_storage->uuid(), 'include_deleted' => TRUE));
$this->assertTrue(isset($storages[$field_storage->uuid()]), 'The field storage exists and is not deleted');
}
/**
* Verify that field storages are preserved and purged correctly as multiple
* fields are deleted and purged.
*/
function testPurgeFieldStorage() {
// Start recording hook invocations.
field_test_memorize();
$field_storage = reset($this->fieldStorages);
$field_name = $field_storage->getName();
// Delete the first field.
$bundle = reset($this->bundles);
$field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
$field->delete();
// Assert that FieldItemInterface::delete() was not called yet.
$mem = field_test_memorize();
$this->assertEqual(count($mem), 0, 'No field hooks were called.');
// Purge the data.
field_purge_batch(10);
// Check hooks invocations.
// FieldItemInterface::delete() should have been called once for each entity in the
// bundle.
$actual_hooks = field_test_memorize();
$hooks = array();
$entities = $this->entitiesByBundles[$bundle];
foreach ($entities as $id => $entity) {
$hooks['field_test_field_delete'][] = $entity;
}
$this->checkHooksInvocations($hooks, $actual_hooks);
// The field still exists, deleted.
$fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
$this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted');
// Purge again to purge the field.
field_purge_batch(0);
// The field is gone.
$fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
$this->assertEqual(count($fields), 0, 'The field is purged.');
// The field storage still exists, not deleted.
$storages = entity_load_multiple_by_properties('field_storage_config', array('uuid' => $field_storage->uuid(), 'include_deleted' => TRUE));
$this->assertTrue(isset($storages[$field_storage->uuid()]) && !$storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is not deleted');
// Delete the second field.
$bundle = next($this->bundles);
$field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
$field->delete();
// Assert that FieldItemInterface::delete() was not called yet.
$mem = field_test_memorize();
$this->assertEqual(count($mem), 0, 'No field hooks were called.');
// Purge the data.
field_purge_batch(10);
// Check hooks invocations (same as above, for the 2nd bundle).
$actual_hooks = field_test_memorize();
$hooks = array();
$entities = $this->entitiesByBundles[$bundle];
foreach ($entities as $id => $entity) {
$hooks['field_test_field_delete'][] = $entity;
}
$this->checkHooksInvocations($hooks, $actual_hooks);
// The field and the storage still exist, deleted.
$fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
$this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted');
$storages = entity_load_multiple_by_properties('field_storage_config', array('uuid' => $field_storage->uuid(), 'include_deleted' => TRUE));
$this->assertTrue(isset($storages[$field_storage->uuid()]) && $storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is deleted');
// Purge again to purge the field and the storage.
field_purge_batch(0);
// The field and the storage are gone.
$fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
$this->assertEqual(count($fields), 0, 'The field is purged.');
$storages = entity_load_multiple_by_properties('field_storage_config', array('uuid' => $field_storage->uuid(), 'include_deleted' => TRUE));
$this->assertEqual(count($storages), 0, 'The field storage is purged.');
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\ConfigFieldDefinitionTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Tests exposing field definitions for configurable fields.
*
* @group field
*/
class ConfigFieldDefinitionTest extends FieldUnitTestBase {
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface;
*/
protected $entityManager;
/**
* @var string
*/
private $entityType;
/**
* @var string
*/
private $bundle;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a field and a storage of type 'test_field', on the 'entity_test'
// entity type.
$this->entityType = 'entity_test';
$this->bundle = 'entity_test';
$this->createFieldWithStorage('', $this->entityType, $this->bundle);
$this->entityManager = $this->container->get('entity.manager');
// Create a second field on 'entity_test_rev'.
$this->createFieldWithStorage('_rev', 'entity_test_rev', 'entity_test_rev');
}
/**
* Makes sure a field definition is exposed for a configurable field.
*/
public function testBundleFieldDefinition() {
$definitions = $this->entityManager->getFieldDefinitions($this->entityType, $this->bundle);
$this->assertTrue(isset($definitions[$this->fieldTestData->field->getName()]));
$this->assertTrue($definitions[$this->fieldTestData->field->getName()] instanceof FieldDefinitionInterface);
// Make sure fields on other entity types are not exposed.
$this->assertFalse(isset($definitions[$this->fieldTestData->field_rev->getName()]));
}
/**
* Makes sure a field storage definition is exposed for a configurable field.
*/
public function testFieldStorageDefinition() {
$field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityType);
$this->assertTrue(isset($field_storage_definitions[$this->fieldTestData->field->getName()]));
$this->assertTrue($field_storage_definitions[$this->fieldTestData->field->getName()] instanceof FieldStorageDefinitionInterface);
// Make sure storages on other entity types are not exposed.
$this->assertFalse(isset($field_storage_definitions[$this->fieldTestData->field_rev->getName()]));
}
}

View file

@ -0,0 +1,305 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\DisplayApiTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Entity\Entity\EntityViewMode;
/**
* Tests the field display API.
*
* @group field
*/
class DisplayApiTest extends FieldUnitTestBase {
/**
* The field name to use in this test.
*
* @var string
*/
protected $fieldName;
/**
* The field label to use in this test.
*
* @var string
*/
protected $label;
/**
* The field cardinality to use in this test.
*
* @var number
*/
protected $cardinality;
/**
* The field display options to use in this test.
*
* @var array
*/
protected $displayOptions;
/**
* The test entity.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* An array of random values, in the format expected for field values.
*
* @var array
*/
protected $values;
protected function setUp() {
parent::setUp();
// Create a field and its storage.
$this->fieldName = 'test_field';
$this->label = $this->randomMachineName();
$this->cardinality = 4;
$field_storage = array(
'field_name' => $this->fieldName,
'entity_type' => 'entity_test',
'type' => 'test_field',
'cardinality' => $this->cardinality,
);
$field = array(
'field_name' => $this->fieldName,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'label' => $this->label,
);
$this->displayOptions = array(
'default' => array(
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $this->randomMachineName(),
),
),
'teaser' => array(
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $this->randomMachineName(),
),
),
);
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $field)->save();
// Create a display for the default view mode.
entity_get_display($field['entity_type'], $field['bundle'], 'default')
->setComponent($this->fieldName, $this->displayOptions['default'])
->save();
// Create a display for the teaser view mode.
EntityViewMode::create(array('id' => 'entity_test.teaser', 'targetEntityType' => 'entity_test'))->save();
entity_get_display($field['entity_type'], $field['bundle'], 'teaser')
->setComponent($this->fieldName, $this->displayOptions['teaser'])
->save();
// Create an entity with values.
$this->values = $this->_generateTestFieldValues($this->cardinality);
$this->entity = entity_create('entity_test');
$this->entity->{$this->fieldName}->setValue($this->values);
$this->entity->save();
}
/**
* Tests the FieldItemListInterface::view() method.
*/
function testFieldItemListView() {
$items = $this->entity->get($this->fieldName);
// No display settings: check that default display settings are used.
$build = $items->view();
$this->render($build);
$settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('field_test_default');
$setting = $settings['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Display settings: Check hidden field.
$display = array(
'label' => 'hidden',
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $this->randomMachineName(),
'alter' => TRUE,
),
);
$build = $items->view($display);
$this->render($build);
$setting = $display['settings']['test_formatter_setting_multiple'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertText('field_test_entity_display_build_alter', 'Alter fired, display passed.');
$this->assertText('entity language is en', 'Language is placed onto the context.');
$array = array();
foreach ($this->values as $delta => $value) {
$array[] = $delta . ':' . $value['value'];
}
$this->assertText($setting . '|' . implode('|', $array), 'Values were displayed with expected setting.');
// Display settings: Check visually_hidden field.
$display = array(
'label' => 'visually_hidden',
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $this->randomMachineName(),
'alter' => TRUE,
),
);
$build = $items->view($display);
$this->render($build);
$setting = $display['settings']['test_formatter_setting_multiple'];
$this->assertRaw('visually-hidden', 'Label was visually hidden.');
$this->assertText('field_test_entity_display_build_alter', 'Alter fired, display passed.');
$this->assertText('entity language is en', 'Language is placed onto the context.');
$array = array();
foreach ($this->values as $delta => $value) {
$array[] = $delta . ':' . $value['value'];
}
$this->assertText($setting . '|' . implode('|', $array), 'Values were displayed with expected setting.');
// Check the prepare_view steps are invoked.
$display = array(
'label' => 'hidden',
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $this->randomMachineName(),
),
);
$build = $items->view($display);
$this->render($build);
$setting = $display['settings']['test_formatter_setting_additional'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertNoText('field_test_entity_display_build_alter', 'Alter not fired.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// View mode: check that display settings specified in the display object
// are used.
$build = $items->view('teaser');
$this->render($build);
$setting = $this->displayOptions['teaser']['settings']['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Unknown view mode: check that display settings for 'default' view mode
// are used.
$build = $items->view('unknown_view_mode');
$this->render($build);
$setting = $this->displayOptions['default']['settings']['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
}
/**
* Tests the FieldItemInterface::view() method.
*/
function testFieldItemView() {
// No display settings: check that default display settings are used.
$settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('field_test_default');
$setting = $settings['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->fieldName}[$delta];
$build = $item->view();
$this->render($build);
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Check that explicit display settings are used.
$display = array(
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $this->randomMachineName(),
),
);
$setting = $display['settings']['test_formatter_setting_multiple'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->fieldName}[$delta];
$build = $item->view($display);
$this->render($build);
$this->assertText($setting . '|0:' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Check that prepare_view steps are invoked.
$display = array(
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $this->randomMachineName(),
),
);
$setting = $display['settings']['test_formatter_setting_additional'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->fieldName}[$delta];
$build = $item->view($display);
$this->render($build);
$this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// View mode: check that display settings specified in the field are used.
$setting = $this->displayOptions['teaser']['settings']['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->fieldName}[$delta];
$build = $item->view('teaser');
$this->render($build);
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Unknown view mode: check that display settings for 'default' view mode
// are used.
$setting = $this->displayOptions['default']['settings']['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->fieldName}[$delta];
$build = $item->view('unknown_view_mode');
$this->render($build);
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
}
/**
* Tests that the prepareView() formatter method still fires for empty values.
*/
function testFieldEmpty() {
// Uses \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldEmptyFormatter.
$display = array(
'label' => 'hidden',
'type' => 'field_empty_test',
'settings' => array(
'test_empty_string' => '**EMPTY FIELD**' . $this->randomMachineName(),
),
);
// $this->entity is set by the setUp() method and by default contains 4
// numeric values. We only want to test the display of this one field.
$build = $this->entity->get($this->fieldName)->view($display);
$this->render($build);
// The test field by default contains values, so should not display the
// default "empty" text.
$this->assertNoText($display['settings']['test_empty_string']);
// Now remove the values from the test field and retest.
$this->entity->{$this->fieldName} = array();
$this->entity->save();
$build = $this->entity->get($this->fieldName)->view($display);
$this->render($build);
// This time, as the field values have been removed, we *should* show the
// default "empty" text.
$this->assertText($display['settings']['test_empty_string']);
}
}

View file

@ -0,0 +1,109 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Email\EmailFieldTest.
*/
namespace Drupal\field\Tests\Email;
use Drupal\Component\Utility\Unicode;
use Drupal\simpletest\WebTestBase;
/**
* Tests email field functionality.
*
* @group field
*/
class EmailFieldTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'entity_test', 'field_ui');
/**
* A field storage to use in this test class.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* The field used in this test class.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(array(
'view test entity',
'administer entity_test content',
'administer content types',
)));
}
/**
* Tests email field.
*/
function testEmailField() {
// Create a field with settings to validate.
$field_name = Unicode::strtolower($this->randomMachineName());
$this->fieldStorage = entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'email',
));
$this->fieldStorage->save();
$this->field = entity_create('field_config', array(
'field_storage' => $this->fieldStorage,
'bundle' => 'entity_test',
));
$this->field->save();
// Create a form display for the default form mode.
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'email_default',
'settings' => array(
'placeholder' => 'example@example.com',
),
))
->save();
// Create a display for the full view mode.
entity_get_display('entity_test', 'entity_test', 'full')
->setComponent($field_name, array(
'type' => 'email_mailto',
))
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget found.');
$this->assertRaw('placeholder="example@example.com"');
// Submit a valid email address and ensure it is accepted.
$value = 'test@example.com';
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
$this->assertRaw($value);
// Verify that a mailto link is displayed.
$entity = entity_load('entity_test', $id);
$display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$this->setRawContent(\Drupal::service('renderer')->renderRoot($content));
$this->assertLinkByHref('mailto:test@example.com');
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Email\EmailItemTest.
*/
namespace Drupal\field\Tests\Email;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\field\Tests\FieldUnitTestBase;
/**
* Tests the new entity API for the email field type.
*
* @group field
*/
class EmailItemTest extends FieldUnitTestBase {
protected function setUp() {
parent::setUp();
// Create an email field storage and field for validation.
entity_create('field_storage_config', array(
'field_name' => 'field_email',
'entity_type' => 'entity_test',
'type' => 'email',
))->save();
entity_create('field_config', array(
'entity_type' => 'entity_test',
'field_name' => 'field_email',
'bundle' => 'entity_test',
))->save();
// Create a form display for the default form mode.
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent('field_email', array(
'type' => 'email_default',
))
->save();
}
/**
* Tests using entity fields of the email field type.
*/
public function testEmailItem() {
// Verify entity creation.
$entity = entity_create('entity_test');
$value = 'test@example.com';
$entity->field_email = $value;
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->field_email instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_email[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_email->value, $value);
$this->assertEqual($entity->field_email[0]->value, $value);
// Verify changing the email value.
$new_value = $this->randomMachineName();
$entity->field_email->value = $new_value;
$this->assertEqual($entity->field_email->value, $new_value);
// Read changed entity and assert changed values.
$entity->save();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->field_email->value, $new_value);
// Test sample item generation.
$entity = entity_create('entity_test');
$entity->field_email->generateSampleItems();
$this->entityValidateAndSave($entity);
}
}

View file

@ -0,0 +1,312 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\EntityReference\EntityReferenceFormatterTest.
*/
namespace Drupal\field\Tests\EntityReference;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\entity_reference\Tests\EntityReferenceTestTrait;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the formatters functionality.
*
* @group entity_reference
*/
class EntityReferenceFormatterTest extends EntityUnitTestBase {
use EntityReferenceTestTrait;
/**
* The entity type used in this test.
*
* @var string
*/
protected $entityType = 'entity_test';
/**
* The bundle used in this test.
*
* @var string
*/
protected $bundle = 'entity_test';
/**
* The name of the field used in this test.
*
* @var string
*/
protected $fieldName = 'field_test';
/**
* The entity to be referenced in this test.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $referencedEntity;
/**
* The entity that is not yet saved to its persistent storage to be referenced
* in this test.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $unsavedReferencedEntity;
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('entity_reference');
protected function setUp() {
parent::setUp();
// Grant the 'view test entity' permission.
$this->installConfig(array('user'));
Role::load(RoleInterface::ANONYMOUS_ID)
->grantPermission('view test entity')
->save();
// The label formatter rendering generates links, so build the router.
$this->installSchema('system', 'router');
$this->container->get('router.builder')->rebuild();
$this->createEntityReferenceField($this->entityType, $this->bundle, $this->fieldName, 'Field test', $this->entityType, 'default', array(), FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
// Set up a field, so that the entity that'll be referenced bubbles up a
// cache tag when rendering it entirely.
entity_create('field_storage_config', array(
'field_name' => 'body',
'entity_type' => $this->entityType,
'type' => 'text',
'settings' => array(),
))->save();
entity_create('field_config', array(
'entity_type' => $this->entityType,
'bundle' => $this->bundle,
'field_name' => 'body',
'label' => 'Body',
))->save();
entity_get_display($this->entityType, $this->bundle, 'default')
->setComponent('body', array(
'type' => 'text_default',
'settings' => array(),
))
->save();
entity_create('filter_format', array(
'format' => 'full_html',
'name' => 'Full HTML',
))->save();
// Create the entity to be referenced.
$this->referencedEntity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$this->referencedEntity->body = array(
'value' => '<p>Hello, world!</p>',
'format' => 'full_html',
);
$this->referencedEntity->save();
// Create another entity to be referenced but do not save it.
$this->unsavedReferencedEntity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$this->unsavedReferencedEntity->body = array(
'value' => '<p>Hello, unsaved world!</p>',
'format' => 'full_html',
);
}
/**
* Assert inaccessible items don't change the data of the fields.
*/
public function testAccess() {
// Revoke the 'view test entity' permission for this test.
Role::load(RoleInterface::ANONYMOUS_ID)
->revokePermission('view test entity')
->save();
$field_name = $this->fieldName;
$referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$referencing_entity->save();
$referencing_entity->{$field_name}->entity = $this->referencedEntity;
// Assert user doesn't have access to the entity.
$this->assertFalse($this->referencedEntity->access('view'), 'Current user does not have access to view the referenced entity.');
$formatter_manager = $this->container->get('plugin.manager.field.formatter');
// Get all the existing formatters.
foreach ($formatter_manager->getOptions('entity_reference') as $formatter => $name) {
// Set formatter type for the 'full' view mode.
entity_get_display($this->entityType, $this->bundle, 'default')
->setComponent($field_name, array(
'type' => $formatter,
))
->save();
// Invoke entity view.
entity_view($referencing_entity, 'default');
// Verify the un-accessible item still exists.
$this->assertEqual($referencing_entity->{$field_name}->target_id, $this->referencedEntity->id(), format_string('The un-accessible item still exists after @name formatter was executed.', array('@name' => $name)));
}
}
/**
* Tests the ID formatter.
*/
public function testIdFormatter() {
$formatter = 'entity_reference_entity_id';
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter);
$this->assertEqual($build[0]['#markup'], $this->referencedEntity->id(), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$this->assertEqual($build[0]['#cache']['tags'], $this->referencedEntity->getCacheTags(), sprintf('The %s formatter has the expected cache tags.', $formatter));
$this->assertTrue(!isset($build[1]), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
}
/**
* Tests the entity formatter.
*/
public function testEntityFormatter() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
$formatter = 'entity_reference_entity_view';
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter);
// Test the first field item.
$expected_rendered_name_field_1 = '<div class="field field-entity-test--name field-name-name field-type-string field-label-hidden">
<div class="field-items">
<div class="field-item">' . $this->referencedEntity->label() . '</div>
</div>
</div>
';
$expected_rendered_body_field_1 = '<div class="field field-entity-test--body field-name-body field-type-text field-label-above">
<div class="field-label">Body</div>
<div class="field-items">
<div class="field-item"><p>Hello, world!</p></div>
</div>
</div>
';
$renderer->renderRoot($build[0]);
$this->assertEqual($build[0]['#markup'], 'default | ' . $this->referencedEntity->label() . $expected_rendered_name_field_1 . $expected_rendered_body_field_1, sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$expected_cache_tags = Cache::mergeTags(
\Drupal::entityManager()->getViewBuilder($this->entityType)->getCacheTags(),
$this->referencedEntity->getCacheTags(),
FilterFormat::load('full_html')->getCacheTags()
);
$this->assertEqual($build[0]['#cache']['tags'], $expected_cache_tags, format_string('The @formatter formatter has the expected cache tags.', array('@formatter' => $formatter)));
// Test the second field item.
$renderer->renderRoot($build[1]);
$this->assertEqual($build[1]['#markup'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
}
/**
* Tests the label formatter.
*/
public function testLabelFormatter() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
$formatter = 'entity_reference_label';
// The 'link' settings is TRUE by default.
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter);
$expected_field_cacheability = [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
];
$this->assertEqual($build['#cache'], $expected_field_cacheability, 'The field render array contains the entity access cacheability metadata');
$expected_item_1 = array(
'#type' => 'link',
'#title' => $this->referencedEntity->label(),
'#url' => $this->referencedEntity->urlInfo(),
'#options' => $this->referencedEntity->urlInfo()->getOptions(),
'#cache' => array(
'contexts' => [
'user.permissions',
],
'tags' => $this->referencedEntity->getCacheTags(),
),
);
$this->assertEqual($renderer->renderRoot($build[0]), $renderer->renderRoot($expected_item_1), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$this->assertEqual(CacheableMetadata::createFromRenderArray($build[0]), CacheableMetadata::createFromRenderArray($expected_item_1));
// The second referenced entity is "autocreated", therefore not saved and
// lacking any URL info.
$expected_item_2 = array(
'#markup' => $this->unsavedReferencedEntity->label(),
'#cache' => array(
'contexts' => [
'user.permissions',
],
'tags' => $this->unsavedReferencedEntity->getCacheTags(),
'max-age' => Cache::PERMANENT,
),
);
$this->assertEqual($build[1], $expected_item_2, sprintf('The render array returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
// Test with the 'link' setting set to FALSE.
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter, array('link' => FALSE));
$this->assertEqual($build[0]['#markup'], $this->referencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$this->assertEqual($build[1]['#markup'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
// Test an entity type that doesn't have any link templates, which means
// \Drupal\Core\Entity\EntityInterface::urlInfo() will throw an exception
// and the label formatter will output only the label instead of a link.
$field_storage_config = FieldStorageConfig::loadByName($this->entityType, $this->fieldName);
$field_storage_config->setSetting('target_type', 'entity_test_label');
$field_storage_config->save();
$referenced_entity_with_no_link_template = entity_create('entity_test_label', array(
'name' => $this->randomMachineName(),
));
$referenced_entity_with_no_link_template->save();
$build = $this->buildRenderArray([$referenced_entity_with_no_link_template], $formatter, array('link' => TRUE));
$this->assertEqual($build[0]['#markup'], $referenced_entity_with_no_link_template->label(), sprintf('The markup returned by the %s formatter is correct for an entity type with no valid link template.', $formatter));
}
/**
* Sets field values and returns a render array as built by
* \Drupal\Core\Field\FieldItemListInterface::view().
*
* @param \Drupal\Core\Entity\EntityInterface[] $referenced_entities
* An array of entity objects that will be referenced.
* @param string $formatter
* The formatted plugin that will be used for building the render array.
* @param array $formatter_options
* Settings specific to the formatter. Defaults to the formatter's default
* settings.
*
* @return array
* A render array.
*/
protected function buildRenderArray(array $referenced_entities, $formatter, $formatter_options = array()) {
// Create the entity that will have the entity reference field.
$referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$items = $referencing_entity->get($this->fieldName);
// Assign the referenced entities.
foreach ($referenced_entities as $referenced_entity) {
$items[] = ['entity' => $referenced_entity];
}
// Build the renderable array for the field.
return $items->view(array('type' => $formatter, 'settings' => $formatter_options));
}
}

View file

@ -0,0 +1,283 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\EntityReference\EntityReferenceItemTest.
*/
namespace Drupal\field\Tests\EntityReference;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_reference\Tests\EntityReferenceTestTrait;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\entity_test\Entity\EntityTestStringId;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Tests\FieldUnitTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests the new entity API for the entity reference field type.
*
* @group entity_reference
*/
class EntityReferenceItemTest extends FieldUnitTestBase {
use EntityReferenceTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('entity_reference', 'taxonomy', 'text', 'filter', 'views');
/**
* The taxonomy vocabulary to test with.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
protected $vocabulary;
/**
* The taxonomy term to test with.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $term;
/**
* The test entity with a string ID.
*
* @var \Drupal\entity_test\Entity\EntityTestStringId
*/
protected $entityStringId;
/**
* Sets up the test.
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_string_id');
$this->installEntitySchema('taxonomy_term');
$this->vocabulary = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomMachineName(),
'vid' => Unicode::strtolower($this->randomMachineName()),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
));
$this->vocabulary->save();
$this->term = entity_create('taxonomy_term', array(
'name' => $this->randomMachineName(),
'vid' => $this->vocabulary->id(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
));
$this->term->save();
$this->entityStringId = EntityTestStringId::create([
'id' => $this->randomMachineName(),
]);
$this->entityStringId->save();
// Use the util to create an instance.
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_term', 'Test content entity reference', 'taxonomy_term');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_entity_test_string_id', 'Test content entity reference with string ID', 'entity_test_string_id');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_vocabulary', 'Test config entity reference', 'taxonomy_vocabulary');
}
/**
* Tests the entity reference field type for referencing content entities.
*/
public function testContentEntityReferenceItem() {
$tid = $this->term->id();
// Just being able to create the entity like this verifies a lot of code.
$entity = entity_create('entity_test');
$entity->field_test_taxonomy_term->target_id = $tid;
$entity->name->value = $this->randomMachineName();
$entity->save();
$entity = entity_load('entity_test', $entity->id());
$this->assertTrue($entity->field_test_taxonomy_term instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_test_taxonomy_term[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_test_taxonomy_term->target_id, $tid);
$this->assertEqual($entity->field_test_taxonomy_term->entity->getName(), $this->term->getName());
$this->assertEqual($entity->field_test_taxonomy_term->entity->id(), $tid);
$this->assertEqual($entity->field_test_taxonomy_term->entity->uuid(), $this->term->uuid());
// Change the name of the term via the reference.
$new_name = $this->randomMachineName();
$entity->field_test_taxonomy_term->entity->setName($new_name);
$entity->field_test_taxonomy_term->entity->save();
// Verify it is the correct name.
$term = Term::load($tid);
$this->assertEqual($term->getName(), $new_name);
// Make sure the computed term reflects updates to the term id.
$term2 = entity_create('taxonomy_term', array(
'name' => $this->randomMachineName(),
'vid' => $this->term->bundle(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
));
$term2->save();
// Test all the possible ways of assigning a value.
$entity->field_test_taxonomy_term->target_id = $term->id();
$this->assertEqual($entity->field_test_taxonomy_term->entity->id(), $term->id());
$this->assertEqual($entity->field_test_taxonomy_term->entity->getName(), $term->getName());
$entity->field_test_taxonomy_term = [['target_id' => $term2->id()]];
$this->assertEqual($entity->field_test_taxonomy_term->entity->id(), $term2->id());
$this->assertEqual($entity->field_test_taxonomy_term->entity->getName(), $term2->getName());
// Test value assignment via the computed 'entity' property.
$entity->field_test_taxonomy_term->entity = $term;
$this->assertEqual($entity->field_test_taxonomy_term->target_id, $term->id());
$this->assertEqual($entity->field_test_taxonomy_term->entity->getName(), $term->getName());
$entity->field_test_taxonomy_term = [['entity' => $term2]];
$this->assertEqual($entity->field_test_taxonomy_term->target_id, $term2->id());
$this->assertEqual($entity->field_test_taxonomy_term->entity->getName(), $term2->getName());
// Test assigning an invalid item throws an exception.
try {
$entity->field_test_taxonomy_term = ['target_id' => 'invalid', 'entity' => $term2];
$this->fail('Assigning an invalid item throws an exception.');
}
catch (\InvalidArgumentException $e) {
$this->pass('Assigning an invalid item throws an exception.');
}
// Delete terms so we have nothing to reference and try again
$term->delete();
$term2->delete();
$entity = entity_create('entity_test', array('name' => $this->randomMachineName()));
$entity->save();
// Test the generateSampleValue() method.
$entity = entity_create('entity_test');
$entity->field_test_taxonomy_term->generateSampleItems();
$entity->field_test_taxonomy_vocabulary->generateSampleItems();
$this->entityValidateAndSave($entity);
}
/**
* Tests referencing content entities with string IDs.
*/
public function testContentEntityReferenceItemWithStringId() {
$entity = EntityTest::create();
$entity->field_test_entity_test_string_id->target_id = $this->entityStringId->id();
$entity->save();
$storage = \Drupal::entityManager()->getStorage('entity_test');
$storage->resetCache();
$this->assertEqual($this->entityStringId->id(), $storage->load($entity->id())->field_test_entity_test_string_id->target_id);
}
/**
* Tests the entity reference field type for referencing config entities.
*/
public function testConfigEntityReferenceItem() {
$referenced_entity_id = $this->vocabulary->id();
// Just being able to create the entity like this verifies a lot of code.
$entity = entity_create('entity_test');
$entity->field_test_taxonomy_vocabulary->target_id = $referenced_entity_id;
$entity->name->value = $this->randomMachineName();
$entity->save();
$entity = entity_load('entity_test', $entity->id());
$this->assertTrue($entity->field_test_taxonomy_vocabulary instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_test_taxonomy_vocabulary[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_test_taxonomy_vocabulary->target_id, $referenced_entity_id);
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->label(), $this->vocabulary->label());
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->id(), $referenced_entity_id);
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->uuid(), $this->vocabulary->uuid());
// Change the name of the term via the reference.
$new_name = $this->randomMachineName();
$entity->field_test_taxonomy_vocabulary->entity->set('name', $new_name);
$entity->field_test_taxonomy_vocabulary->entity->save();
// Verify it is the correct name.
$vocabulary = Vocabulary::load($referenced_entity_id);
$this->assertEqual($vocabulary->label(), $new_name);
// Make sure the computed term reflects updates to the term id.
$vocabulary2 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomMachineName(),
'vid' => Unicode::strtolower($this->randomMachineName()),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
));
$vocabulary2->save();
$entity->field_test_taxonomy_vocabulary->target_id = $vocabulary2->id();
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->id(), $vocabulary2->id());
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->label(), $vocabulary2->label());
// Delete terms so we have nothing to reference and try again
$this->vocabulary->delete();
$vocabulary2->delete();
$entity = entity_create('entity_test', array('name' => $this->randomMachineName()));
$entity->save();
}
/**
* Test saving order sequence doesn't matter.
*/
public function testEntitySaveOrder() {
// The term entity is unsaved here.
$term = entity_create('taxonomy_term', array(
'name' => $this->randomMachineName(),
'vid' => $this->term->bundle(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
));
$entity = entity_create('entity_test');
// Now assign the unsaved term to the field.
$entity->field_test_taxonomy_term->entity = $term;
$entity->name->value = $this->randomMachineName();
// Now save the term.
$term->save();
// And then the entity.
$entity->save();
$this->assertEqual($entity->field_test_taxonomy_term->entity->id(), $term->id());
}
/**
* Tests that the 'handler' field setting stores the proper plugin ID.
*/
public function testSelectionHandlerSettings() {
$field_name = Unicode::strtolower($this->randomMachineName());
$field_storage = FieldStorageConfig::create(array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'entity_reference',
'settings' => array(
'target_type' => 'entity_test'
),
));
$field_storage->save();
// Do not specify any value for the 'handler' setting in order to verify
// that the default value is properly used.
$field = FieldConfig::create(array(
'field_storage' => $field_storage,
'bundle' => 'entity_test',
));
$field->save();
$field = FieldConfig::load($field->id());
$this->assertTrue($field->getSetting('handler') == 'default:entity_test');
$field->setSetting('handler', 'views');
$field->save();
$field = FieldConfig::load($field->id());
$this->assertTrue($field->getSetting('handler') == 'views');
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldAccessTest.
*/
namespace Drupal\field\Tests;
/**
* Tests Field access.
*
* @group field
*/
class FieldAccessTest extends FieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'field_test');
/**
* Node entity to use in this test.
*
* @var \Drupal\node\Entity\Node
*/
protected $node;
/**
* Field value to test display on nodes.
*
* @var string
*/
protected $testViewFieldValue;
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(array('view test_view_field content'));
$this->drupalLogin($web_user);
// Create content type.
$content_type_info = $this->drupalCreateContentType();
$content_type = $content_type_info->id();
$field_storage = array(
'field_name' => 'test_view_field',
'entity_type' => 'node',
'type' => 'text',
);
entity_create('field_storage_config', $field_storage)->save();
$field = array(
'field_name' => $field_storage['field_name'],
'entity_type' => 'node',
'bundle' => $content_type,
);
entity_create('field_config', $field)->save();
// Assign display properties for the 'default' and 'teaser' view modes.
foreach (array('default', 'teaser') as $view_mode) {
entity_get_display('node', $content_type, $view_mode)
->setComponent($field_storage['field_name'])
->save();
}
// Create test node.
$this->testViewFieldValue = 'This is some text';
$settings = array();
$settings['type'] = $content_type;
$settings['title'] = 'Field view access test';
$settings['test_view_field'] = array(array('value' => $this->testViewFieldValue));
$this->node = $this->drupalCreateNode($settings);
}
/**
* Test that hook_entity_field_access() is called.
*/
function testFieldAccess() {
// Assert the text is visible.
$this->drupalGet('node/' . $this->node->id());
$this->assertText($this->testViewFieldValue);
// Assert the text is not visible for anonymous users.
// The field_test module implements hook_entity_field_access() which will
// specifically target the 'test_view_field' field.
$this->drupalLogout();
$this->drupalGet('node/' . $this->node->id());
$this->assertNoText($this->testViewFieldValue);
}
}

View file

@ -0,0 +1,380 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldAttachOtherTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Form\FormState;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests other Field API functions.
*
* @group field
* @todo move this to the Entity module
*/
class FieldAttachOtherTest extends FieldUnitTestBase {
protected function setUp() {
parent::setUp();
$this->container->get('router.builder')->rebuild();
$this->installEntitySchema('entity_test_rev');
$this->createFieldWithStorage();
}
/**
* Test rendering fields with EntityDisplay build().
*/
function testEntityDisplayBuild() {
$this->createFieldWithStorage('_2');
$entity_type = 'entity_test';
$entity_init = entity_create($entity_type);
// Populate values to be displayed.
$values = $this->_generateTestFieldValues($this->fieldTestData->field_storage->getCardinality());
$entity_init->{$this->fieldTestData->field_name}->setValue($values);
$values_2 = $this->_generateTestFieldValues($this->fieldTestData->field_storage_2->getCardinality());
$entity_init->{$this->fieldTestData->field_name_2}->setValue($values_2);
// Simple formatter, label displayed.
$entity = clone($entity_init);
$display = entity_get_display($entity_type, $entity->bundle(), 'full');
$formatter_setting = $this->randomMachineName();
$display_options = array(
'label' => 'above',
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $formatter_setting,
),
);
$display->setComponent($this->fieldTestData->field_name, $display_options);
$formatter_setting_2 = $this->randomMachineName();
$display_options_2 = array(
'label' => 'above',
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $formatter_setting_2,
),
);
$display->setComponent($this->fieldTestData->field_name_2, $display_options_2);
// View all fields.
$content = $display->build($entity);
$this->render($content);
$this->assertRaw($this->fieldTestData->field->getLabel(), "First field's label is displayed.");
foreach ($values as $delta => $value) {
$this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
}
$this->assertRaw($this->fieldTestData->field_2->getLabel(), "Second field's label is displayed.");
foreach ($values_2 as $delta => $value) {
$this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
}
// Label hidden.
$entity = clone($entity_init);
$display_options['label'] = 'hidden';
$display->setComponent($this->fieldTestData->field_name, $display_options);
$content = $display->build($entity);
$this->render($content);
$this->assertNoRaw($this->fieldTestData->field->getLabel(), "Hidden label: label is not displayed.");
// Field hidden.
$entity = clone($entity_init);
$display->removeComponent($this->fieldTestData->field_name);
$content = $display->build($entity);
$this->render($content);
$this->assertNoRaw($this->fieldTestData->field->getLabel(), "Hidden field: label is not displayed.");
foreach ($values as $delta => $value) {
$this->assertNoRaw("$formatter_setting|{$value['value']}", "Hidden field: value $delta is not displayed.");
}
// Multiple formatter.
$entity = clone($entity_init);
$formatter_setting = $this->randomMachineName();
$display->setComponent($this->fieldTestData->field_name, array(
'label' => 'above',
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $formatter_setting,
),
));
$content = $display->build($entity);
$this->render($content);
$expected_output = $formatter_setting;
foreach ($values as $delta => $value) {
$expected_output .= "|$delta:{$value['value']}";
}
$this->assertRaw($expected_output, "Multiple formatter: all values are displayed, formatter settings are applied.");
// Test a formatter that uses hook_field_formatter_prepare_view().
$entity = clone($entity_init);
$formatter_setting = $this->randomMachineName();
$display->setComponent($this->fieldTestData->field_name, array(
'label' => 'above',
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $formatter_setting,
),
));
$content = $display->build($entity);
$this->render($content);
foreach ($values as $delta => $value) {
$expected = $formatter_setting . '|' . $value['value'] . '|' . ($value['value'] + 1);
$this->assertRaw($expected, "Value $delta is displayed, formatter settings are applied.");
}
// TODO:
// - check display order with several fields
}
/**
* Tests rendering fields with EntityDisplay::buildMultiple().
*/
function testEntityDisplayViewMultiple() {
// Use a formatter that has a prepareView() step.
$display = entity_get_display('entity_test', 'entity_test', 'full')
->setComponent($this->fieldTestData->field_name, array(
'type' => 'field_test_with_prepare_view',
));
// Create two entities.
$entity1 = entity_create('entity_test', array('id' => 1, 'type' => 'entity_test'));
$entity1->{$this->fieldTestData->field_name}->setValue($this->_generateTestFieldValues(1));
$entity2 = entity_create('entity_test', array('id' => 2, 'type' => 'entity_test'));
$entity2->{$this->fieldTestData->field_name}->setValue($this->_generateTestFieldValues(1));
// Run buildMultiple(), and check that the entities come out as expected.
$display->buildMultiple(array($entity1, $entity2));
$item1 = $entity1->{$this->fieldTestData->field_name}[0];
$this->assertEqual($item1->additional_formatter_value, $item1->value + 1, 'Entity 1 ran through the prepareView() formatter method.');
$item2 = $entity2->{$this->fieldTestData->field_name}[0];
$this->assertEqual($item2->additional_formatter_value, $item2->value + 1, 'Entity 2 ran through the prepareView() formatter method.');
}
/**
* Test entity cache.
*
* Complements unit test coverage in
* \Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTest.
*/
function testEntityCache() {
// Initialize random values and a test entity.
$entity_init = entity_create('entity_test', array('type' => $this->fieldTestData->field->getTargetBundle()));
$values = $this->_generateTestFieldValues($this->fieldTestData->field_storage->getCardinality());
// Non-cacheable entity type.
$entity_type = 'entity_test';
$cid = "values:$entity_type:" . $entity_init->id();
// Check that no initial cache entry is present.
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Non-cached: no initial cache entry');
// Save, and check that no cache entry is present.
$entity = clone($entity_init);
$entity->{$this->fieldTestData->field_name}->setValue($values);
$entity = $this->entitySaveReload($entity);
$cid = "values:$entity_type:" . $entity->id();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Non-cached: no cache entry on insert and load');
// Cacheable entity type.
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('_2', $entity_type);
$entity_init = entity_create($entity_type, array(
'type' => $entity_type,
));
// Check that no initial cache entry is present.
$cid = "values:$entity_type:" . $entity->id();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no initial cache entry');
// Save, and check that no cache entry is present.
$entity = clone($entity_init);
$entity->{$this->fieldTestData->field_name_2} = $values;
$entity->save();
$cid = "values:$entity_type:" . $entity->id();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on insert');
// Load, and check that a cache entry is present with the expected values.
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
$cached_entity = $controller->load($entity->id());
$cache = \Drupal::cache('entity')->get($cid);
$this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load');
// Update with different values, and check that the cache entry is wiped.
$values = $this->_generateTestFieldValues($this->fieldTestData->field_storage_2->getCardinality());
$entity->{$this->fieldTestData->field_name_2} = $values;
$entity->save();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on update');
// Load, and check that a cache entry is present with the expected values.
$controller->resetCache();
$cached_entity = $controller->load($entity->id());
$cache = \Drupal::cache('entity')->get($cid);
$this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load');
// Create a new revision, and check that the cache entry is wiped.
$values = $this->_generateTestFieldValues($this->fieldTestData->field_storage_2->getCardinality());
$entity->{$this->fieldTestData->field_name_2} = $values;
$entity->setNewRevision();
$entity->save();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on new revision creation');
// Load, and check that a cache entry is present with the expected values.
$controller->resetCache();
$cached_entity = $controller->load($entity->id());
$cache = \Drupal::cache('entity')->get($cid);
$this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load');
// Delete, and check that the cache entry is wiped.
$entity->delete();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry after delete');
}
/**
* Tests \Drupal\Core\Entity\Display\EntityFormDisplayInterface::buildForm().
*
* This could be much more thorough, but it does verify that the correct
* widgets show up.
*/
function testEntityFormDisplayBuildForm() {
$this->createFieldWithStorage('_2');
$entity_type = 'entity_test';
$entity = entity_create($entity_type, array('id' => 1, 'revision_id' => 1, 'type' => $this->fieldTestData->field->getTargetBundle()));
// Test generating widgets for all fields.
$display = entity_get_form_display($entity_type, $this->fieldTestData->field->getTargetBundle(), 'default');
$form = array();
$form_state = new FormState();
$display->buildForm($entity, $form, $form_state);
$this->assertEqual($form[$this->fieldTestData->field_name]['widget']['#title'], $this->fieldTestData->field->getLabel(), "First field's form title is {$this->fieldTestData->field->getLabel()}");
$this->assertEqual($form[$this->fieldTestData->field_name_2]['widget']['#title'], $this->fieldTestData->field_2->getLabel(), "Second field's form title is {$this->fieldTestData->field_2->getLabel()}");
for ($delta = 0; $delta < $this->fieldTestData->field_storage->getCardinality(); $delta++) {
// field_test_widget uses 'textfield'
$this->assertEqual($form[$this->fieldTestData->field_name]['widget'][$delta]['value']['#type'], 'textfield', "First field's form delta $delta widget is textfield");
}
for ($delta = 0; $delta < $this->fieldTestData->field_storage_2->getCardinality(); $delta++) {
// field_test_widget uses 'textfield'
$this->assertEqual($form[$this->fieldTestData->field_name_2]['widget'][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield");
}
// Test generating widgets for all fields.
$display = entity_get_form_display($entity_type, $this->fieldTestData->field->getTargetBundle(), 'default');
foreach ($display->getComponents() as $name => $options) {
if ($name != $this->fieldTestData->field_name_2) {
$display->removeComponent($name);
}
}
$form = array();
$form_state = new FormState();
$display->buildForm($entity, $form, $form_state);
$this->assertFalse(isset($form[$this->fieldTestData->field_name]), 'The first field does not exist in the form');
$this->assertEqual($form[$this->fieldTestData->field_name_2]['widget']['#title'], $this->fieldTestData->field_2->getLabel(), "Second field's form title is {$this->fieldTestData->field_2->getLabel()}");
for ($delta = 0; $delta < $this->fieldTestData->field_storage_2->getCardinality(); $delta++) {
// field_test_widget uses 'textfield'
$this->assertEqual($form[$this->fieldTestData->field_name_2]['widget'][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield");
}
}
/**
* Tests \Drupal\Core\Entity\Display\EntityFormDisplayInterface::extractFormValues().
*/
function testEntityFormDisplayExtractFormValues() {
$this->createFieldWithStorage('_2');
$entity_type = 'entity_test';
$entity_init = entity_create($entity_type, array('id' => 1, 'revision_id' => 1, 'type' => $this->fieldTestData->field->getTargetBundle()));
// Build the form for all fields.
$display = entity_get_form_display($entity_type, $this->fieldTestData->field->getTargetBundle(), 'default');
$form = array();
$form_state = new FormState();
$display->buildForm($entity_init, $form, $form_state);
// Simulate incoming values.
// First field.
$values = array();
$weights = array();
for ($delta = 0; $delta < $this->fieldTestData->field_storage->getCardinality(); $delta++) {
$values[$delta]['value'] = mt_rand(1, 127);
// Assign random weight.
do {
$weight = mt_rand(0, $this->fieldTestData->field_storage->getCardinality());
} while (in_array($weight, $weights));
$weights[$delta] = $weight;
$values[$delta]['_weight'] = $weight;
}
// Leave an empty value. 'field_test' fields are empty if empty().
$values[1]['value'] = 0;
// Second field.
$values_2 = array();
$weights_2 = array();
for ($delta = 0; $delta < $this->fieldTestData->field_storage_2->getCardinality(); $delta++) {
$values_2[$delta]['value'] = mt_rand(1, 127);
// Assign random weight.
do {
$weight = mt_rand(0, $this->fieldTestData->field_storage_2->getCardinality());
} while (in_array($weight, $weights_2));
$weights_2[$delta] = $weight;
$values_2[$delta]['_weight'] = $weight;
}
// Leave an empty value. 'field_test' fields are empty if empty().
$values_2[1]['value'] = 0;
// Pretend the form has been built.
$form_state->setFormObject(\Drupal::entityManager()->getFormObject($entity_type, 'default'));
\Drupal::formBuilder()->prepareForm('field_test_entity_form', $form, $form_state);
\Drupal::formBuilder()->processForm('field_test_entity_form', $form, $form_state);
$form_state->setValue($this->fieldTestData->field_name, $values);
$form_state->setValue($this->fieldTestData->field_name_2, $values_2);
// Extract values for all fields.
$entity = clone($entity_init);
$display->extractFormValues($entity, $form, $form_state);
asort($weights);
asort($weights_2);
$expected_values = array();
$expected_values_2 = array();
foreach ($weights as $key => $value) {
if ($key != 1) {
$expected_values[] = array('value' => $values[$key]['value']);
}
}
$this->assertIdentical($entity->{$this->fieldTestData->field_name}->getValue(), $expected_values, 'Submit filters empty values');
foreach ($weights_2 as $key => $value) {
if ($key != 1) {
$expected_values_2[] = array('value' => $values_2[$key]['value']);
}
}
$this->assertIdentical($entity->{$this->fieldTestData->field_name_2}->getValue(), $expected_values_2, 'Submit filters empty values');
// Call EntityFormDisplayInterface::extractFormValues() for a single field (the second field).
foreach ($display->getComponents() as $name => $options) {
if ($name != $this->fieldTestData->field_name_2) {
$display->removeComponent($name);
}
}
$entity = clone($entity_init);
$display->extractFormValues($entity, $form, $form_state);
$expected_values_2 = array();
foreach ($weights_2 as $key => $value) {
if ($key != 1) {
$expected_values_2[] = array('value' => $values_2[$key]['value']);
}
}
$this->assertTrue($entity->{$this->fieldTestData->field_name}->isEmpty(), 'The first field is empty in the entity object');
$this->assertIdentical($entity->{$this->fieldTestData->field_name_2}->getValue(), $expected_values_2, 'Submit filters empty values');
}
}

View file

@ -0,0 +1,378 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldAttachStorageTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\field\Entity\FieldConfig;
/**
* Tests storage-related Field Attach API functions.
*
* @group field
* @todo move this to the Entity module
*/
class FieldAttachStorageTest extends FieldUnitTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_rev');
}
/**
* Check field values insert, update and load.
*
* Works independently of the underlying field storage backend. Inserts or
* updates random field data and then loads and verifies the data.
*/
function testFieldAttachSaveLoad() {
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('', $entity_type);
$cardinality = $this->fieldTestData->field_storage->getCardinality();
// TODO : test empty values filtering and "compression" (store consecutive deltas).
// Preparation: create three revisions and store them in $revision array.
$values = array();
$entity = entity_create($entity_type);
for ($revision_id = 0; $revision_id < 3; $revision_id++) {
// Note: we try to insert one extra value.
$current_values = $this->_generateTestFieldValues($cardinality + 1);
$entity->{$this->fieldTestData->field_name}->setValue($current_values);
$entity->setNewRevision();
$entity->save();
$entity_id = $entity->id();
$current_revision = $entity->getRevisionId();
$values[$current_revision] = $current_values;
}
$storage = $this->container->get('entity.manager')->getStorage($entity_type);
$storage->resetCache();
$entity = $storage->load($entity_id);
// Confirm current revision loads the correct data.
// Number of values per field loaded equals the field cardinality.
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), $cardinality, 'Current revision: expected number of values');
for ($delta = 0; $delta < $cardinality; $delta++) {
// The field value loaded matches the one inserted or updated.
$this->assertEqual($entity->{$this->fieldTestData->field_name}[$delta]->value , $values[$current_revision][$delta]['value'], format_string('Current revision: expected value %delta was found.', array('%delta' => $delta)));
}
// Confirm each revision loads the correct data.
foreach (array_keys($values) as $revision_id) {
$entity = $storage->loadRevision($revision_id);
// Number of values per field loaded equals the field cardinality.
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), $cardinality, format_string('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id)));
for ($delta = 0; $delta < $cardinality; $delta++) {
// The field value loaded matches the one inserted or updated.
$this->assertEqual($entity->{$this->fieldTestData->field_name}[$delta]->value, $values[$revision_id][$delta]['value'], format_string('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta)));
}
}
}
/**
* Test the 'multiple' load feature.
*/
function testFieldAttachLoadMultiple() {
$entity_type = 'entity_test_rev';
// Define 2 bundles.
$bundles = array(
1 => 'test_bundle_1',
2 => 'test_bundle_2',
);
entity_test_create_bundle($bundles[1]);
entity_test_create_bundle($bundles[2]);
// Define 3 fields:
// - field_1 is in bundle_1 and bundle_2,
// - field_2 is in bundle_1,
// - field_3 is in bundle_2.
$field_bundles_map = array(
1 => array(1, 2),
2 => array(1),
3 => array(2),
);
for ($i = 1; $i <= 3; $i++) {
$field_names[$i] = 'field_' . $i;
$field_storage = entity_create('field_storage_config', array(
'field_name' => $field_names[$i],
'entity_type' => $entity_type,
'type' => 'test_field',
));
$field_storage->save();
$field_ids[$i] = $field_storage->uuid();
foreach ($field_bundles_map[$i] as $bundle) {
entity_create('field_config', array(
'field_name' => $field_names[$i],
'entity_type' => $entity_type,
'bundle' => $bundles[$bundle],
))->save();
}
}
// Create one test entity per bundle, with random values.
foreach ($bundles as $index => $bundle) {
$entities[$index] = entity_create($entity_type, array('id' => $index, 'revision_id' => $index, 'type' => $bundle));
$entity = clone($entities[$index]);
foreach ($field_names as $field_name) {
if (!$entity->hasField($field_name)) {
continue;
}
$values[$index][$field_name] = mt_rand(1, 127);
$entity->$field_name->setValue(array('value' => $values[$index][$field_name]));
}
$entity->enforceIsnew();
$entity->save();
}
// Check that a single load correctly loads field values for both entities.
$controller = \Drupal::entityManager()->getStorage($entity->getEntityTypeId());
$controller->resetCache();
$entities = $controller->loadMultiple();
foreach ($entities as $index => $entity) {
foreach ($field_names as $field_name) {
if (!$entity->hasField($field_name)) {
continue;
}
// The field value loaded matches the one inserted.
$this->assertEqual($entity->{$field_name}->value, $values[$index][$field_name], format_string('Entity %index: expected value was found.', array('%index' => $index)));
}
}
}
/**
* Tests insert and update with empty or NULL fields.
*/
function testFieldAttachSaveEmptyData() {
$entity_type = 'entity_test';
$this->createFieldWithStorage('', $entity_type);
$entity_init = entity_create($entity_type, array('id' => 1));
// Insert: Field is NULL.
$entity = clone $entity_init;
$entity->{$this->fieldTestData->field_name} = NULL;
$entity->enforceIsNew();
$entity = $this->entitySaveReload($entity);
$this->assertTrue($entity->{$this->fieldTestData->field_name}->isEmpty(), 'Insert: NULL field results in no value saved');
// All saves after this point should be updates, not inserts.
$entity_init->enforceIsNew(FALSE);
// Add some real data.
$entity = clone($entity_init);
$values = $this->_generateTestFieldValues(1);
$entity->{$this->fieldTestData->field_name} = $values;
$entity = $this->entitySaveReload($entity);
$this->assertEqual($entity->{$this->fieldTestData->field_name}->getValue(), $values, 'Field data saved');
// Update: Field is NULL. Data should be wiped.
$entity = clone($entity_init);
$entity->{$this->fieldTestData->field_name} = NULL;
$entity = $this->entitySaveReload($entity);
$this->assertTrue($entity->{$this->fieldTestData->field_name}->isEmpty(), 'Update: NULL field removes existing values');
// Re-add some data.
$entity = clone($entity_init);
$values = $this->_generateTestFieldValues(1);
$entity->{$this->fieldTestData->field_name} = $values;
$entity = $this->entitySaveReload($entity);
$this->assertEqual($entity->{$this->fieldTestData->field_name}->getValue(), $values, 'Field data saved');
// Update: Field is empty array. Data should be wiped.
$entity = clone($entity_init);
$entity->{$this->fieldTestData->field_name} = array();
$entity = $this->entitySaveReload($entity);
$this->assertTrue($entity->{$this->fieldTestData->field_name}->isEmpty(), 'Update: empty array removes existing values');
}
/**
* Test insert with empty or NULL fields, with default value.
*/
function testFieldAttachSaveEmptyDataDefaultValue() {
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('', $entity_type);
// Add a default value function.
$this->fieldTestData->field->set('default_value_callback', 'field_test_default_value');
$this->fieldTestData->field->save();
// Verify that fields are populated with default values.
$entity_init = entity_create($entity_type, array('id' => 1, 'revision_id' => 1));
$default = field_test_default_value($entity_init, $this->fieldTestData->field);
$this->assertEqual($entity_init->{$this->fieldTestData->field_name}->getValue(), $default, 'Default field value correctly populated.');
// Insert: Field is NULL.
$entity = clone($entity_init);
$entity->{$this->fieldTestData->field_name} = NULL;
$entity->enforceIsNew();
$entity = $this->entitySaveReload($entity);
$this->assertTrue($entity->{$this->fieldTestData->field_name}->isEmpty(), 'Insert: NULL field results in no value saved');
// Verify that prepopulated field values are not overwritten by defaults.
$value = array(array('value' => $default[0]['value'] - mt_rand(1, 127)));
$entity = entity_create($entity_type, array('type' => $entity_init->bundle(), $this->fieldTestData->field_name => $value));
$this->assertEqual($entity->{$this->fieldTestData->field_name}->getValue(), $value, 'Prepopulated field value correctly maintained.');
}
/**
* Test entity deletion.
*/
function testFieldAttachDelete() {
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('', $entity_type);
$cardinality = $this->fieldTestData->field_storage->getCardinality();
$entity = entity_create($entity_type, array('type' => $this->fieldTestData->field->getTargetBundle()));
$vids = array();
// Create revision 0
$values = $this->_generateTestFieldValues($cardinality);
$entity->{$this->fieldTestData->field_name} = $values;
$entity->save();
$vids[] = $entity->getRevisionId();
// Create revision 1
$entity->setNewRevision();
$entity->save();
$vids[] = $entity->getRevisionId();
// Create revision 2
$entity->setNewRevision();
$entity->save();
$vids[] = $entity->getRevisionId();
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
// Confirm each revision loads
foreach ($vids as $vid) {
$revision = $controller->loadRevision($vid);
$this->assertEqual(count($revision->{$this->fieldTestData->field_name}), $cardinality, "The test entity revision $vid has $cardinality values.");
}
// Delete revision 1, confirm the other two still load.
$controller->deleteRevision($vids[1]);
$controller->resetCache();
foreach (array(0, 2) as $key) {
$vid = $vids[$key];
$revision = $controller->loadRevision($vid);
$this->assertEqual(count($revision->{$this->fieldTestData->field_name}), $cardinality, "The test entity revision $vid has $cardinality values.");
}
// Confirm the current revision still loads
$controller->resetCache();
$current = $controller->load($entity->id());
$this->assertEqual(count($current->{$this->fieldTestData->field_name}), $cardinality, "The test entity current revision has $cardinality values.");
// Delete all field data, confirm nothing loads
$entity->delete();
$controller->resetCache();
foreach (array(0, 1, 2) as $vid) {
$revision = $controller->loadRevision($vid);
$this->assertFalse($revision);
}
$this->assertFalse($controller->load($entity->id()));
}
/**
* Test entity_bundle_create() and entity_bundle_rename().
*/
function testEntityCreateRenameBundle() {
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('', $entity_type);
$cardinality = $this->fieldTestData->field_storage->getCardinality();
// Create a new bundle.
$new_bundle = 'test_bundle_' . Unicode::strtolower($this->randomMachineName());
entity_test_create_bundle($new_bundle, NULL, $entity_type);
// Add a field to that bundle.
$this->fieldTestData->field_definition['bundle'] = $new_bundle;
entity_create('field_config', $this->fieldTestData->field_definition)->save();
// Save an entity with data in the field.
$entity = entity_create($entity_type, array('type' => $this->fieldTestData->field->getTargetBundle()));
$values = $this->_generateTestFieldValues($cardinality);
$entity->{$this->fieldTestData->field_name} = $values;
// Verify the field data is present on load.
$entity = $this->entitySaveReload($entity);
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), $cardinality, "Data is retrieved for the new bundle");
// Rename the bundle.
$new_bundle = 'test_bundle_' . Unicode::strtolower($this->randomMachineName());
entity_test_rename_bundle($this->fieldTestData->field_definition['bundle'], $new_bundle, $entity_type);
// Check that the field definition has been updated.
$this->fieldTestData->field = FieldConfig::loadByName($entity_type, $new_bundle, $this->fieldTestData->field_name);
$this->assertIdentical($this->fieldTestData->field->getTargetBundle(), $new_bundle, "Bundle name has been updated in the field.");
// Verify the field data is present on load.
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
$entity = $controller->load($entity->id());
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), $cardinality, "Bundle name has been updated in the field storage");
}
/**
* Test entity_bundle_delete().
*/
function testEntityDeleteBundle() {
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('', $entity_type);
// Create a new bundle.
$new_bundle = 'test_bundle_' . Unicode::strtolower($this->randomMachineName());
entity_test_create_bundle($new_bundle, NULL, $entity_type);
// Add a field to that bundle.
$this->fieldTestData->field_definition['bundle'] = $new_bundle;
entity_create('field_config', $this->fieldTestData->field_definition)->save();
// Create a second field for the test bundle
$field_name = Unicode::strtolower($this->randomMachineName() . '_field_name');
$field_storage = array(
'field_name' => $field_name,
'entity_type' => $entity_type,
'type' => 'test_field',
'cardinality' => 1,
);
entity_create('field_storage_config', $field_storage)->save();
$field = array(
'field_name' => $field_name,
'entity_type' => $entity_type,
'bundle' => $this->fieldTestData->field->getTargetBundle(),
'label' => $this->randomMachineName() . '_label',
'description' => $this->randomMachineName() . '_description',
'weight' => mt_rand(0, 127),
);
entity_create('field_config', $field)->save();
// Save an entity with data for both fields
$entity = entity_create($entity_type, array('type' => $this->fieldTestData->field->getTargetBundle()));
$values = $this->_generateTestFieldValues($this->fieldTestData->field_storage->getCardinality());
$entity->{$this->fieldTestData->field_name} = $values;
$entity->{$field_name} = $this->_generateTestFieldValues(1);
$entity = $this->entitySaveReload($entity);
// Verify the fields are present on load
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), 4, 'First field got loaded');
$this->assertEqual(count($entity->{$field_name}), 1, 'Second field got loaded');
// Delete the bundle.
entity_test_delete_bundle($this->fieldTestData->field->getTargetBundle(), $entity_type);
// Verify no data gets loaded
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
$entity= $controller->load($entity->id());
$this->assertTrue(empty($entity->{$this->fieldTestData->field_name}), 'No data for first field');
$this->assertTrue(empty($entity->{$field_name}), 'No data for second field');
// Verify that the fields are gone.
$this->assertFalse(FieldConfig::load('entity_test.' . $this->fieldTestData->field->getTargetBundle() . '.' . $this->fieldTestData->field_name), "First field is deleted");
$this->assertFalse(FieldConfig::load('entity_test.' . $field['bundle']. '.' . $field_name), "Second field is deleted");
}
}

View file

@ -0,0 +1,269 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldCrudTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\FieldException;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
/**
* Create field entities by attaching fields to entities.
*
* @group field
*/
class FieldCrudTest extends FieldUnitTestBase {
/**
* The field storage entity.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* The field entity definition.
*
* @var array
*/
protected $fieldStorageDefinition;
/**
* The field entity definition.
*
* @var array
*/
protected $fieldDefinition;
function setUp() {
parent::setUp();
$this->fieldStorageDefinition = array(
'field_name' => Unicode::strtolower($this->randomMachineName()),
'entity_type' => 'entity_test',
'type' => 'test_field',
);
$this->fieldStorage = entity_create('field_storage_config', $this->fieldStorageDefinition);
$this->fieldStorage->save();
$this->fieldDefinition = array(
'field_name' => $this->fieldStorage->getName(),
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
);
}
// TODO : test creation with
// - a full fledged $field structure, check that all the values are there
// - a minimal $field structure, check all default values are set
// defer actual $field comparison to a helper function, used for the two cases above,
// and for testUpdateField
/**
* Test the creation of a field.
*/
function testCreateField() {
// Set a state flag so that field_test.module knows to add an in-memory
// constraint for this field.
\Drupal::state()->set('field_test_add_constraint', $this->fieldStorage->getName());
/** @var \Drupal\Core\Field\FieldConfigInterface $field */
$field = entity_create('field_config', $this->fieldDefinition);
$field->save();
$field = FieldConfig::load($field->id());
$this->assertTrue($field->getSetting('field_setting_from_config_data'));
$this->assertNull($field->getSetting('config_data_from_field_setting'));
// Read the configuration. Check against raw configuration data rather than
// the loaded ConfigEntity, to be sure we check that the defaults are
// applied on write.
$config = $this->config('field.field.' . $field->id())->get();
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$this->assertTrue($config['settings']['config_data_from_field_setting']);
$this->assertTrue(!isset($config['settings']['field_setting_from_config_data']));
// Since we are working with raw configuration, this needs to be unset
// manually.
// @see Drupal\field_test\Plugin\Field\FieldType\TestItem::fieldSettingsFromConfigData()
unset($config['settings']['config_data_from_field_setting']);
// Check that default values are set.
$this->assertEqual($config['required'], FALSE, 'Required defaults to false.');
$this->assertIdentical($config['label'], $this->fieldDefinition['field_name'], 'Label defaults to field name.');
$this->assertIdentical($config['description'], '', 'Description defaults to empty string.');
// Check that default settings are set.
$this->assertEqual($config['settings'], $field_type_manager->getDefaultFieldSettings($this->fieldStorageDefinition['type']) , 'Default field settings have been written.');
// Check that the denormalized 'field_type' was properly written.
$this->assertEqual($config['field_type'], $this->fieldStorageDefinition['type']);
// Test constraints are applied. A Range constraint is added dynamically to
// limit the field to values between 0 and 32.
// @see field_test_entity_bundle_field_info_alter()
$this->doFieldValidationTests();
// Test FieldConfigBase::setPropertyConstraints().
\Drupal::state()->set('field_test_set_constraint', $this->fieldStorage->getName());
\Drupal::state()->set('field_test_add_constraint', FALSE);
\Drupal::entityManager()->clearCachedFieldDefinitions();
$this->doFieldValidationTests();
// Guarantee that the field/bundle combination is unique.
try {
entity_create('field_config', $this->fieldDefinition)->save();
$this->fail(t('Cannot create two fields with the same field / bundle combination.'));
}
catch (EntityStorageException $e) {
$this->pass(t('Cannot create two fields with the same field / bundle combination.'));
}
// Check that the specified field exists.
try {
$this->fieldDefinition['field_name'] = $this->randomMachineName();
entity_create('field_config', $this->fieldDefinition)->save();
$this->fail(t('Cannot create a field with a non-existing storage.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a non-existing storage.'));
}
// TODO: test other failures.
}
/**
* Test reading back a field definition.
*/
function testReadField() {
entity_create('field_config', $this->fieldDefinition)->save();
// Read the field back.
$field = FieldConfig::load('entity_test.' . $this->fieldDefinition['bundle'] . '.' . $this->fieldDefinition['field_name']);
$this->assertTrue($this->fieldDefinition['field_name'] == $field->getName(), 'The field was properly read.');
$this->assertTrue($this->fieldDefinition['entity_type'] == $field->getTargetEntityTypeId(), 'The field was properly read.');
$this->assertTrue($this->fieldDefinition['bundle'] == $field->getTargetBundle(), 'The field was properly read.');
}
/**
* Test the update of a field.
*/
function testUpdateField() {
entity_create('field_config', $this->fieldDefinition)->save();
// Check that basic changes are saved.
$field = FieldConfig::load('entity_test.' . $this->fieldDefinition['bundle'] . '.' . $this->fieldDefinition['field_name']);
$field->setRequired(!$field->isRequired());
$field->setLabel($this->randomMachineName());
$field->set('description', $this->randomMachineName());
$field->setSetting('test_field_setting', $this->randomMachineName());
$field->save();
$field_new = FieldConfig::load('entity_test.' . $this->fieldDefinition['bundle'] . '.' . $this->fieldDefinition['field_name']);
$this->assertEqual($field->isRequired(), $field_new->isRequired(), '"required" change is saved');
$this->assertEqual($field->getLabel(), $field_new->getLabel(), '"label" change is saved');
$this->assertEqual($field->getDescription(), $field_new->getDescription(), '"description" change is saved');
// TODO: test failures.
}
/**
* Test the deletion of a field.
*/
function testDeleteField() {
// TODO: Test deletion of the data stored in the field also.
// Need to check that data for a 'deleted' field / storage doesn't get loaded
// Need to check data marked deleted is cleaned on cron (not implemented yet...)
// Create two fields for the same field storage so we can test that only one
// is deleted.
entity_create('field_config', $this->fieldDefinition)->save();
$another_field_definition = $this->fieldDefinition;
$another_field_definition['bundle'] .= '_another_bundle';
entity_test_create_bundle($another_field_definition['bundle']);
entity_create('field_config', $another_field_definition)->save();
// Test that the first field is not deleted, and then delete it.
$field = current(entity_load_multiple_by_properties('field_config', array('entity_type' => 'entity_test', 'field_name' => $this->fieldDefinition['field_name'], 'bundle' => $this->fieldDefinition['bundle'], 'include_deleted' => TRUE)));
$this->assertTrue(!empty($field) && empty($field->deleted), 'A new field is not marked for deletion.');
$field->delete();
// Make sure the field is marked as deleted when it is specifically loaded.
$field = current(entity_load_multiple_by_properties('field_config', array('entity_type' => 'entity_test', 'field_name' => $this->fieldDefinition['field_name'], 'bundle' => $this->fieldDefinition['bundle'], 'include_deleted' => TRUE)));
$this->assertTrue($field->isDeleted(), 'A deleted field is marked for deletion.');
// Try to load the field normally and make sure it does not show up.
$field = FieldConfig::load('entity_test.' . '.' . $this->fieldDefinition['bundle'] . '.' . $this->fieldDefinition['field_name']);
$this->assertTrue(empty($field), 'A deleted field is not loaded by default.');
// Make sure the other field is not deleted.
$another_field = FieldConfig::load('entity_test.' . $another_field_definition['bundle'] . '.' . $another_field_definition['field_name']);
$this->assertTrue(!empty($another_field) && empty($another_field->deleted), 'A non-deleted field is not marked for deletion.');
}
/**
* Tests the cross deletion behavior between field storages and fields.
*/
function testDeleteFieldCrossDeletion() {
$field_definition_2 = $this->fieldDefinition;
$field_definition_2['bundle'] .= '_another_bundle';
entity_test_create_bundle($field_definition_2['bundle']);
// Check that deletion of a field storage deletes its fields.
$field_storage = $this->fieldStorage;
entity_create('field_config', $this->fieldDefinition)->save();
entity_create('field_config', $field_definition_2)->save();
$field_storage->delete();
$this->assertFalse(FieldConfig::loadByName('entity_test', $this->fieldDefinition['bundle'], $field_storage->getName()));
$this->assertFalse(FieldConfig::loadByName('entity_test', $field_definition_2['bundle'], $field_storage->getName()));
// Check that deletion of the last field deletes the storage.
$field_storage = entity_create('field_storage_config', $this->fieldStorageDefinition);
$field_storage->save();
$field = entity_create('field_config', $this->fieldDefinition);
$field->save();
$field_2 = entity_create('field_config', $field_definition_2);
$field_2->save();
$field->delete();
$this->assertTrue(FieldStorageConfig::loadByName('entity_test', $field_storage->getName()));
$field_2->delete();
$this->assertFalse(FieldStorageConfig::loadByName('entity_test', $field_storage->getName()));
// Check that deletion of all fields using a storage simultaneously deletes
// the storage.
$field_storage = entity_create('field_storage_config', $this->fieldStorageDefinition);
$field_storage->save();
$field = entity_create('field_config', $this->fieldDefinition);
$field->save();
$field_2 = entity_create('field_config', $field_definition_2);
$field_2->save();
$this->container->get('entity.manager')->getStorage('field_config')->delete(array($field, $field_2));
$this->assertFalse(FieldStorageConfig::loadByName('entity_test', $field_storage->getName()));
}
/**
* Tests configurable field validation.
*
* @see field_test_entity_bundle_field_info_alter()
*/
protected function doFieldValidationTests() {
$entity = entity_create('entity_test');
$entity->set($this->fieldStorage->getName(), 1);
$violations = $entity->validate();
$this->assertEqual(count($violations), 0, 'No violations found when in-range value passed.');
$entity->set($this->fieldStorage->getName(), 33);
$violations = $entity->validate();
$this->assertEqual(count($violations), 1, 'Violations found when using value outside the range.');
$this->assertEqual($violations[0]->getPropertyPath(), $this->fieldStorage->getName() . '.0.value');
$this->assertEqual($violations[0]->getMessage(), t('This value should be %limit or less.', [
'%limit' => 32,
]));
}
}

View file

@ -0,0 +1,139 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldDataCountTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Tests counting field data records and the hasData() method on
* FieldStorageConfig entity.
*
* @group field
* @see \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData()
* @see \Drupal\field\Entity\FieldStorageConfig::hasData()
*/
class FieldDataCountTest extends FieldUnitTestBase {
/**
* @var \Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface
*/
protected $storage;
/**
* @var \Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface
*/
protected $storageRev;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_rev');
$this->storage = \Drupal::entityManager()->getStorage('entity_test');
$this->storageRev = \Drupal::entityManager()->getStorage('entity_test_rev');
}
/**
* Tests entityCount() and hadData() methods.
*/
public function testEntityCountAndHasData() {
// Create a field with a cardinality of 2 to show that we are counting
// entities and not rows in a table.
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_int',
'entity_type' => 'entity_test',
'type' => 'integer',
'cardinality' => 2,
));
$field_storage->save();
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => 'entity_test',
))->save();
$this->assertIdentical($field_storage->hasdata(), FALSE, 'There are no entities with field data.');
$this->assertIdentical($this->storage->countFieldData($field_storage), 0, 'There are 0 entities with field data.');
// Create 1 entity without the field.
$entity = entity_create('entity_test');
$entity->name->value = $this->randomMachineName();
$entity->save();
$this->assertIdentical($field_storage->hasdata(), FALSE, 'There are no entities with field data.');
$this->assertIdentical($this->storage->countFieldData($field_storage), 0, 'There are 0 entities with field data.');
// Create 12 entities to ensure that the purging works as expected.
for ($i=0; $i < 12; $i++) {
$entity = entity_create('entity_test');
$entity->field_int[] = mt_rand(1,99);
$entity->field_int[] = mt_rand(1,99);
$entity->name[] = $this->randomMachineName();
$entity->save();
}
$storage = \Drupal::entityManager()->getStorage('entity_test');
if ($storage instanceof SqlContentEntityStorage) {
// Count the actual number of rows in the field table.
$table_mapping = $storage->getTableMapping();
$field_table_name = $table_mapping->getDedicatedDataTableName($field_storage);
$result = db_select($field_table_name, 't')
->fields('t')
->countQuery()
->execute()
->fetchField();
$this->assertEqual($result, 24, 'The field table has 24 rows.');
}
$this->assertIdentical($field_storage->hasdata(), TRUE, 'There are entities with field data.');
$this->assertEqual($this->storage->countFieldData($field_storage), 12, 'There are 12 entities with field data.');
// Ensure the methods work on deleted fields.
$field_storage->delete();
$this->assertIdentical($field_storage->hasdata(), TRUE, 'There are entities with deleted field data.');
$this->assertEqual($this->storage->countFieldData($field_storage), 12, 'There are 12 entities with deleted field data.');
field_purge_batch(6);
$this->assertIdentical($field_storage->hasdata(), TRUE, 'There are entities with deleted field data.');
$this->assertEqual($this->storage->countFieldData($field_storage), 6, 'There are 6 entities with deleted field data.');
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('_2', $entity_type);
$entity_init = entity_create($entity_type, array(
'type' => $entity_type,
));
$cardinality = $this->fieldTestData->field_storage_2->getCardinality();
$this->assertIdentical($this->fieldTestData->field_storage_2->hasData(), FALSE, 'There are no entities with field data.');
$this->assertIdentical($this->storageRev->countFieldData($this->fieldTestData->field_storage_2), 0, 'There are 0 entities with field data.');
// Create 1 entity with the field.
$entity = clone($entity_init);
$values = $this->_generateTestFieldValues($this->fieldTestData->field_storage_2->getCardinality());
$entity->{$this->fieldTestData->field_name_2} = $values;
$entity->setNewRevision();
$entity->save();
$first_revision = $entity->getRevisionId();
$this->assertIdentical($this->fieldTestData->field_storage_2->hasData(), TRUE, 'There are entities with field data.');
$this->assertIdentical($this->storageRev->countFieldData($this->fieldTestData->field_storage_2), 1, 'There is 1 entity with field data.');
$entity->{$this->fieldTestData->field_name_2} = array();
$entity->setNewRevision();
$entity->save();
$this->assertIdentical($this->fieldTestData->field_storage_2->hasData(), TRUE, 'There are entities with field data.');
$storage = $this->container->get('entity.manager')->getStorage($entity_type);
$entity = $storage->loadRevision($first_revision);
$this->assertEqual(count($entity->{$this->fieldTestData->field_name_2}), $cardinality, format_string('Revision %revision_id: expected number of values.', array('%revision_id' => $first_revision)));
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldDefinitionIntegrityTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Extension\Extension;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the integrity of field API plugin definitions.
*
* @group field
*/
class FieldDefinitionIntegrityTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Enable all core modules that provide field plugins.
$modules = system_rebuild_module_data();
$modules = array_filter($modules, function (Extension $module) {
// Filter contrib, hidden, already enabled modules and modules in the
// Testing package.
if ($module->origin === 'core'
&& empty($module->info['hidden'])
&& $module->status == FALSE
&& $module->info['package'] !== 'Testing'
&& is_readable($module->getPath() . '/src/Plugin/Field')) {
return TRUE;
}
return FALSE;
});
$this->enableModules(array_keys($modules));
}
/**
* Tests the integrity of field plugin definitions.
*/
public function testFieldPluginDefinitionIntegrity() {
// Load the IDs of all available field type plugins.
$available_field_type_ids = [];
/** @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface $field_type_manager */
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
foreach ($field_type_manager->getDefinitions() as $definition) {
$available_field_type_ids[] = $definition['id'];
}
// Test the field widget plugins.
/** @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface $field_widget_manager */
$field_widget_manager = \Drupal::service('plugin.manager.field.widget');
foreach ($field_widget_manager->getDefinitions() as $definition) {
$missing_field_type_ids = array_diff($definition['field_types'], $available_field_type_ids);
if ($missing_field_type_ids) {
$this->fail(sprintf('Field widget %s integrates with non-existent field types: %s', $definition['id'], implode(', ', $missing_field_type_ids)));
}
else {
$this->pass(sprintf('Field widget %s integrates with existing field types.', $definition['id']));
}
}
// Test the field formatter plugins.
/** @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface $field_formatter_manager */
$field_formatter_manager = \Drupal::service('plugin.manager.field.formatter');
foreach ($field_formatter_manager->getDefinitions() as $definition) {
$missing_field_type_ids = array_diff($definition['field_types'], $available_field_type_ids);
if ($missing_field_type_ids) {
$this->fail(sprintf('Field formatter %s integrates with non-existent field types: %s', $definition['id'], implode(', ', $missing_field_type_ids)));
}
else {
$this->pass(sprintf('Field formatter %s integrates with existing field types.', $definition['id']));
}
}
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldHelpTest.
*/
namespace Drupal\field\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests help display for the Field module.
*
* @group field
*/
class FieldHelpTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array.
*/
public static $modules = array('field', 'help');
// Tests field help implementation without optional core modules enabled.
protected $profile = 'minimal';
/**
* The admin user that will be created.
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
// Create the admin user.
$this->adminUser = $this->drupalCreateUser(array('access administration pages', 'view the administration theme'));
}
/**
* Test the Field module's help page.
*/
public function testFieldHelp() {
// Login the admin user.
$this->drupalLogin($this->adminUser);
// Visit the Help page and make sure no warnings or notices are thrown.
$this->drupalGet('admin/help/field');
// Enable the Options, Email and Field API Test modules.
\Drupal::service('module_installer')->install(array('options', 'field_test'));
$this->resetAll();
\Drupal::service('plugin.manager.field.widget')->clearCachedDefinitions();
\Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions();
$this->drupalGet('admin/help/field');
$this->assertLink('Options', 0, 'Options module is listed on the Field help page.');
$this->assertText('Field API Test', 'Modules with field types that do not implement hook_help are listed.');
$this->assertNoLink('Field API Test', 'Modules with field types that do not implement hook_help are not linked.');
$this->assertNoLink('Link', 'Modules that have not been installed, are not listed.');
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldImportChangeTest.
*/
namespace Drupal\field\Tests;
use Drupal\field\Entity\FieldConfig;
/**
* Update field storage and fields during config change method invocation.
*
* @group field
*/
class FieldImportChangeTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* The default configuration provided by field_test_config is imported by
* \Drupal\field\Tests\FieldUnitTestBase::setUp() when it installs field
* configuration.
*
* @var array
*/
public static $modules = array('field_test_config');
/**
* Tests importing an updated field.
*/
function testImportChange() {
$this->installConfig(['field_test_config']);
$field_storage_id = 'field_test_import';
$field_id = "entity_test.entity_test.$field_storage_id";
$field_config_name = "field.field.$field_id";
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
// Save as files in the staging directory.
$field = $active->read($field_config_name);
$new_label = 'Test update import field';
$field['label'] = $new_label;
$staging->write($field_config_name, $field);
// Import the content of the staging directory.
$this->configImporter()->import();
// Check that the updated config was correctly imported.
$field = FieldConfig::load($field_id);
$this->assertEqual($field->getLabel(), $new_label, 'field label updated');
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldImportCreateTest.
*/
namespace Drupal\field\Tests;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Create field storages and fields during config create method invocation.
*
* @group field
*/
class FieldImportCreateTest extends FieldUnitTestBase {
/**
* Tests creating field storages and fields during default config import.
*/
function testImportCreateDefault() {
$field_name = 'field_test_import';
$field_storage_id = "entity_test.$field_name";
$field_id = "entity_test.entity_test.$field_name";
$field_name_2 = 'field_test_import_2';
$field_storage_id_2 = "entity_test.$field_name_2";
$field_id_2a = "entity_test.entity_test.$field_name_2";
$field_id_2b = "entity_test.test_bundle.$field_name_2";
// Check that the field storages and fields do not exist yet.
$this->assertFalse(FieldStorageConfig::load($field_storage_id));
$this->assertFalse(FieldConfig::load($field_id));
$this->assertFalse(FieldStorageConfig::load($field_storage_id_2));
$this->assertFalse(FieldConfig::load($field_id_2a));
$this->assertFalse(FieldConfig::load($field_id_2b));
// Create a second bundle for the 'Entity test' entity type.
entity_test_create_bundle('test_bundle');
// Enable field_test_config module and check that the field and storage
// shipped in the module's default config were created.
\Drupal::service('module_installer')->install(array('field_test_config'));
// A field storage with one single field.
$field_storage = FieldStorageConfig::load($field_storage_id);
$this->assertTrue($field_storage, 'The field was created.');
$field = FieldConfig::load($field_id);
$this->assertTrue($field, 'The field was deleted.');
// A field storage with two fields.
$field_storage_2 = FieldStorageConfig::load($field_storage_id_2);
$this->assertTrue($field_storage_2, 'The second field was created.');
$this->assertTrue($field->getTargetBundle(), 'test_bundle', 'The second field was created on bundle test_bundle.');
$this->assertTrue($field->getTargetBundle(), 'test_bundle_2', 'The second field was created on bundle test_bundle_2.');
// Tests fields.
$ids = \Drupal::entityQuery('field_config')
->condition('entity_type', 'entity_test')
->condition('bundle', 'entity_test')
->execute();
$this->assertEqual(count($ids), 2);
$this->assertTrue(isset($ids['entity_test.entity_test.field_test_import']));
$this->assertTrue(isset($ids['entity_test.entity_test.field_test_import_2']));
$ids = \Drupal::entityQuery('field_config')
->condition('entity_type', 'entity_test')
->condition('bundle', 'test_bundle')
->execute();
$this->assertEqual(count($ids), 1);
$this->assertTrue(isset($ids['entity_test.test_bundle.field_test_import_2']));
}
/**
* Tests creating field storages and fields during config import.
*/
function testImportCreate() {
// A field storage with one single field.
$field_name = 'field_test_import_staging';
$field_storage_id = "entity_test.$field_name";
$field_id = "entity_test.entity_test.$field_name";
$field_storage_config_name = "field.storage.$field_storage_id";
$field_config_name = "field.field.$field_id";
// A field storage with two fields.
$field_name_2 = 'field_test_import_staging_2';
$field_storage_id_2 = "entity_test.$field_name_2";
$field_id_2a = "entity_test.test_bundle.$field_name_2";
$field_id_2b = "entity_test.test_bundle_2.$field_name_2";
$field_storage_config_name_2 = "field.storage.$field_storage_id_2";
$field_config_name_2a = "field.field.$field_id_2a";
$field_config_name_2b = "field.field.$field_id_2b";
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
// Add the new files to the staging directory.
$src_dir = drupal_get_path('module', 'field_test_config') . '/staging';
$target_dir = $this->configDirectories[CONFIG_STAGING_DIRECTORY];
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_storage_config_name.yml", "$target_dir/$field_storage_config_name.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "$target_dir/$field_config_name.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_storage_config_name_2.yml", "$target_dir/$field_storage_config_name_2.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2a.yml", "$target_dir/$field_config_name_2a.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2b.yml", "$target_dir/$field_config_name_2b.yml"));
// Import the content of the staging directory.
$this->configImporter()->import();
// Check that the field and storage were created.
$field_storage = FieldStorageConfig::load($field_storage_id);
$this->assertTrue($field_storage, 'Test import storage field from staging exists');
$field = FieldConfig::load($field_id);
$this->assertTrue($field, 'Test import field from staging exists');
$field_storage = FieldStorageConfig::load($field_storage_id_2);
$this->assertTrue($field_storage, 'Test import storage field 2 from staging exists');
$field = FieldConfig::load($field_id_2a);
$this->assertTrue($field, 'Test import field 2a from staging exists');
$field = FieldConfig::load($field_id_2b);
$this->assertTrue($field, 'Test import field 2b from staging exists');
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldImportDeleteTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Delete field storages and fields during config delete method invocation.
*
* @group field
*/
class FieldImportDeleteTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* The default configuration provided by field_test_config is imported by
* \Drupal\field\Tests\FieldUnitTestBase::setUp() when it installs field
* configuration.
*
* @var array
*/
public static $modules = array('field_test_config');
/**
* Tests deleting field storages and fields as part of config import.
*/
public function testImportDelete() {
$this->installConfig(['field_test_config']);
// At this point there are 5 field configuration objects in the active
// storage.
// - field.storage.entity_test.field_test_import
// - field.storage.entity_test.field_test_import_2
// - field.field.entity_test.entity_test.field_test_import
// - field.field.entity_test.entity_test.field_test_import_2
// - field.field.entity_test.test_bundle.field_test_import_2
$field_name = 'field_test_import';
$field_storage_id = "entity_test.$field_name";
$field_name_2 = 'field_test_import_2';
$field_storage_id_2 = "entity_test.$field_name_2";
$field_id = "entity_test.entity_test.$field_name";
$field_id_2a = "entity_test.entity_test.$field_name_2";
$field_id_2b = "entity_test.test_bundle.$field_name_2";
$field_storage_config_name = "field.storage.$field_storage_id";
$field_storage_config_name_2 = "field.storage.$field_storage_id_2";
$field_config_name = "field.field.$field_id";
$field_config_name_2a = "field.field.$field_id_2a";
$field_config_name_2b = "field.field.$field_id_2b";
// Create a second bundle for the 'Entity test' entity type.
entity_test_create_bundle('test_bundle');
// Get the uuid's for the field storages.
$field_storage_uuid = FieldStorageConfig::load($field_storage_id)->uuid();
$field_storage_uuid_2 = FieldStorageConfig::load($field_storage_id_2)->uuid();
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
$this->assertTrue($staging->delete($field_storage_config_name), SafeMarkup::format('Deleted field storage: !field_storage', array('!field_storage' => $field_storage_config_name)));
$this->assertTrue($staging->delete($field_storage_config_name_2), SafeMarkup::format('Deleted field storage: !field_storage', array('!field_storage' => $field_storage_config_name_2)));
$this->assertTrue($staging->delete($field_config_name), SafeMarkup::format('Deleted field: !field', array('!field' => $field_config_name)));
$this->assertTrue($staging->delete($field_config_name_2a), SafeMarkup::format('Deleted field: !field', array('!field' => $field_config_name_2a)));
$this->assertTrue($staging->delete($field_config_name_2b), SafeMarkup::format('Deleted field: !field', array('!field' => $field_config_name_2b)));
$deletes = $this->configImporter()->getUnprocessedConfiguration('delete');
$this->assertEqual(count($deletes), 5, 'Importing configuration will delete 3 fields and 2 field storages.');
// Import the content of the staging directory.
$this->configImporter()->import();
// Check that the field storages and fields are gone.
\Drupal::entityManager()->getStorage('field_storage_config')->resetCache(array($field_storage_id));
$field_storage = FieldStorageConfig::load($field_storage_id);
$this->assertFalse($field_storage, 'The field storage was deleted.');
\Drupal::entityManager()->getStorage('field_storage_config')->resetCache(array($field_storage_id_2));
$field_storage_2 = FieldStorageConfig::load($field_storage_id_2);
$this->assertFalse($field_storage_2, 'The second field storage was deleted.');
\Drupal::entityManager()->getStorage('field_config')->resetCache(array($field_id));
$field = FieldConfig::load($field_id);
$this->assertFalse($field, 'The field was deleted.');
\Drupal::entityManager()->getStorage('field_config')->resetCache(array($field_id_2a));
$field_2a = FieldConfig::load($field_id_2a);
$this->assertFalse($field_2a, 'The second field on test bundle was deleted.');
\Drupal::entityManager()->getStorage('field_config')->resetCache(array($field_id_2b));
$field_2b = FieldConfig::load($field_id_2b);
$this->assertFalse($field_2b, 'The second field on test bundle 2 was deleted.');
// Check that all config files are gone.
$active = $this->container->get('config.storage');
$this->assertIdentical($active->listAll($field_storage_config_name), array());
$this->assertIdentical($active->listAll($field_storage_config_name_2), array());
$this->assertIdentical($active->listAll($field_config_name), array());
$this->assertIdentical($active->listAll($field_config_name_2a), array());
$this->assertIdentical($active->listAll($field_config_name_2b), array());
// Check that the storage definition is preserved in state.
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
$this->assertTrue(isset($deleted_storages[$field_storage_uuid]));
$this->assertTrue(isset($deleted_storages[$field_storage_uuid_2]));
// Purge field data, and check that the storage definition has been
// completely removed once the data is purged.
field_purge_batch(10);
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
$this->assertTrue(empty($deleted_storages), 'Fields are deleted');
}
}

View file

@ -0,0 +1,169 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldImportDeleteUninstallTest.
*/
namespace Drupal\field\Tests;
/**
* Delete field storages and fields during config synchronization and uninstall
* module that provides the field type.
*
* @group field
* @see \Drupal\field\ConfigImporterFieldPurger
* @see field_config_import_steps_alter()
*/
class FieldImportDeleteUninstallTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('telephone');
protected function setUp() {
parent::setUp();
// Module uninstall requires the router and users_data tables.
// @see drupal_flush_all_caches()
// @see user_modules_uninstalled()
$this->installSchema('user', array('users_data'));
}
/**
* Tests deleting field storages and fields as part of config import.
*/
public function testImportDeleteUninstall() {
// Create a field to delete to prove that
// \Drupal\field\ConfigImporterFieldPurger does not purge fields that are
// not related to the configuration synchronization.
$unrelated_field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_int',
'entity_type' => 'entity_test',
'type' => 'integer',
));
$unrelated_field_storage->save();
entity_create('field_config', array(
'field_storage' => $unrelated_field_storage,
'bundle' => 'entity_test',
))->save();
// Create a telephone field for validation.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_test',
'entity_type' => 'entity_test',
'type' => 'telephone',
));
$field_storage->save();
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => 'entity_test',
))->save();
$entity = entity_create('entity_test');
$value = '+0123456789';
$entity->field_test = $value;
$entity->field_int = '99';
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->field_test->value, $value);
$this->assertEqual($entity->field_test[0]->value, $value);
$this->assertEqual($entity->field_int->value, '99');
// Delete unrelated field before copying configuration and running the
// synchronization.
$unrelated_field_storage->delete();
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
// Stage uninstall of the Telephone module.
$core_extension = $this->config('core.extension')->get();
unset($core_extension['module']['telephone']);
$staging->write('core.extension', $core_extension);
// Stage the field deletion
$staging->delete('field.storage.entity_test.field_test');
$staging->delete('field.field.entity_test.entity_test.field_test');
$steps = $this->configImporter()->initialize();
$this->assertIdentical($steps[0], array('\Drupal\field\ConfigImporterFieldPurger', 'process'), 'The additional process configuration synchronization step has been added.');
// This will purge all the data, delete the field and uninstall the
// Telephone module.
$this->configImporter()->import();
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('telephone'));
$this->assertFalse(\Drupal::entityManager()->loadEntityByUuid('field_storage_config', $field_storage->uuid()), 'The test field has been deleted by the configuration synchronization');
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
$this->assertFalse(isset($deleted_storages[$field_storage->uuid()]), 'Telephone field has been completed removed from the system.');
$this->assertTrue(isset($deleted_storages[$unrelated_field_storage->uuid()]), 'Unrelated field not purged by configuration synchronization.');
}
/**
* Tests purging already deleted field storages and fields during a config
* import.
*/
public function testImportAlreadyDeletedUninstall() {
// Create a telephone field for validation.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_test',
'entity_type' => 'entity_test',
'type' => 'telephone',
));
$field_storage->save();
$field_storage_uuid = $field_storage->uuid();
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => 'entity_test',
))->save();
// Create 12 entities to ensure that the purging works as expected.
for ($i=0; $i < 12; $i++) {
$entity = entity_create('entity_test');
$value = '+0123456789';
$entity->field_test = $value;
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->field_test->value, $value);
}
// Delete the field.
$field_storage->delete();
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
// Stage uninstall of the Telephone module.
$core_extension = $this->config('core.extension')->get();
unset($core_extension['module']['telephone']);
$staging->write('core.extension', $core_extension);
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
$this->assertTrue(isset($deleted_storages[$field_storage_uuid]), 'Field has been deleted and needs purging before configuration synchronization.');
$steps = $this->configImporter()->initialize();
$this->assertIdentical($steps[0], array('\Drupal\field\ConfigImporterFieldPurger', 'process'), 'The additional process configuration synchronization step has been added.');
// This will purge all the data, delete the field and uninstall the
// Telephone module.
$this->configImporter()->import();
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('telephone'));
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
$this->assertFalse(isset($deleted_storages[$field_storage_uuid]), 'Field has been completed removed from the system.');
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldImportDeleteUninstallUiTest.
*/
namespace Drupal\field\Tests;
/**
* Delete field storages and fields during config synchronization and uninstall
* module that provides the field type through the UI.
*
* @group field
* @see \Drupal\field\ConfigImporterFieldPurger
* @see field_config_import_steps_alter()
* @see field_form_config_admin_import_form_alter()
*/
class FieldImportDeleteUninstallUiTest extends FieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test', 'telephone', 'config', 'filter', 'datetime');
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(array('synchronize configuration')));
}
/**
* Tests deleting field storages and fields as part of config import.
*/
public function testImportDeleteUninstall() {
// Create a telephone field.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_tel',
'entity_type' => 'entity_test',
'type' => 'telephone',
));
$field_storage->save();
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => 'entity_test',
))->save();
// Create a text field.
$date_field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_date',
'entity_type' => 'entity_test',
'type' => 'datetime',
));
$date_field_storage->save();
entity_create('field_config', array(
'field_storage' => $date_field_storage,
'bundle' => 'entity_test',
))->save();
// Create an entity which has values for the telephone and text field.
$entity = entity_create('entity_test');
$value = '+0123456789';
$entity->field_tel = $value;
$entity->field_date = time();
$entity->name->value = $this->randomMachineName();
$entity->save();
// Delete the text field before exporting configuration so that we can test
// that deleted fields that are provided by modules that will be uninstalled
// are also purged and that the UI message includes such fields.
$date_field_storage->delete();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->field_tel->value, $value);
$this->assertEqual($entity->field_tel[0]->value, $value);
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
// Stage uninstall of the Telephone module.
$core_extension = $this->config('core.extension')->get();
unset($core_extension['module']['telephone']);
$staging->write('core.extension', $core_extension);
// Stage the field deletion
$staging->delete('field.storage.entity_test.field_tel');
$staging->delete('field.field.entity_test.entity_test.field_tel');
$this->drupalGet('admin/config/development/configuration');
// Test that the message for one field being purged during a configuration
// synchronization is correct.
$this->assertText('This synchronization will delete data from the field entity_test.field_tel.');
// Stage an uninstall of the datetime module to test the message for
// multiple fields.
unset($core_extension['module']['datetime']);
$staging->write('core.extension', $core_extension);
$this->drupalGet('admin/config/development/configuration');
$this->assertText('This synchronization will delete data from the fields: entity_test.field_tel, entity_test.field_date.');
// This will purge all the data, delete the field and uninstall the
// Telephone and Text modules.
$this->drupalPostForm(NULL, array(), t('Import all'));
$this->assertNoText('Field data will be deleted by this synchronization.');
$this->rebuildContainer();
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('telephone'));
$this->assertFalse(\Drupal::entityManager()->loadEntityByUuid('field_storage_config', $field_storage->uuid()), 'The telephone field has been deleted by the configuration synchronization');
$deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: array();
$this->assertFalse(isset($deleted_storages[$field_storage->uuid()]), 'Telephone field has been completed removed from the system.');
$this->assertFalse(isset($deleted_storages[$field_storage->uuid()]), 'Text field has been completed removed from the system.');
}
}

View file

@ -0,0 +1,461 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldStorageCrudTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Field\FieldException;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests field storage create, read, update, and delete.
*
* @group field
*/
class FieldStorageCrudTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array();
// TODO : test creation with
// - a full fledged $field structure, check that all the values are there
// - a minimal $field structure, check all default values are set
// defer actual $field comparison to a helper function, used for the two cases above
/**
* Test the creation of a field storage.
*/
function testCreate() {
$field_storage_definition = array(
'field_name' => 'field_2',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
field_test_memorize();
$field_storage = entity_create('field_storage_config', $field_storage_definition);
$field_storage->save();
$field_storage = FieldStorageConfig::load($field_storage->id());
$this->assertTrue($field_storage->getSetting('storage_setting_from_config_data'));
$this->assertNull($field_storage->getSetting('config_data_from_storage_setting'));
$mem = field_test_memorize();
$this->assertIdentical($mem['field_test_field_storage_config_create'][0][0]->getName(), $field_storage_definition['field_name'], 'hook_entity_create() called with correct arguments.');
$this->assertIdentical($mem['field_test_field_storage_config_create'][0][0]->getType(), $field_storage_definition['type'], 'hook_entity_create() called with correct arguments.');
// Read the configuration. Check against raw configuration data rather than
// the loaded ConfigEntity, to be sure we check that the defaults are
// applied on write.
$field_storage_config = $this->config('field.storage.' . $field_storage->id())->get();
$this->assertTrue($field_storage_config['settings']['config_data_from_storage_setting']);
$this->assertTrue(!isset($field_storage_config['settings']['storage_setting_from_config_data']));
// Since we are working with raw configuration, this needs to be unset
// manually.
// @see Drupal\field_test\Plugin\Field\FieldType\TestItem::storageSettingsFromConfigData()
unset($field_storage_config['settings']['config_data_from_storage_setting']);
// Ensure that basic properties are preserved.
$this->assertEqual($field_storage_config['field_name'], $field_storage_definition['field_name'], 'The field name is properly saved.');
$this->assertEqual($field_storage_config['entity_type'], $field_storage_definition['entity_type'], 'The field entity type is properly saved.');
$this->assertEqual($field_storage_config['id'], $field_storage_definition['entity_type'] . '.' . $field_storage_definition['field_name'], 'The field id is properly saved.');
$this->assertEqual($field_storage_config['type'], $field_storage_definition['type'], 'The field type is properly saved.');
// Ensure that cardinality defaults to 1.
$this->assertEqual($field_storage_config['cardinality'], 1, 'Cardinality defaults to 1.');
// Ensure that default settings are present.
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$this->assertEqual($field_storage_config['settings'], $field_type_manager->getDefaultStorageSettings($field_storage_definition['type']), 'Default storage settings have been written.');
// Guarantee that the name is unique.
try {
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create two fields with the same name.'));
}
catch (EntityStorageException $e) {
$this->pass(t('Cannot create two fields with the same name.'));
}
// Check that field type is required.
try {
$field_storage_definition = array(
'field_name' => 'field_1',
'entity_type' => 'entity_type',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create a field with no type.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with no type.'));
}
// Check that field name is required.
try {
$field_storage_definition = array(
'type' => 'test_field',
'entity_type' => 'entity_test',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create an unnamed field.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create an unnamed field.'));
}
// Check that entity type is required.
try {
$field_storage_definition = array(
'field_name' => 'test_field',
'type' => 'test_field'
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail('Cannot create a field without an entity type.');
}
catch (FieldException $e) {
$this->pass('Cannot create a field without an entity type.');
}
// Check that field name must start with a letter or _.
try {
$field_storage_definition = array(
'field_name' => '2field_2',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create a field with a name starting with a digit.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a name starting with a digit.'));
}
// Check that field name must only contain lowercase alphanumeric or _.
try {
$field_storage_definition = array(
'field_name' => 'field#_3',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create a field with a name containing an illegal character.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a name containing an illegal character.'));
}
// Check that field name cannot be longer than 32 characters long.
try {
$field_storage_definition = array(
'field_name' => '_12345678901234567890123456789012',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create a field with a name longer than 32 characters.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a name longer than 32 characters.'));
}
// Check that field name can not be an entity key.
// "id" is known as an entity key from the "entity_test" type.
try {
$field_storage_definition = array(
'type' => 'test_field',
'field_name' => 'id',
'entity_type' => 'entity_test',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$this->fail(t('Cannot create a field bearing the name of an entity key.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field bearing the name of an entity key.'));
}
}
/**
* Tests that an explicit schema can be provided on creation.
*
* This behavior is needed to allow field storage creation within updates,
* since plugin classes (and thus the field type schema) cannot be accessed.
*/
function testCreateWithExplicitSchema() {
$schema = array(
'dummy' => 'foobar'
);
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_2',
'entity_type' => 'entity_test',
'type' => 'test_field',
'schema' => $schema,
));
$this->assertEqual($field_storage->getSchema(), $schema);
}
/**
* Tests reading field storage definitions.
*/
function testRead() {
$field_storage_definition = array(
'field_name' => 'field_1',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
$field_storage = entity_create('field_storage_config', $field_storage_definition);
$field_storage->save();
$id = $field_storage->id();
// Check that 'single column' criteria works.
$fields = entity_load_multiple_by_properties('field_storage_config', array('field_name' => $field_storage_definition['field_name']));
$this->assertTrue(count($fields) == 1 && isset($fields[$id]), 'The field was properly read.');
// Check that 'multi column' criteria works.
$fields = entity_load_multiple_by_properties('field_storage_config', array('field_name' => $field_storage_definition['field_name'], 'type' => $field_storage_definition['type']));
$this->assertTrue(count($fields) == 1 && isset($fields[$id]), 'The field was properly read.');
$fields = entity_load_multiple_by_properties('field_storage_config', array('field_name' => $field_storage_definition['field_name'], 'type' => 'foo'));
$this->assertTrue(empty($fields), 'No field was found.');
// Create a field from the field storage.
$field_definition = array(
'field_name' => $field_storage_definition['field_name'],
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
);
entity_create('field_config', $field_definition)->save();
}
/**
* Test creation of indexes on data column.
*/
function testIndexes() {
// Check that indexes specified by the field type are used by default.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_1',
'entity_type' => 'entity_test',
'type' => 'test_field',
));
$field_storage->save();
$field_storage = FieldStorageConfig::load($field_storage->id());
$schema = $field_storage->getSchema();
$expected_indexes = array('value' => array('value'));
$this->assertEqual($schema['indexes'], $expected_indexes, 'Field type indexes saved by default');
// Check that indexes specified by the field definition override the field
// type indexes.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_2',
'entity_type' => 'entity_test',
'type' => 'test_field',
'indexes' => array(
'value' => array(),
),
));
$field_storage->save();
$field_storage = FieldStorageConfig::load($field_storage->id());
$schema = $field_storage->getSchema();
$expected_indexes = array('value' => array());
$this->assertEqual($schema['indexes'], $expected_indexes, 'Field definition indexes override field type indexes');
// Check that indexes specified by the field definition add to the field
// type indexes.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_3',
'entity_type' => 'entity_test',
'type' => 'test_field',
'indexes' => array(
'value_2' => array('value'),
),
));
$field_storage->save();
$id = $field_storage->id();
$field_storage = FieldStorageConfig::load($id);
$schema = $field_storage->getSchema();
$expected_indexes = array('value' => array('value'), 'value_2' => array('value'));
$this->assertEqual($schema['indexes'], $expected_indexes, 'Field definition indexes are merged with field type indexes');
}
/**
* Test the deletion of a field storage.
*/
function testDelete() {
// TODO: Also test deletion of the data stored in the field ?
// Create two fields (so we can test that only one is deleted).
$field_storage_definition = array(
'field_name' => 'field_1',
'type' => 'test_field',
'entity_type' => 'entity_test',
);
entity_create('field_storage_config', $field_storage_definition)->save();
$another_field_storage_definition = array(
'field_name' => 'field_2',
'type' => 'test_field',
'entity_type' => 'entity_test',
);
entity_create('field_storage_config', $another_field_storage_definition)->save();
// Create fields for each.
$field_definition = array(
'field_name' => $field_storage_definition['field_name'],
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
);
entity_create('field_config', $field_definition)->save();
$another_field_definition = $field_definition;
$another_field_definition['field_name'] = $another_field_storage_definition['field_name'];
entity_create('field_config', $another_field_definition)->save();
// Test that the first field is not deleted, and then delete it.
$field_storage = current(entity_load_multiple_by_properties('field_storage_config', array('field_name' => $field_storage_definition['field_name'], 'include_deleted' => TRUE)));
$this->assertTrue(!empty($field_storage) && !$field_storage->isDeleted(), 'A new storage is not marked for deletion.');
FieldStorageConfig::loadByName('entity_test', $field_storage_definition['field_name'])->delete();
// Make sure that the field is marked as deleted when it is specifically
// loaded.
$field_storage = current(entity_load_multiple_by_properties('field_storage_config', array('field_name' => $field_storage_definition['field_name'], 'include_deleted' => TRUE)));
$this->assertTrue($field_storage->isDeleted(), 'A deleted storage is marked for deletion.');
// Make sure that this field is marked as deleted when it is
// specifically loaded.
$field = current(entity_load_multiple_by_properties('field_config', array('entity_type' => 'entity_test', 'field_name' => $field_definition['field_name'], 'bundle' => $field_definition['bundle'], 'include_deleted' => TRUE)));
$this->assertTrue($field->isDeleted(), 'A field whose storage was deleted is marked for deletion.');
// Try to load the storage normally and make sure it does not show up.
$field_storage = FieldStorageConfig::load('entity_test.' . $field_storage_definition['field_name']);
$this->assertTrue(empty($field_storage), 'A deleted storage is not loaded by default.');
// Try to load the field normally and make sure it does not show up.
$field = FieldConfig::load('entity_test.' . '.' . $field_definition['bundle'] . '.' . $field_definition['field_name']);
$this->assertTrue(empty($field), 'A field whose storage was deleted is not loaded by default.');
// Make sure the other field and its storage are not deleted.
$another_field_storage = FieldStorageConfig::load('entity_test.' . $another_field_storage_definition['field_name']);
$this->assertTrue(!empty($another_field_storage) && !$another_field_storage->isDeleted(), 'A non-deleted storage is not marked for deletion.');
$another_field = FieldConfig::load('entity_test.' . $another_field_definition['bundle'] . '.' . $another_field_definition['field_name']);
$this->assertTrue(!empty($another_field) && !$another_field->isDeleted(), 'A field whose storage was not deleted is not marked for deletion.');
// Try to create a new field the same name as a deleted field and
// write data into it.
entity_create('field_storage_config', $field_storage_definition)->save();
entity_create('field_config', $field_definition)->save();
$field_storage = FieldStorageConfig::load('entity_test.' . $field_storage_definition['field_name']);
$this->assertTrue(!empty($field_storage) && !$field_storage->isDeleted(), 'A new storage with a previously used name is created.');
$field = FieldConfig::load('entity_test.' . $field_definition['bundle'] . '.' . $field_definition['field_name'] );
$this->assertTrue(!empty($field) && !$field->isDeleted(), 'A new field for a previously used field name is created.');
// Save an entity with data for the field
$entity = entity_create('entity_test');
$values[0]['value'] = mt_rand(1, 127);
$entity->{$field_storage->getName()}->value = $values[0]['value'];
$entity = $this->entitySaveReload($entity);
// Verify the field is present on load
$this->assertIdentical(count($entity->{$field_storage->getName()}), count($values), "Data in previously deleted field saves and loads correctly");
foreach ($values as $delta => $value) {
$this->assertEqual($entity->{$field_storage->getName()}[$delta]->value, $values[$delta]['value'], "Data in previously deleted field saves and loads correctly");
}
}
function testUpdateFieldType() {
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_type',
'entity_type' => 'entity_test',
'type' => 'decimal',
));
$field_storage->save();
try {
$field_storage->set('type', 'integer');
$field_storage->save();
$this->fail(t('Cannot update a field to a different type.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot update a field to a different type.'));
}
}
/**
* Test updating a field storage.
*/
function testUpdate() {
// Create a field with a defined cardinality, so that we can ensure it's
// respected. Since cardinality enforcement is consistent across database
// systems, it makes a good test case.
$cardinality = 4;
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_update',
'entity_type' => 'entity_test',
'type' => 'test_field',
'cardinality' => $cardinality,
));
$field_storage->save();
$field = entity_create('field_config', array(
'field_storage' => $field_storage,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
));
$field->save();
do {
$entity = entity_create('entity_test');
// Fill in the entity with more values than $cardinality.
for ($i = 0; $i < 20; $i++) {
// We can not use $i here because 0 values are filtered out.
$entity->field_update[] = $i + 1;
}
// Load back and assert there are $cardinality number of values.
$entity = $this->entitySaveReload($entity);
$this->assertEqual(count($entity->field_update), $field_storage->getCardinality());
// Now check the values themselves.
for ($delta = 0; $delta < $cardinality; $delta++) {
$this->assertEqual($entity->field_update[$delta]->value, $delta + 1);
}
// Increase $cardinality and set the field cardinality to the new value.
$field_storage->setCardinality(++$cardinality);
$field_storage->save();
} while ($cardinality < 6);
}
/**
* Test field type modules forbidding an update.
*/
function testUpdateForbid() {
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'forbidden',
'entity_type' => 'entity_test',
'type' => 'test_field',
'settings' => array(
'changeable' => 0,
'unchangeable' => 0
)));
$field_storage->save();
$field_storage->setSetting('changeable', $field_storage->getSetting('changeable') + 1);
try {
$field_storage->save();
$this->pass(t("A changeable setting can be updated."));
}
catch (FieldStorageDefinitionUpdateForbiddenException $e) {
$this->fail(t("An unchangeable setting cannot be updated."));
}
$field_storage->setSetting('unchangeable', $field_storage->getSetting('unchangeable') + 1);
try {
$field_storage->save();
$this->fail(t("An unchangeable setting can be updated."));
}
catch (FieldStorageDefinitionUpdateForbiddenException $e) {
$this->pass(t("An unchangeable setting cannot be updated."));
}
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldTestBase.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\simpletest\WebTestBase;
/**
* Parent class for Field API tests.
*/
abstract class FieldTestBase extends WebTestBase {
/**
* Generate random values for a field_test field.
*
* @param $cardinality
* Number of values to generate.
* @return
* An array of random values, in the format expected for field values.
*/
function _generateTestFieldValues($cardinality) {
$values = array();
for ($i = 0; $i < $cardinality; $i++) {
// field_test fields treat 0 as 'empty value'.
$values[$i]['value'] = mt_rand(1, 127);
}
return $values;
}
/**
* Assert that a field has the expected values in an entity.
*
* This function only checks a single column in the field values.
*
* @param EntityInterface $entity
* The entity to test.
* @param $field_name
* The name of the field to test
* @param $expected_values
* The array of expected values.
* @param $langcode
* (Optional) The language code for the values. Defaults to
* \Drupal\Core\Language\LanguageInterface::LANGCODE_NOT_SPECIFIED.
* @param $column
* (Optional) The name of the column to check. Defaults to 'value'.
*/
function assertFieldValues(EntityInterface $entity, $field_name, $expected_values, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $column = 'value') {
// Re-load the entity to make sure we have the latest changes.
\Drupal::entityManager()->getStorage($entity->getEntityTypeId())->resetCache(array($entity->id()));
$e = entity_load($entity->getEntityTypeId(), $entity->id());
$field = $values = $e->getTranslation($langcode)->$field_name;
// Filter out empty values so that they don't mess with the assertions.
$field->filterEmptyItems();
$values = $field->getValue();
$this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.');
foreach ($expected_values as $key => $value) {
$this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value)));
}
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldTypePluginManagerTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\entity_test\Entity\EntityTest;
/**
* Tests the field type manager.
*
* @group field
*/
class FieldTypePluginManagerTest extends FieldUnitTestBase {
/**
* Tests the default settings convenience methods.
*/
function testDefaultSettings() {
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
foreach (array('test_field', 'shape', 'hidden_test_field') as $type) {
$definition = $field_type_manager->getDefinition($type);
$this->assertIdentical($field_type_manager->getDefaultStorageSettings($type), $definition['class']::defaultStorageSettings(), format_string("%type storage settings were returned", array('%type' => $type)));
$this->assertIdentical($field_type_manager->getDefaultFieldSettings($type), $definition['class']::defaultFieldSettings(), format_string(" %type field settings were returned", array('%type' => $type)));
}
}
/**
* Tests creation of field item instances.
*/
public function testCreateInstance() {
/** @var \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager */
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
foreach (array('test_field', 'shape', 'hidden_test_field') as $type) {
$definition = $field_type_manager->getDefinition($type);
$class = $definition['class'];
$field_name = 'field_' . $type;
$field_definition = BaseFieldDefinition::create($type);
$configuration = array(
'field_definition' => $field_definition,
'name' => $field_name,
'parent' => NULL,
);
$instance = $field_type_manager->createInstance($type, $configuration);
$this->assertTrue($instance instanceof $class, SafeMarkup::format('Created a @class instance', array('@class' => $class)));
$this->assertEqual($field_name, $instance->getName(), SafeMarkup::format('Instance name is @name', array('@name' => $field_name)));
}
}
/**
* Tests creation of field item instances.
*/
public function testCreateInstanceWithConfig() {
/** @var \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager */
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$type = 'test_field';
$definition = $field_type_manager->getDefinition($type);
$class = $definition['class'];
$field_name = 'field_' . $type;
$field_definition = BaseFieldDefinition::create($type)
->setLabel('Jenny')
->setDefaultValue(8675309);
$configuration = array(
'field_definition' => $field_definition,
'name' => $field_name,
'parent' => NULL,
);
$entity = EntityTest::create();
$instance = $field_type_manager->createInstance($type, $configuration);
$this->assertTrue($instance instanceof $class, SafeMarkup::format('Created a @class instance', array('@class' => $class)));
$this->assertEqual($field_name, $instance->getName(), SafeMarkup::format('Instance name is @name', array('@name' => $field_name)));
$this->assertEqual($instance->getFieldDefinition()->getLabel(), 'Jenny', 'Instance label is Jenny');
$this->assertEqual($instance->getFieldDefinition()->getDefaultValue($entity), [['value' => 8675309]], 'Instance default_value is 8675309');
}
}

View file

@ -0,0 +1,206 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldUnitTestBase.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\simpletest\KernelTestBase;
/**
* Parent class for Field API unit tests.
*/
abstract class FieldUnitTestBase extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('user', 'system', 'field', 'text', 'entity_test', 'field_test', 'entity_reference');
/**
* Bag of created field storages and fields.
*
* Allows easy access to test field storage/field names/IDs/objects via:
* - $this->fieldTestData->field_name[suffix]
* - $this->fieldTestData->field_storage[suffix]
* - $this->fieldTestData->field_storage_uuid[suffix]
* - $this->fieldTestData->field[suffix]
* - $this->fieldTestData->field_definition[suffix]
*
* @see \Drupal\field\Tests\FieldUnitTestBase::createFieldWithStorage()
*
* @var \ArrayObject
*/
protected $fieldTestData;
/**
* Set the default field storage backend for fields created during tests.
*/
protected function setUp() {
parent::setUp();
$this->fieldTestData = new \ArrayObject(array(), \ArrayObject::ARRAY_AS_PROPS);
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
$this->installSchema('system', ['router', 'sequences', 'key_value']);
// Set default storage backend and configure the theme system.
$this->installConfig(array('field', 'system'));
// Create user 1.
$storage = \Drupal::entityManager()->getStorage('user');
$storage
->create(array(
'uid' => 1,
'name' => 'entity-test',
'mail' => 'entity@localhost',
'status' => TRUE,
))
->save();
}
/**
* Create a field and an associated field storage.
*
* @param string $suffix
* (optional) A string that should only contain characters that are valid in
* PHP variable names as well.
* @param string $entity_type
* (optional) The entity type on which the field should be created.
* Defaults to "entity_test".
* @param string $bundle
* (optional) The entity type on which the field should be created.
* Defaults to the default bundle of the entity type.
*/
protected function createFieldWithStorage($suffix = '', $entity_type = 'entity_test', $bundle = NULL) {
if (empty($bundle)) {
$bundle = $entity_type;
}
$field_name = 'field_name' . $suffix;
$field_storage = 'field_storage' . $suffix;
$field_storage_uuid = 'field_storage_uuid' . $suffix;
$field = 'field' . $suffix;
$field_definition = 'field_definition' . $suffix;
$this->fieldTestData->$field_name = Unicode::strtolower($this->randomMachineName() . '_field_name' . $suffix);
$this->fieldTestData->$field_storage = entity_create('field_storage_config', array(
'field_name' => $this->fieldTestData->$field_name,
'entity_type' => $entity_type,
'type' => 'test_field',
'cardinality' => 4,
));
$this->fieldTestData->$field_storage->save();
$this->fieldTestData->$field_storage_uuid = $this->fieldTestData->$field_storage->uuid();
$this->fieldTestData->$field_definition = array(
'field_storage' => $this->fieldTestData->$field_storage,
'bundle' => $bundle,
'label' => $this->randomMachineName() . '_label',
'description' => $this->randomMachineName() . '_description',
'settings' => array(
'test_field_setting' => $this->randomMachineName(),
),
);
$this->fieldTestData->$field = entity_create('field_config', $this->fieldTestData->$field_definition);
$this->fieldTestData->$field->save();
entity_get_form_display($entity_type, $bundle, 'default')
->setComponent($this->fieldTestData->$field_name, array(
'type' => 'test_field_widget',
'settings' => array(
'test_widget_setting' => $this->randomMachineName(),
)
))
->save();
}
/**
* Saves and reloads an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to save.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity, freshly reloaded from storage.
*/
protected function entitySaveReload(EntityInterface $entity) {
$entity->save();
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
return $controller->load($entity->id());
}
/**
* Validate and save entity. Fail if violations are found.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to save.
*
* @return void
*/
protected function entityValidateAndSave(EntityInterface $entity) {
$violations = $entity->validate();
if ($violations->count()) {
$this->fail($violations);
}
else {
$entity->save();
}
}
/**
* Generate random values for a field_test field.
*
* @param $cardinality
* Number of values to generate.
* @return
* An array of random values, in the format expected for field values.
*/
protected function _generateTestFieldValues($cardinality) {
$values = array();
for ($i = 0; $i < $cardinality; $i++) {
// field_test fields treat 0 as 'empty value'.
$values[$i]['value'] = mt_rand(1, 127);
}
return $values;
}
/**
* Assert that a field has the expected values in an entity.
*
* This function only checks a single column in the field values.
*
* @param EntityInterface $entity
* The entity to test.
* @param $field_name
* The name of the field to test
* @param $expected_values
* The array of expected values.
* @param $langcode
* (Optional) The language code for the values. Defaults to
* \Drupal\Core\Language\LanguageInterface::LANGCODE_NOT_SPECIFIED.
* @param $column
* (Optional) The name of the column to check. Defaults to 'value'.
*/
protected function assertFieldValues(EntityInterface $entity, $field_name, $expected_values, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $column = 'value') {
// Re-load the entity to make sure we have the latest changes.
\Drupal::entityManager()->getStorage($entity->getEntityTypeId())->resetCache(array($entity->id()));
$e = entity_load($entity->getEntityTypeId(), $entity->id());
$field = $values = $e->getTranslation($langcode)->$field_name;
// Filter out empty values so that they don't mess with the assertions.
$field->filterEmptyItems();
$values = $field->getValue();
$this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.');
foreach ($expected_values as $key => $value) {
$this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value)));
}
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FieldValidationTest.
*/
namespace Drupal\field\Tests;
/**
* Tests field validation.
*
* @group field
*/
class FieldValidationTest extends FieldUnitTestBase {
/**
* @var string
*/
private $entityType;
/**
* @var string
*/
private $bundle;
/**
* @var \Drupal\Core\Entity\EntityInterface
*/
private $entity;
protected function setUp() {
parent::setUp();
// Create a field and storage of type 'test_field', on the 'entity_test'
// entity type.
$this->entityType = 'entity_test';
$this->bundle = 'entity_test';
$this->createFieldWithStorage('', $this->entityType, $this->bundle);
// Create an 'entity_test' entity.
$this->entity = entity_create($this->entityType, array(
'type' => $this->bundle,
));
}
/**
* Tests that the number of values is validated against the field cardinality.
*/
function testCardinalityConstraint() {
$cardinality = $this->fieldTestData->field_storage->getCardinality();
$entity = $this->entity;
for ($delta = 0; $delta < $cardinality + 1; $delta++) {
$entity->{$this->fieldTestData->field_name}[] = array('value' => 1);
}
// Validate the field.
$violations = $entity->{$this->fieldTestData->field_name}->validate();
// Check that the expected constraint violations are reported.
$this->assertEqual(count($violations), 1);
$this->assertEqual($violations[0]->getPropertyPath(), '');
$this->assertEqual($violations[0]->getMessage(), t('%name: this field cannot hold more than @count values.', array('%name' => $this->fieldTestData->field->getLabel(), '@count' => $cardinality)));
}
/**
* Tests that constraints defined by the field type are validated.
*/
function testFieldConstraints() {
$cardinality = $this->fieldTestData->field_storage->getCardinality();
$entity = $this->entity;
// The test is only valid if the field cardinality is greater than 2.
$this->assertTrue($cardinality >= 2);
// Set up values for the field.
$expected_violations = array();
for ($delta = 0; $delta < $cardinality; $delta++) {
// All deltas except '1' have incorrect values.
if ($delta == 1) {
$value = 1;
}
else {
$value = -1;
$expected_violations[$delta . '.value'][] = t('%name does not accept the value -1.', array('%name' => $this->fieldTestData->field->getLabel()));
}
$entity->{$this->fieldTestData->field_name}[] = $value;
}
// Validate the field.
$violations = $entity->{$this->fieldTestData->field_name}->validate();
// Check that the expected constraint violations are reported.
$violations_by_path = array();
foreach ($violations as $violation) {
$violations_by_path[$violation->getPropertyPath()][] = $violation->getMessage();
}
$this->assertEqual($violations_by_path, $expected_violations);
}
}

View file

@ -0,0 +1,652 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FormTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormState;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests field form handling.
*
* @group field
*/
class FormTest extends FieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'field_test', 'options', 'entity_test');
/**
* An array of values defining a field single.
*
* @var array
*/
protected $fieldStorageSingle;
/**
* An array of values defining a field multiple.
*
* @var array
*/
protected $fieldStorageMultiple;
/**
* An array of values defining a field with unlimited cardinality.
*
* @var array
*/
protected $fieldStorageUnlimited;
/**
* An array of values defining a field.
*
* @var array
*/
protected $field;
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(array('view test entity', 'administer entity_test content'));
$this->drupalLogin($web_user);
$this->fieldStorageSingle = array(
'field_name' => 'field_single',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
$this->fieldStorageMultiple = array(
'field_name' => 'field_multiple',
'entity_type' => 'entity_test',
'type' => 'test_field',
'cardinality' => 4,
);
$this->fieldStorageUnlimited = array(
'field_name' => 'field_unlimited',
'entity_type' => 'entity_test',
'type' => 'test_field',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
);
$this->field = array(
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
'description' => '[site:name]_description',
'weight' => mt_rand(0, 127),
'settings' => array(
'test_field_setting' => $this->randomMachineName(),
),
);
}
function testFieldFormSingle() {
$field_storage = $this->fieldStorageSingle;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
// Create token value expected for description.
$token_description = SafeMarkup::checkPlain($this->config('system.site')->get('name')) . '_description';
$this->assertText($token_description, 'Token replacement for description is displayed');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget is displayed');
$this->assertNoField("{$field_name}[1][value]", 'No extraneous widget is displayed');
// Check that hook_field_widget_form_alter() does not believe this is the
// default value form.
$this->assertNoText('From hook_field_widget_form_alter(): Default form is true.', 'Not default value form in hook_field_widget_form_alter().');
// Submit with invalid value (field-level validation).
$edit = array(
"{$field_name}[0][value]" => -1
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name does not accept the value -1.', array('%name' => $this->field['label'])), 'Field validation fails with invalid input.');
// TODO : check that the correct field is flagged for error.
// Create an entity
$value = mt_rand(1, 127);
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->{$field_name}->value, $value, 'Field value was saved');
// Display edit form.
$this->drupalGet('entity_test/manage/' . $id);
$this->assertFieldByName("{$field_name}[0][value]", $value, 'Widget is displayed with the correct default value');
$this->assertNoField("{$field_name}[1][value]", 'No extraneous widget is displayed');
// Update the entity.
$value = mt_rand(1, 127);
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertText(t('entity_test @id has been updated.', array('@id' => $id)), 'Entity was updated');
$this->container->get('entity.manager')->getStorage('entity_test')->resetCache(array($id));
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->{$field_name}->value, $value, 'Field value was updated');
// Empty the field.
$value = '';
$edit = array(
"{$field_name}[0][value]" => $value
);
$this->drupalPostForm('entity_test/manage/' . $id, $edit, t('Save'));
$this->assertText(t('entity_test @id has been updated.', array('@id' => $id)), 'Entity was updated');
$this->container->get('entity.manager')->getStorage('entity_test')->resetCache(array($id));
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->{$field_name}->isEmpty(), 'Field was emptied');
}
/**
* Tests field widget default values on entity forms.
*/
function testFieldFormDefaultValue() {
$field_storage = $this->fieldStorageSingle;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
$default = rand(1, 127);
$this->field['default_value'] = array(array('value' => $default));
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
// Test that the default value is displayed correctly.
$this->assertFieldByXpath("//input[@name='{$field_name}[0][value]' and @value='$default']");
// Try to submit an empty value.
$edit = array(
"{$field_name}[0][value]" => '',
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created.');
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->{$field_name}->isEmpty(), 'Field is now empty.');
}
function testFieldFormSingleRequired() {
$field_storage = $this->fieldStorageSingle;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
$this->field['required'] = TRUE;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Submit with missing required value.
$edit = array();
$this->drupalPostForm('entity_test/add', $edit, t('Save'));
$this->assertRaw(t('!name field is required.', array('!name' => $this->field['label'])), 'Required field with no value fails validation');
// Create an entity
$value = mt_rand(1, 127);
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->{$field_name}->value, $value, 'Field value was saved');
// Edit with missing required value.
$value = '';
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm('entity_test/manage/' . $id, $edit, t('Save'));
$this->assertRaw(t('!name field is required.', array('!name' => $this->field['label'])), 'Required field with no value fails validation');
}
// function testFieldFormMultiple() {
// $this->field = $this->field_multiple;
// $field_name = $this->field['field_name'];
// $this->instance['field_name'] = $field_name;
// entity_create('field_storage_config', $this->field)->save();
// entity_create('field_config', $this->instance)->save();
// }
function testFieldFormUnlimited() {
$field_storage = $this->fieldStorageUnlimited;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Display creation form -> 1 widget.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget 1 is displayed');
$this->assertNoField("{$field_name}[1][value]", 'No extraneous widget is displayed');
// Press 'add more' button -> 2 widgets.
$this->drupalPostForm(NULL, array(), t('Add another item'));
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget 1 is displayed');
$this->assertFieldByName("{$field_name}[1][value]", '', 'New widget is displayed');
$this->assertNoField("{$field_name}[2][value]", 'No extraneous widget is displayed');
// TODO : check that non-field inputs are preserved ('title'), etc.
// Yet another time so that we can play with more values -> 3 widgets.
$this->drupalPostForm(NULL, array(), t('Add another item'));
// Prepare values and weights.
$count = 3;
$delta_range = $count - 1;
$values = $weights = $pattern = $expected_values = array();
$edit = array();
for ($delta = 0; $delta <= $delta_range; $delta++) {
// Assign unique random values and weights.
do {
$value = mt_rand(1, 127);
} while (in_array($value, $values));
do {
$weight = mt_rand(-$delta_range, $delta_range);
} while (in_array($weight, $weights));
$edit["{$field_name}[$delta][value]"] = $value;
$edit["{$field_name}[$delta][_weight]"] = $weight;
// We'll need three slightly different formats to check the values.
$values[$delta] = $value;
$weights[$delta] = $weight;
$field_values[$weight]['value'] = (string) $value;
$pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*";
}
// Press 'add more' button -> 4 widgets
$this->drupalPostForm(NULL, $edit, t('Add another item'));
for ($delta = 0; $delta <= $delta_range; $delta++) {
$this->assertFieldByName("{$field_name}[$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
$this->assertFieldByName("{$field_name}[$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
}
ksort($pattern);
$pattern = implode('.*', array_values($pattern));
$this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order');
$this->assertFieldByName("{$field_name}[$delta][value]", '', "New widget is displayed");
$this->assertFieldByName("{$field_name}[$delta][_weight]", $delta, "New widget has the right weight");
$this->assertNoField("{$field_name}[" . ($delta + 1) . '][value]', 'No extraneous widget is displayed');
// Submit the form and create the entity.
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = entity_load('entity_test', $id);
ksort($field_values);
$field_values = array_values($field_values);
$this->assertIdentical($entity->{$field_name}->getValue(), $field_values, 'Field values were saved in the correct order');
// Display edit form: check that the expected number of widgets is
// displayed, with correct values change values, reorder, leave an empty
// value in the middle.
// Submit: check that the entity is updated with correct values
// Re-submit: check that the field can be emptied.
// Test with several multiple fields in a form
}
/**
* Tests the position of the required label.
*/
public function testFieldFormUnlimitedRequired() {
$field_name = $this->fieldStorageUnlimited['field_name'];
$this->field['field_name'] = $field_name;
$this->field['required'] = TRUE;
FieldStorageConfig::create($this->fieldStorageUnlimited)->save();
FieldConfig::create($this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Display creation form -> 1 widget.
$this->drupalGet('entity_test/add');
// Check that the Required symbol is present for the multifield label.
$this->assertRaw(SafeMarkup::format('<h4 class="label form-required">@label</h4>', array('@label' => $this->field['label'])),
'Required symbol added field label.');
// Check that the label of the field input is visually hidden and contains
// the field title and an indication of the delta for a11y.
$this->assertRaw(SafeMarkup::format('<label for="edit-field-unlimited-0-value" class="visually-hidden form-required">@label (value 1)</label>', array('@label' => $this->field['label'])),
'Required symbol not added for field input.');
}
/**
* Tests widget handling of multiple required radios.
*/
function testFieldFormMultivalueWithRequiredRadio() {
// Create a multivalue test field.
$field_storage = $this->fieldStorageUnlimited;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Add a required radio field.
entity_create('field_storage_config', array(
'field_name' => 'required_radio_test',
'entity_type' => 'entity_test',
'type' => 'list_string',
'settings' => array(
'allowed_values' => array('yes' => 'yes', 'no' => 'no'),
),
))->save();
$field = array(
'field_name' => 'required_radio_test',
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'required' => TRUE,
);
entity_create('field_config', $field)->save();
entity_get_form_display($field['entity_type'], $field['bundle'], 'default')
->setComponent($field['field_name'], array(
'type' => 'options_buttons',
))
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
// Press the 'Add more' button.
$this->drupalPostForm(NULL, array(), t('Add another item'));
// Verify that no error is thrown by the radio element.
$this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed.');
// Verify that the widget is added.
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget 1 is displayed');
$this->assertFieldByName("{$field_name}[1][value]", '', 'New widget is displayed');
$this->assertNoField("{$field_name}[2][value]", 'No extraneous widget is displayed');
}
function testFieldFormJSAddMore() {
$field_storage = $this->fieldStorageUnlimited;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name)
->save();
// Display creation form -> 1 widget.
$this->drupalGet('entity_test/add');
// Press 'add more' button a couple times -> 3 widgets.
// drupalPostAjaxForm() will not work iteratively, so we add those through
// non-JS submission.
$this->drupalPostForm(NULL, array(), t('Add another item'));
$this->drupalPostForm(NULL, array(), t('Add another item'));
// Prepare values and weights.
$count = 3;
$delta_range = $count - 1;
$values = $weights = $pattern = $expected_values = $edit = array();
for ($delta = 0; $delta <= $delta_range; $delta++) {
// Assign unique random values and weights.
do {
$value = mt_rand(1, 127);
} while (in_array($value, $values));
do {
$weight = mt_rand(-$delta_range, $delta_range);
} while (in_array($weight, $weights));
$edit["{$field_name}[$delta][value]"] = $value;
$edit["{$field_name}[$delta][_weight]"] = $weight;
// We'll need three slightly different formats to check the values.
$values[$delta] = $value;
$weights[$delta] = $weight;
$field_values[$weight]['value'] = (string) $value;
$pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*";
}
// Press 'add more' button through Ajax, and place the expected HTML result
// as the tested content.
$commands = $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_add_more');
$this->setRawContent($commands[2]['data']);
for ($delta = 0; $delta <= $delta_range; $delta++) {
$this->assertFieldByName("{$field_name}[$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
$this->assertFieldByName("{$field_name}[$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
}
ksort($pattern);
$pattern = implode('.*', array_values($pattern));
$this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order');
$this->assertFieldByName("{$field_name}[$delta][value]", '', "New widget is displayed");
$this->assertFieldByName("{$field_name}[$delta][_weight]", $delta, "New widget has the right weight");
$this->assertNoField("{$field_name}[" . ($delta + 1) . '][value]', 'No extraneous widget is displayed');
}
/**
* Tests widgets handling multiple values.
*/
function testFieldFormMultipleWidget() {
// Create a field with fixed cardinality, configure the form to use a
// "multiple" widget.
$field_storage = $this->fieldStorageMultiple;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($field_name, array(
'type' => 'test_field_widget_multiple',
))
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName($field_name, '', 'Widget is displayed.');
// Create entity with three values.
$edit = array(
$field_name => '1, 2, 3',
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
// Check that the values were saved.
$entity_init = entity_load('entity_test', $id);
$this->assertFieldValues($entity_init, $field_name, array(1, 2, 3));
// Display the form, check that the values are correctly filled in.
$this->drupalGet('entity_test/manage/' . $id);
$this->assertFieldByName($field_name, '1, 2, 3', 'Widget is displayed.');
// Submit the form with more values than the field accepts.
$edit = array($field_name => '1, 2, 3, 4, 5');
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw('this field cannot hold more than 4 values', 'Form validation failed.');
// Check that the field values were not submitted.
$this->assertFieldValues($entity_init, $field_name, array(1, 2, 3));
}
/**
* Tests fields with no 'edit' access.
*/
function testFieldFormAccess() {
$entity_type = 'entity_test_rev';
// Create a "regular" field.
$field_storage = $this->fieldStorageSingle;
$field_storage['entity_type'] = $entity_type;
$field_name = $field_storage['field_name'];
$field = $this->field;
$field['field_name'] = $field_name;
$field['entity_type'] = $entity_type;
$field['bundle'] = $entity_type;
entity_create('field_storage_config', $field_storage)->save();
entity_create('field_config', $field)->save();
entity_get_form_display($entity_type, $entity_type, 'default')
->setComponent($field_name)
->save();
// Create a field with no edit access. See
// field_test_entity_field_access().
$field_storage_no_access = array(
'field_name' => 'field_no_edit_access',
'entity_type' => $entity_type,
'type' => 'test_field',
);
$field_name_no_access = $field_storage_no_access['field_name'];
$field_no_access = array(
'field_name' => $field_name_no_access,
'entity_type' => $entity_type,
'bundle' => $entity_type,
'default_value' => array(0 => array('value' => 99)),
);
entity_create('field_storage_config', $field_storage_no_access)->save();
entity_create('field_config', $field_no_access)->save();
entity_get_form_display($field_no_access['entity_type'], $field_no_access['bundle'], 'default')
->setComponent($field_name_no_access)
->save();
// Test that the form structure includes full information for each delta
// apart from #access.
$entity = entity_create($entity_type, array('id' => 0, 'revision_id' => 0));
$display = entity_get_form_display($entity_type, $entity_type, 'default');
$form = array();
$form_state = new FormState();
$display->buildForm($entity, $form, $form_state);
$this->assertFalse($form[$field_name_no_access]['#access'], 'Field #access is FALSE for the field without edit access.');
// Display creation form.
$this->drupalGet($entity_type . '/add');
$this->assertNoFieldByName("{$field_name_no_access}[0][value]", '', 'Widget is not displayed if field access is denied.');
// Create entity.
$edit = array(
"{$field_name}[0][value]" => 1,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match("|$entity_type/manage/(\d+)|", $this->url, $match);
$id = $match[1];
// Check that the default value was saved.
$entity = entity_load($entity_type, $id);
$this->assertEqual($entity->$field_name_no_access->value, 99, 'Default value was saved for the field with no edit access.');
$this->assertEqual($entity->$field_name->value, 1, 'Entered value vas saved for the field with edit access.');
// Create a new revision.
$edit = array(
"{$field_name}[0][value]" => 2,
'revision' => TRUE,
);
$this->drupalPostForm($entity_type . '/manage/' . $id, $edit, t('Save'));
// Check that the new revision has the expected values.
$this->container->get('entity.manager')->getStorage($entity_type)->resetCache(array($id));
$entity = entity_load($entity_type, $id);
$this->assertEqual($entity->$field_name_no_access->value, 99, 'New revision has the expected value for the field with no edit access.');
$this->assertEqual($entity->$field_name->value, 2, 'New revision has the expected value for the field with edit access.');
// Check that the revision is also saved in the revisions table.
// $entity = entity_revision_load($entity_type, $entity->getRevisionId());
$this->assertEqual($entity->$field_name_no_access->value, 99, 'New revision has the expected value for the field with no edit access.');
$this->assertEqual($entity->$field_name->value, 2, 'New revision has the expected value for the field with edit access.');
}
/**
* Tests hiding a field in a form.
*/
function testHiddenField() {
$entity_type = 'entity_test_rev';
$field_storage = $this->fieldStorageSingle;
$field_storage['entity_type'] = $entity_type;
$field_name = $field_storage['field_name'];
$this->field['field_name'] = $field_name;
$this->field['default_value'] = array(0 => array('value' => 99));
$this->field['entity_type'] = $entity_type;
$this->field['bundle'] = $entity_type;
entity_create('field_storage_config', $field_storage)->save();
$this->field = entity_create('field_config', $this->field);
$this->field->save();
// We explicitly do not assign a widget in a form display, so the field
// stays hidden in forms.
// Display the entity creation form.
$this->drupalGet($entity_type . '/add');
// Create an entity and test that the default value is assigned correctly to
// the field that uses the hidden widget.
$this->assertNoField("{$field_name}[0][value]", 'The field does not appear in the form');
$this->drupalPostForm(NULL, array(), t('Save'));
preg_match('|' . $entity_type . '/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test_rev @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = entity_load($entity_type, $id);
$this->assertEqual($entity->{$field_name}->value, 99, 'Default value was saved');
// Update the field to remove the default value, and switch to the default
// widget.
$this->field->default_value = array();
$this->field->save();
entity_get_form_display($entity_type, $this->field->getTargetBundle(), 'default')
->setComponent($this->field->getName(), array(
'type' => 'test_field_widget',
))
->save();
// Display edit form.
$this->drupalGet($entity_type . '/manage/' . $id);
$this->assertFieldByName("{$field_name}[0][value]", 99, 'Widget is displayed with the correct default value');
// Update the entity.
$value = mt_rand(1, 127);
$edit = array("{$field_name}[0][value]" => $value);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertText(t('entity_test_rev @id has been updated.', array('@id' => $id)), 'Entity was updated');
\Drupal::entityManager()->getStorage($entity_type)->resetCache(array($id));
$entity = entity_load($entity_type, $id);
$this->assertEqual($entity->{$field_name}->value, $value, 'Field value was updated');
// Set the field back to hidden.
entity_get_form_display($entity_type, $this->field->getTargetBundle(), 'default')
->removeComponent($this->field->getName())
->save();
// Create a new revision.
$edit = array('revision' => TRUE);
$this->drupalPostForm($entity_type . '/manage/' . $id, $edit, t('Save'));
// Check that the expected value has been carried over to the new revision.
\Drupal::entityManager()->getStorage($entity_type)->resetCache(array($id));
$entity = entity_load($entity_type, $id);
$this->assertEqual($entity->{$field_name}->value, $value, 'New revision has the expected value for the field with the Hidden widget');
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\FormatterPluginManagerTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FormatterPluginManager;
/**
* Tests the field formatter plugin manager.
*
* @group field
*/
class FormatterPluginManagerTest extends FieldUnitTestBase {
/**
* Tests that getInstance falls back on default if current is not applicable.
*
* @see \Drupal\field\Tests\WidgetPluginManagerTest::testNotApplicableFallback()
*/
public function testNotApplicableFallback() {
/** @var FormatterPluginManager $formatter_plugin_manager */
$formatter_plugin_manager = \Drupal::service('plugin.manager.field.formatter');
$base_field_definition = BaseFieldDefinition::create('test_field')
// Set a name that will make isApplicable() return TRUE.
->setName('field_test_field');
$formatter_options = array(
'field_definition' => $base_field_definition,
'view_mode' => 'default',
'configuration' => array(
'type' => 'field_test_applicable',
),
);
$instance = $formatter_plugin_manager->getInstance($formatter_options);
$this->assertEqual($instance->getPluginId(), 'field_test_applicable');
// Now set name to something that makes isApplicable() return FALSE.
$base_field_definition->setName('deny_applicable');
$instance = $formatter_plugin_manager->getInstance($formatter_options);
// Instance should be default widget.
$this->assertNotEqual($instance->getPluginId(), 'field_test_applicable');
$this->assertEqual($instance->getPluginId(), 'field_test_default');
}
}

View file

@ -0,0 +1,168 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\NestedFormTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Tests field elements in nested forms.
*
* @group field
*/
class NestedFormTest extends FieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field_test', 'entity_test');
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(array('view test entity', 'administer entity_test content'));
$this->drupalLogin($web_user);
$this->fieldStorageSingle = array(
'field_name' => 'field_single',
'entity_type' => 'entity_test',
'type' => 'test_field',
);
$this->fieldStorageUnlimited = array(
'field_name' => 'field_unlimited',
'entity_type' => 'entity_test',
'type' => 'test_field',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
);
$this->field = array(
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
'description' => '[site:name]_description',
'weight' => mt_rand(0, 127),
'settings' => array(
'test_field_setting' => $this->randomMachineName(),
),
);
}
/**
* Tests Field API form integration within a subform.
*/
function testNestedFieldForm() {
// Add two fields on the 'entity_test'
entity_create('field_storage_config', $this->fieldStorageSingle)->save();
entity_create('field_storage_config', $this->fieldStorageUnlimited)->save();
$this->field['field_name'] = 'field_single';
$this->field['label'] = 'Single field';
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($this->field['field_name'])
->save();
$this->field['field_name'] = 'field_unlimited';
$this->field['label'] = 'Unlimited field';
entity_create('field_config', $this->field)->save();
entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
->setComponent($this->field['field_name'])
->save();
// Create two entities.
$entity_type = 'entity_test';
$entity_1 = entity_create($entity_type, array('id' => 1));
$entity_1->enforceIsNew();
$entity_1->field_single->value = 0;
$entity_1->field_unlimited->value = 1;
$entity_1->save();
$entity_2 = entity_create($entity_type, array('id' => 2));
$entity_2->enforceIsNew();
$entity_2->field_single->value = 10;
$entity_2->field_unlimited->value = 11;
$entity_2->save();
// Display the 'combined form'.
$this->drupalGet('test-entity/nested/1/2');
$this->assertFieldByName('field_single[0][value]', 0, 'Entity 1: field_single value appears correctly is the form.');
$this->assertFieldByName('field_unlimited[0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_single][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.');
// Submit the form and check that the entities are updated accordingly.
$edit = array(
'field_single[0][value]' => 1,
'field_unlimited[0][value]' => 2,
'field_unlimited[1][value]' => 3,
'entity_2[field_single][0][value]' => 11,
'entity_2[field_unlimited][0][value]' => 12,
'entity_2[field_unlimited][1][value]' => 13,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$entity_1 = entity_load($entity_type, 1);
$entity_2 = entity_load($entity_type, 2);
$this->assertFieldValues($entity_1, 'field_single', array(1));
$this->assertFieldValues($entity_1, 'field_unlimited', array(2, 3));
$this->assertFieldValues($entity_2, 'field_single', array(11));
$this->assertFieldValues($entity_2, 'field_unlimited', array(12, 13));
// Submit invalid values and check that errors are reported on the
// correct widgets.
$edit = array(
'field_unlimited[1][value]' => -1,
);
$this->drupalPostForm('test-entity/nested/1/2', $edit, t('Save'));
$this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.');
$error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-1-value'));
$this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.');
$edit = array(
'entity_2[field_unlimited][1][value]' => -1,
);
$this->drupalPostForm('test-entity/nested/1/2', $edit, t('Save'));
$this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.');
$error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-1-value'));
$this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.');
// Test that reordering works on both entities.
$edit = array(
'field_unlimited[0][_weight]' => 0,
'field_unlimited[1][_weight]' => -1,
'entity_2[field_unlimited][0][_weight]' => 0,
'entity_2[field_unlimited][1][_weight]' => -1,
);
$this->drupalPostForm('test-entity/nested/1/2', $edit, t('Save'));
$this->assertFieldValues($entity_1, 'field_unlimited', array(3, 2));
$this->assertFieldValues($entity_2, 'field_unlimited', array(13, 12));
// Test the 'add more' buttons. Only Ajax submission is tested, because
// the two 'add more' buttons present in the form have the same #value,
// which confuses drupalPostForm().
// 'Add more' button in the first entity:
$this->drupalGet('test-entity/nested/1/2');
$this->drupalPostAjaxForm(NULL, array(), 'field_unlimited_add_more');
$this->assertFieldByName('field_unlimited[0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.');
$this->assertFieldByName('field_unlimited[1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.');
$this->assertFieldByName('field_unlimited[2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.');
$this->assertFieldByName('field_unlimited[3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.');
// 'Add more' button in the first entity (changing field values):
$edit = array(
'entity_2[field_unlimited][0][value]' => 13,
'entity_2[field_unlimited][1][value]' => 14,
'entity_2[field_unlimited][2][value]' => 15,
);
$this->drupalPostAjaxForm(NULL, $edit, 'entity_2_field_unlimited_add_more');
$this->assertFieldByName('entity_2[field_unlimited][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.');
// Save the form and check values are saved correctly.
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertFieldValues($entity_1, 'field_unlimited', array(3, 2));
$this->assertFieldValues($entity_2, 'field_unlimited', array(13, 14, 15));
}
}

View file

@ -0,0 +1,530 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Number\NumberFieldTest.
*/
namespace Drupal\field\Tests\Number;
use Drupal\Component\Utility\Unicode;
use Drupal\simpletest\WebTestBase;
/**
* Tests the creation of numeric fields.
*
* @group field
*/
class NumberFieldTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'entity_test', 'field_ui');
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(array(
'view test entity',
'administer entity_test content',
'administer content types',
'administer node fields',
'administer node display',
'bypass node access',
'administer entity_test fields',
)));
}
/**
* Test decimal field.
*/
function testNumberDecimalField() {
// Create a field with settings to validate.
$field_name = Unicode::strtolower($this->randomMachineName());
entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'decimal',
'settings' => array(
'precision' => 8, 'scale' => 4,
)
))->save();
entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
))->save();
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'number',
'settings' => array(
'placeholder' => '0.00'
),
))
->save();
entity_get_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'number_decimal',
))
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget is displayed');
$this->assertRaw('placeholder="0.00"');
// Submit a signed decimal value within the allowed precision and scale.
$value = '-1234.5678';
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
$this->assertRaw($value, 'Value is displayed.');
// Try to create entries with more than one decimal separator; assert fail.
$wrong_entries = array(
'3.14.159',
'0..45469',
'..4589',
'6.459.52',
'6.3..25',
);
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $wrong_entry,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name must be a number.', array('%name' => $field_name)), 'Correctly failed to save decimal value with more than one decimal point.');
}
// Try to create entries with minus sign not in the first position.
$wrong_entries = array(
'3-3',
'4-',
'1.3-',
'1.2-4',
'-10-10',
);
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $wrong_entry,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name must be a number.', array('%name' => $field_name)), 'Correctly failed to save decimal value with minus sign in the wrong position.');
}
}
/**
* Test integer field.
*/
function testNumberIntegerField() {
$minimum = rand(-4000, -2000);
$maximum = rand(2000, 4000);
// Create a field with settings to validate.
$field_name = Unicode::strtolower($this->randomMachineName());
$storage = entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'integer',
));
$storage->save();
entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'settings' => array(
'min' => $minimum, 'max' => $maximum,
)
))->save();
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'number',
'settings' => array(
'placeholder' => '4'
),
))
->save();
entity_get_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'number_integer',
))
->save();
// Check the storage schema.
$expected = array(
'columns' => array(
'value' => array(
'type' => 'int',
'unsigned' => '',
'size' => 'normal'
),
),
'unique keys' => array(),
'indexes' => array(),
'foreign keys' => array()
);
$this->assertEqual($storage->getSchema(), $expected);
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget is displayed');
$this->assertRaw('placeholder="4"');
// Submit a valid integer
$value = rand($minimum, $maximum);
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
// Try to set a value below the minimum value
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $minimum - 1,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name must be higher than or equal to %minimum.', array('%name' => $field_name, '%minimum' => $minimum)), 'Correctly failed to save integer value less than minimum allowed value.');
// Try to set a decimal value
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => 1.5,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name is not a valid number.', array('%name' => $field_name)), 'Correctly failed to save decimal value to integer field.');
// Try to set a value above the maximum value
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $maximum + 1,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name must be lower than or equal to %maximum.', array('%name' => $field_name, '%maximum' => $maximum)), 'Correctly failed to save integer value greater than maximum allowed value.');
// Test with valid entries.
$valid_entries = array(
'-1234',
'0',
'1234',
);
foreach ($valid_entries as $valid_entry) {
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $valid_entry,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
$this->assertRaw($valid_entry, 'Value is displayed.');
}
}
/**
* Test float field.
*/
function testNumberFloatField() {
// Create a field with settings to validate.
$field_name = Unicode::strtolower($this->randomMachineName());
entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'float',
))->save();
entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
))->save();
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'number',
'settings' => array(
'placeholder' => '0.00'
),
))
->save();
entity_get_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => 'number_decimal',
))
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget is displayed');
$this->assertRaw('placeholder="0.00"');
// Submit a signed decimal value within the allowed precision and scale.
$value = '-1234.5678';
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
$this->assertRaw(round($value, 2), 'Value is displayed.');
// Try to create entries with more than one decimal separator; assert fail.
$wrong_entries = array(
'3.14.159',
'0..45469',
'..4589',
'6.459.52',
'6.3..25',
);
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $wrong_entry,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name must be a number.', array('%name' => $field_name)), 'Correctly failed to save float value with more than one decimal point.');
}
// Try to create entries with minus sign not in the first position.
$wrong_entries = array(
'3-3',
'4-',
'1.3-',
'1.2-4',
'-10-10',
);
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('entity_test/add');
$edit = array(
"{$field_name}[0][value]" => $wrong_entry,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw(t('%name must be a number.', array('%name' => $field_name)), 'Correctly failed to save float value with minus sign in the wrong position.');
}
}
/**
* Test default formatter behavior
*/
function testNumberFormatter() {
$type = Unicode::strtolower($this->randomMachineName());
$float_field = Unicode::strtolower($this->randomMachineName());
$integer_field = Unicode::strtolower($this->randomMachineName());
$thousand_separators = array('', '.', ',', ' ', chr(8201), "'");
$decimal_separators = array('.', ',');
$prefix = $this->randomMachineName();
$suffix = $this->randomMachineName();
$random_float = rand(0,pow(10,6));
$random_integer = rand(0, pow(10,6));
// Create a content type containing float and integer fields.
$this->drupalCreateContentType(array('type' => $type));
entity_create('field_storage_config', array(
'field_name' => $float_field,
'entity_type' => 'node',
'type' => 'float',
))->save();
entity_create('field_storage_config', array(
'field_name' => $integer_field,
'entity_type' => 'node',
'type' => 'integer',
))->save();
entity_create('field_config', array(
'field_name' => $float_field,
'entity_type' => 'node',
'bundle' => $type,
'settings' => array(
'prefix' => $prefix,
'suffix' => $suffix
),
))->save();
entity_create('field_config', array(
'field_name' => $integer_field,
'entity_type' => 'node',
'bundle' => $type,
'settings' => array(
'prefix' => $prefix,
'suffix' => $suffix
),
))->save();
entity_get_form_display('node', $type, 'default')
->setComponent($float_field, array(
'type' => 'number',
'settings' => array(
'placeholder' => '0.00'
),
))
->setComponent($integer_field, array(
'type' => 'number',
'settings' => array(
'placeholder' => '0.00'
),
))
->save();
entity_get_display('node', $type, 'default')
->setComponent($float_field, array(
'type' => 'number_decimal',
))
->setComponent($integer_field, array(
'type' => 'number_unformatted',
))
->save();
// Create a node to test formatters.
$node = entity_create('node', array(
'type' => $type,
'title' => $this->randomMachineName(),
$float_field => array(
'value' => $random_float,
),
$integer_field => array(
'value' => $random_integer,
),
));
$node->save();
// Go to manage display page.
$this->drupalGet("admin/structure/types/manage/$type/display");
// Configure number_decimal formatter for the 'float' field type.
$thousand_separator = $thousand_separators[array_rand($thousand_separators)];
$decimal_separator = $decimal_separators[array_rand($decimal_separators)];
$scale = rand(0, 10);
$this->drupalPostAjaxForm(NULL, array(), "${float_field}_settings_edit");
$edit = array(
"fields[${float_field}][settings_edit_form][settings][prefix_suffix]" => TRUE,
"fields[${float_field}][settings_edit_form][settings][scale]" => $scale,
"fields[${float_field}][settings_edit_form][settings][decimal_separator]" => $decimal_separator,
"fields[${float_field}][settings_edit_form][settings][thousand_separator]" => $thousand_separator,
);
$this->drupalPostAjaxForm(NULL, $edit, "${float_field}_plugin_settings_update");
$this->drupalPostForm(NULL, array(), t('Save'));
// Check number_decimal and number_unformatted formatters behavior.
$this->drupalGet('node/' . $node->id());
$float_formatted = number_format($random_float, $scale, $decimal_separator, $thousand_separator);
$this->assertRaw("$prefix$float_formatted$suffix", 'Prefix and suffix added');
$this->assertRaw((string) $random_integer);
// Configure the number_decimal formatter.
entity_get_display('node', $type, 'default')
->setComponent($integer_field, array(
'type' => 'number_integer',
))
->save();
$this->drupalGet("admin/structure/types/manage/$type/display");
$thousand_separator = $thousand_separators[array_rand($thousand_separators)];
$this->drupalPostAjaxForm(NULL, array(), "${integer_field}_settings_edit");
$edit = array(
"fields[${integer_field}][settings_edit_form][settings][prefix_suffix]" => FALSE,
"fields[${integer_field}][settings_edit_form][settings][thousand_separator]" => $thousand_separator,
);
$this->drupalPostAjaxForm(NULL, $edit, "${integer_field}_plugin_settings_update");
$this->drupalPostForm(NULL, array(), t('Save'));
// Check number_integer formatter behavior.
$this->drupalGet('node/' . $node->id());
$integer_formatted = number_format($random_integer, 0, '', $thousand_separator);
$this->assertRaw($integer_formatted, 'Random integer formatted');
}
/**
* Tests setting the minimum value of a float field through the interface.
*/
function testCreateNumberFloatField() {
// Create a float field.
$field_name = Unicode::strtolower($this->randomMachineName());
entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'float',
))->save();
$field = entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
));
$field->save();
// Set the minimum value to a float value.
$this->assertSetMinimumValue($field, 0.0001);
// Set the minimum value to an integer value.
$this->assertSetMinimumValue($field, 1);
}
/**
* Tests setting the minimum value of a decimal field through the interface.
*/
function testCreateNumberDecimalField() {
// Create a decimal field.
$field_name = Unicode::strtolower($this->randomMachineName());
entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'decimal',
))->save();
$field = entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
));
$field->save();
// Set the minimum value to a decimal value.
$this->assertSetMinimumValue($field, 0.1);
// Set the minimum value to an integer value.
$this->assertSetMinimumValue($field, 1);
}
/**
* Helper function to set the minimum value of a field.
*/
function assertSetMinimumValue($field, $minimum_value) {
$field_configuration_url = 'entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field->getName();
// Set the minimum value.
$edit = array(
'settings[min]' => $minimum_value,
);
$this->drupalPostForm($field_configuration_url, $edit, t('Save settings'));
// Check if an error message is shown.
$this->assertNoRaw(t('%name is not a valid number.', array('%name' => t('Minimum'))), 'Saved ' . gettype($minimum_value) .' value as minimal value on a ' . $field->getType() . ' field');
// Check if a success message is shown.
$this->assertRaw(t('Saved %label configuration.', array('%label' => $field->getLabel())));
// Check if the minimum value was actually set.
$this->drupalGet($field_configuration_url);
$this->assertFieldById('edit-settings-min', $minimum_value, 'Minimal ' . gettype($minimum_value) .' value was set on a ' . $field->getType() . ' field.');
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Number\NumberItemTest.
*/
namespace Drupal\field\Tests\Number;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\field\Tests\FieldUnitTestBase;
/**
* Tests the new entity API for the number field type.
*
* @group field
*/
class NumberItemTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array();
protected function setUp() {
parent::setUp();
// Create number field storages and fields for validation.
foreach (array('integer', 'float', 'decimal') as $type) {
entity_create('field_storage_config', array(
'entity_type' => 'entity_test',
'field_name' => 'field_' . $type,
'type' => $type,
))->save();
entity_create('field_config', array(
'entity_type' => 'entity_test',
'field_name' => 'field_' . $type,
'bundle' => 'entity_test',
))->save();
}
}
/**
* Tests using entity fields of the number field type.
*/
public function testNumberItem() {
// Verify entity creation.
$entity = entity_create('entity_test');
$integer = rand(0, 10);
$entity->field_integer = $integer;
$float = 3.14;
$entity->field_float = $float;
$decimal = '31.3';
$entity->field_decimal = $decimal;
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->field_integer instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_integer[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_integer->value, $integer);
$this->assertEqual($entity->field_integer[0]->value, $integer);
$this->assertTrue($entity->field_float instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_float[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_float->value, $float);
$this->assertEqual($entity->field_float[0]->value, $float);
$this->assertTrue($entity->field_decimal instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_decimal[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_decimal->value, $decimal);
$this->assertEqual($entity->field_decimal[0]->value, $decimal);
// Verify changing the number value.
$new_integer = rand(11, 20);
$new_float = rand(1001, 2000) / 100;
$new_decimal = '18.2';
$entity->field_integer->value = $new_integer;
$this->assertEqual($entity->field_integer->value, $new_integer);
$entity->field_float->value = $new_float;
$this->assertEqual($entity->field_float->value, $new_float);
$entity->field_decimal->value = $new_decimal;
$this->assertEqual($entity->field_decimal->value, $new_decimal);
// Read changed entity and assert changed values.
$entity->save();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->field_integer->value, $new_integer);
$this->assertEqual($entity->field_float->value, $new_float);
$this->assertEqual($entity->field_decimal->value, $new_decimal);
/// Test sample item generation.
$entity = entity_create('entity_test');
$entity->field_integer->generateSampleItems();
$entity->field_float->generateSampleItems();
$entity->field_decimal->generateSampleItems();
$this->entityValidateAndSave($entity);
}
}

View file

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\ShapeItemTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Tests the new entity API for the shape field type.
*
* @group field
*/
class ShapeItemTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field_test');
/**
* The name of the field to use in this test.
*
* @var string
*/
protected $fieldName = 'field_shape';
protected function setUp() {
parent::setUp();
// Create a 'shape' field and storage for validation.
entity_create('field_storage_config', array(
'field_name' => $this->fieldName,
'entity_type' => 'entity_test',
'type' => 'shape',
))->save();
entity_create('field_config', array(
'entity_type' => 'entity_test',
'field_name' => $this->fieldName,
'bundle' => 'entity_test',
))->save();
}
/**
* Tests using entity fields of the field field type.
*/
public function testShapeItem() {
// Verify entity creation.
$entity = entity_create('entity_test');
$shape = 'cube';
$color = 'blue';
$entity->{$this->fieldName}->shape = $shape;
$entity->{$this->fieldName}->color = $color;
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->{$this->fieldName} instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->{$this->fieldName}[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->{$this->fieldName}->shape, $shape);
$this->assertEqual($entity->{$this->fieldName}->color, $color);
$this->assertEqual($entity->{$this->fieldName}[0]->shape, $shape);
$this->assertEqual($entity->{$this->fieldName}[0]->color, $color);
// Verify changing the field value.
$new_shape = 'circle';
$new_color = 'red';
$entity->{$this->fieldName}->shape = $new_shape;
$entity->{$this->fieldName}->color = $new_color;
$this->assertEqual($entity->{$this->fieldName}->shape, $new_shape);
$this->assertEqual($entity->{$this->fieldName}->color, $new_color);
// Read changed entity and assert changed values.
$entity->save();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->{$this->fieldName}->shape, $new_shape);
$this->assertEqual($entity->{$this->fieldName}->color, $new_color);
}
}

View file

@ -0,0 +1,129 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\String\RawStringFormatterTest.
*/
namespace Drupal\field\Tests\String;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the raw string formatter
*
* @group field
*/
class RawStringFormatterTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field', 'text', 'entity_test', 'system', 'filter', 'user');
/**
* @var string
*/
protected $entityType;
/**
* @var string
*/
protected $bundle;
/**
* @var string
*/
protected $fieldName;
/**
* @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface
*/
protected $display;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Configure the theme system.
$this->installConfig(array('system', 'field'));
$this->installSchema('system', 'router');
\Drupal::service('router.builder')->rebuild();
$this->installEntitySchema('entity_test');
$this->entityType = 'entity_test';
$this->bundle = $this->entityType;
$this->fieldName = Unicode::strtolower($this->randomMachineName());
$field_storage = FieldStorageConfig::create(array(
'field_name' => $this->fieldName,
'entity_type' => $this->entityType,
'type' => 'string_long',
));
$field_storage->save();
$instance = FieldConfig::create(array(
'field_storage' => $field_storage,
'bundle' => $this->bundle,
'label' => $this->randomMachineName(),
));
$instance->save();
$this->display = entity_get_display($this->entityType, $this->bundle, 'default')
->setComponent($this->fieldName, array(
'type' => 'string',
'settings' => array(),
));
$this->display->save();
}
/**
* Renders fields of a given entity with a given display.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object with attached fields to render.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The display to render the fields in.
*
* @return string
* The rendered entity fields.
*/
protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) {
$content = $display->build($entity);
$content = $this->render($content);
return $content;
}
/**
* Tests string formatter output.
*/
public function testStringFormatter() {
$value = $this->randomString();
$value .= "\n\n<strong>" . $this->randomString() . '</strong>';
$value .= "\n\n" . $this->randomString();
$entity = EntityTest::create(array());
$entity->{$this->fieldName}->value = $value;
// Verify that all HTML is escaped and newlines are retained.
$this->renderEntityFields($entity, $this->display);
$this->assertNoRaw($value);
$this->assertRaw(nl2br(SafeMarkup::checkPlain($value)));
// Verify the cache tags.
$build = $entity->{$this->fieldName}->view();
$this->assertTrue(!isset($build[0]['#cache']), format_string('The string formatter has no cache tags.'));
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\String\StringFieldTest.
*/
namespace Drupal\field\Tests\String;
use Drupal\Component\Utility\Unicode;
use Drupal\simpletest\WebTestBase;
/**
* Tests the creation of string fields.
*
* @group text
*/
class StringFieldTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test');
/**
* A user without any special permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
protected function setUp() {
parent::setUp();
$this->webUser = $this->drupalCreateUser(array('view test entity', 'administer entity_test content'));
$this->drupalLogin($this->webUser);
}
// Test fields.
/**
* Test widgets.
*/
function testTextfieldWidgets() {
$this->_testTextfieldWidgets('string', 'string_textfield');
$this->_testTextfieldWidgets('string_long', 'string_textarea');
}
/**
* Helper function for testTextfieldWidgets().
*/
function _testTextfieldWidgets($field_type, $widget_type) {
// Create a field.
$field_name = Unicode::strtolower($this->randomMachineName());
$field_storage = entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => $field_type
));
$field_storage->save();
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
))->save();
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent($field_name, array(
'type' => $widget_type,
'settings' => array(
'placeholder' => 'A placeholder on ' . $widget_type,
),
))
->save();
entity_get_display('entity_test', 'entity_test', 'full')
->setComponent($field_name)
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertFieldByName("{$field_name}[0][value]", '', 'Widget is displayed');
$this->assertNoFieldByName("{$field_name}[0][format]", '1', 'Format selector is not displayed');
$this->assertRaw(format_string('placeholder="A placeholder on !widget_type"', array('!widget_type' => $widget_type)));
// Submit with some value.
$value = $this->randomMachineName();
$edit = array(
"{$field_name}[0][value]" => $value,
);
$this->drupalPostForm(NULL, $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
// Display the entity.
$entity = entity_load('entity_test', $id);
$display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$this->setRawContent(\Drupal::service('renderer')->renderRoot($content));
$this->assertText($value, 'Filtered tags are not displayed');
}
}

View file

@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\String\StringFormatterTest.
*/
namespace Drupal\field\Tests\String;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the creation of text fields.
*
* @group field
*/
class StringFormatterTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field', 'text', 'entity_test', 'system', 'filter', 'user');
/**
* @var string
*/
protected $entityType;
/**
* @var string
*/
protected $bundle;
/**
* @var string
*/
protected $fieldName;
/**
* @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface
*/
protected $display;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Configure the theme system.
$this->installConfig(array('system', 'field'));
$this->installSchema('system', 'router');
\Drupal::service('router.builder')->rebuild();
$this->installEntitySchema('entity_test_rev');
$this->entityType = 'entity_test_rev';
$this->bundle = $this->entityType;
$this->fieldName = Unicode::strtolower($this->randomMachineName());
$field_storage = FieldStorageConfig::create(array(
'field_name' => $this->fieldName,
'entity_type' => $this->entityType,
'type' => 'string',
));
$field_storage->save();
$instance = FieldConfig::create(array(
'field_storage' => $field_storage,
'bundle' => $this->bundle,
'label' => $this->randomMachineName(),
));
$instance->save();
$this->display = entity_get_display($this->entityType, $this->bundle, 'default')
->setComponent($this->fieldName, array(
'type' => 'string',
'settings' => array(),
));
$this->display->save();
}
/**
* Renders fields of a given entity with a given display.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object with attached fields to render.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The display to render the fields in.
*
* @return string
* The rendered entity fields.
*/
protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) {
$content = $display->build($entity);
$content = $this->render($content);
return $content;
}
/**
* Tests string formatter output.
*/
public function testStringFormatter() {
$value = $this->randomString();
$value .= "\n\n<strong>" . $this->randomString() . '</strong>';
$value .= "\n\n" . $this->randomString();
$entity = EntityTestRev::create(array());
$entity->{$this->fieldName}->value = $value;
// Verify that all HTML is escaped and newlines are retained.
$this->renderEntityFields($entity, $this->display);
$this->assertNoRaw($value);
$this->assertRaw(nl2br(SafeMarkup::checkPlain($value)));
// Verify the cache tags.
$build = $entity->{$this->fieldName}->view();
$this->assertTrue(!isset($build[0]['#cache']), format_string('The string formatter has no cache tags.'));
$value = $this->randomMachineName();
$entity->{$this->fieldName}->value = $value;
$entity->save();
// Set the formatter to link to the entity.
$this->display->setComponent($this->fieldName, [
'type' => 'string',
'settings' => [
'link_to_entity' => TRUE,
],
]);
$this->display->save();
$this->renderEntityFields($entity, $this->display);
$this->assertLink($value, 0);
$this->assertLinkByHref($entity->url());
// $entity->url('revision') falls back to the canonical URL if this is no
// revision.
$this->assertLinkByHref($entity->url('revision'));
// Make the entity a new revision.
$old_revision_id = $entity->getRevisionId();
$entity->setNewRevision(TRUE);
$value2 = $this->randomMachineName();
$entity->{$this->fieldName}->value = $value2;
$entity->save();
$entity_new_revision = \Drupal::entityManager()->getStorage('entity_test_rev')->loadRevision($old_revision_id);
$this->renderEntityFields($entity, $this->display);
$this->assertLink($value2, 0);
$this->assertLinkByHref($entity->url('revision'));
$this->renderEntityFields($entity_new_revision, $this->display);
$this->assertLink($value, 0);
$this->assertLinkByHref('/entity_test_rev/' . $entity_new_revision->id() . '/revision/' . $entity_new_revision->getRevisionId() . '/view');
}
}

View file

@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\String\UuidFormatterTest.
*/
namespace Drupal\field\Tests\String;
use Drupal\simpletest\KernelTestBase;
use Drupal\entity_test\Entity\EntityTest;
/**
* Tests the output of a UUID field.
*
* @group field
*/
class UuidFormatterTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['field', 'entity_test', 'system', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['system', 'field']);
$this->installSchema('system', 'router');
\Drupal::service('router.builder')->rebuild();
$this->installEntitySchema('entity_test');
}
/**
* Tests string formatter output.
*/
public function testUuidStringFormatter() {
$entity = EntityTest::create([]);
$entity->save();
$uuid_field = $entity->get('uuid');
$render_array = $uuid_field->view([]);
$this->assertIdentical($render_array[0]['#markup'], $entity->uuid(), 'The rendered UUID matches the entity UUID.');
$render_array = $uuid_field->view(['settings' => ['link_to_entity' => TRUE]]);
$this->assertIdentical($render_array[0]['#type'], 'link');
$this->assertIdentical($render_array[0]['#title'], $entity->uuid());
$this->assertIdentical($render_array[0]['#url']->toString(), $entity->url());
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\TestItemTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Tests the new entity API for the test field type.
*
* @group field
*/
class TestItemTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field_test');
/**
* The name of the field to use in this test.
*
* @var string
*/
protected $fieldName = 'field_test';
protected function setUp() {
parent::setUp();
// Create a 'test_field' field and storage for validation.
entity_create('field_storage_config', array(
'field_name' => $this->fieldName,
'entity_type' => 'entity_test',
'type' => 'test_field',
))->save();
entity_create('field_config', array(
'entity_type' => 'entity_test',
'field_name' => $this->fieldName,
'bundle' => 'entity_test',
))->save();
}
/**
* Tests using entity fields of the field field type.
*/
public function testTestItem() {
// Verify entity creation.
$entity = entity_create('entity_test');
$value = rand(1, 10);
$entity->field_test = $value;
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id);
$this->assertTrue($entity->{$this->fieldName} instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->{$this->fieldName}[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->{$this->fieldName}->value, $value);
$this->assertEqual($entity->{$this->fieldName}[0]->value, $value);
// Verify changing the field value.
$new_value = rand(1, 10);
$entity->field_test->value = $new_value;
$this->assertEqual($entity->{$this->fieldName}->value, $new_value);
// Read changed entity and assert changed values.
$entity->save();
$entity = entity_load('entity_test', $id);
$this->assertEqual($entity->{$this->fieldName}->value, $new_value);
// Test the schema for this field type.
$expected_schema = array(
'columns' => array(
'value' => array(
'type' => 'int',
'size' => 'medium',
),
),
'unique keys' => array(),
'indexes' => array(
'value' => array('value'),
),
'foreign keys' => array(),
);
$field_schema = BaseFieldDefinition::create('test_field')->getSchema();
$this->assertEqual($field_schema, $expected_schema);
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\TestItemWithDependenciesTest.
*/
namespace Drupal\field\Tests;
/**
* Tests the new entity API for the test field with dependencies type.
*
* @group field
*/
class TestItemWithDependenciesTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field_test');
/**
* The name of the field to use in this test.
*
* @var string
*/
protected $fieldName = 'field_test';
/**
* Tests that field types can add dependencies to field config entities.
*/
public function testTestItemWithDepenencies() {
// Create a 'test_field_with_dependencies' field and storage for validation.
entity_create('field_storage_config', array(
'field_name' => $this->fieldName,
'entity_type' => 'entity_test',
'type' => 'test_field_with_dependencies',
))->save();
$field = entity_create('field_config', array(
'entity_type' => 'entity_test',
'field_name' => $this->fieldName,
'bundle' => 'entity_test',
));
$field->save();
// Validate that the field configuration entity has the expected
// dependencies.
$this->assertEqual([
'content' => ['node:article:uuid'],
'config' => ['field.storage.entity_test.field_test'],
'module' => ['field_test', 'test_module']
], $field->getDependencies());
}
}

View file

@ -0,0 +1,194 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Timestamp\TimestampFormatterTest.
*/
namespace Drupal\field\Tests\Timestamp;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the timestamp formatters.
*
* @group field
*/
class TimestampFormatterTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['system', 'field', 'text', 'entity_test', 'user'];
/**
* @var string
*/
protected $entityType;
/**
* @var string
*/
protected $bundle;
/**
* @var string
*/
protected $fieldName;
/**
* @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface
*/
protected $display;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['system']);
$this->installConfig(['field']);
$this->installEntitySchema('entity_test');
$this->entityType = 'entity_test';
$this->bundle = $this->entityType;
$this->fieldName = Unicode::strtolower($this->randomMachineName());
$field_storage = FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => $this->entityType,
'type' => 'timestamp',
]);
$field_storage->save();
$instance = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $this->bundle,
'label' => $this->randomMachineName(),
]);
$instance->save();
$this->display = entity_get_display($this->entityType, $this->bundle, 'default')
->setComponent($this->fieldName, [
'type' => 'boolean',
'settings' => [],
]);
$this->display->save();
}
/**
* Renders fields of a given entity with a given display.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object with attached fields to render.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The display to render the fields in.
*
* @return string
* The rendered entity fields.
*/
protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) {
$content = $display->build($entity);
$content = $this->render($content);
return $content;
}
/**
* Tests TimestampFormatter.
*/
protected function testTimestampFormatter() {
$data = [];
// Test standard formats.
$date_formats = array_keys(\Drupal::entityManager()->getStorage('date_format')->loadMultiple());
foreach ($date_formats as $date_format) {
$data[] = ['date_format' => $date_format, 'custom_date_format' => '', 'timezone' => ''];
}
$data[] = ['date_format' => 'custom', 'custom_date_format' => 'r', 'timezone' => ''];
$data[] = ['date_format' => 'custom', 'custom_date_format' => 'e', 'timezone' => 'Asia/Tokyo'];
foreach ($data as $settings) {
list($date_format, $custom_date_format, $timezone) = array_values($settings);
if (empty($timezone)) {
$timezone = NULL;
}
$value = REQUEST_TIME - 87654321;
$expected = \Drupal::service('date.formatter')->format($value, $date_format, $custom_date_format, $timezone);
$component = $this->display->getComponent($this->fieldName);
$component['type'] = 'timestamp';
$component['settings'] = $settings;
$this->display->setComponent($this->fieldName, $component);
$entity = EntityTest::create([]);
$entity->{$this->fieldName}->value = $value;
$this->renderEntityFields($entity, $this->display);
$this->assertRaw($expected);
}
}
/**
* Tests TimestampAgoFormatter.
*/
protected function testTimestampAgoFormatter() {
$data = [];
foreach (array(1,2,3,4,5,6) as $granularity) {
$data[] = [
'future_format' => '@interval hence',
'past_format' => '@interval ago',
'granularity' => $granularity,
];
}
foreach ($data as $settings) {
$future_format = $settings['future_format'];
$past_format = $settings['past_format'];
$granularity = $settings['granularity'];
$request_time = \Drupal::requestStack()->getCurrentRequest()->server->get('REQUEST_TIME');
// Test a timestamp in the past
$value = $request_time - 87654321;
$expected = SafeMarkup::format($past_format, ['@interval' => \Drupal::service('date.formatter')->formatTimeDiffSince($value, ['granularity' => $granularity])]);
$component = $this->display->getComponent($this->fieldName);
$component['type'] = 'timestamp_ago';
$component['settings'] = $settings;
$this->display->setComponent($this->fieldName, $component);
$entity = EntityTest::create([]);
$entity->{$this->fieldName}->value = $value;
$this->renderEntityFields($entity, $this->display);
$this->assertRaw($expected);
// Test a timestamp in the future
$value = $request_time + 87654321;
$expected = SafeMarkup::format($future_format, ['@interval' => \Drupal::service('date.formatter')->formatTimeDiffUntil($value, ['granularity' => $granularity])]);
$component = $this->display->getComponent($this->fieldName);
$component['type'] = 'timestamp_ago';
$component['settings'] = $settings;
$this->display->setComponent($this->fieldName, $component);
$entity = EntityTest::create([]);
$entity->{$this->fieldName}->value = $value;
$this->renderEntityFields($entity, $this->display);
$this->assertRaw($expected);
}
}
}

View file

@ -0,0 +1,205 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\TranslationTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests multilanguage fields logic.
*
* The following tests will check the multilanguage logic in field handling.
*
* @group field
*/
class TranslationTest extends FieldUnitTestBase {
/**
* Modules to enable.
*
* node is required because the tests alter the node entity type.
*
* @var array
*/
public static $modules = array('language', 'node');
/**
* The name of the field to use in this test.
*
* @var string
*/
protected $fieldName;
/**
* The name of the entity type to use in this test.
*
* @var string
*/
protected $entityType = 'test_entity';
/**
* An array defining the field storage to use in this test.
*
* @var array
*/
protected $fieldStorageDefinition;
/**
* An array defining the field to use in this test.
*
* @var array
*/
protected $fieldDefinition;
/**
* The field storage to use in this test.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* The field to use in this test.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(array('language'));
$this->fieldName = Unicode::strtolower($this->randomMachineName());
$this->entityType = 'entity_test';
$this->fieldStorageDefinition = array(
'field_name' => $this->fieldName,
'entity_type' => $this->entityType,
'type' => 'test_field',
'cardinality' => 4,
);
$this->fieldStorage = entity_create('field_storage_config', $this->fieldStorageDefinition);
$this->fieldStorage->save();
$this->fieldDefinition = array(
'field_storage' => $this->fieldStorage,
'bundle' => 'entity_test',
);
$this->field = entity_create('field_config', $this->fieldDefinition);
$this->field->save();
for ($i = 0; $i < 3; ++$i) {
ConfigurableLanguage::create(array(
'id' => 'l' . $i,
'label' => $this->randomString(),
))->save();
}
}
/**
* Test translatable fields storage/retrieval.
*/
function testTranslatableFieldSaveLoad() {
// Enable field translations for nodes.
field_test_entity_info_translatable('node', TRUE);
$entity_type = \Drupal::entityManager()->getDefinition('node');
$this->assertTrue($entity_type->isTranslatable(), 'Nodes are translatable.');
// Prepare the field translations.
$entity_type_id = 'entity_test';
field_test_entity_info_translatable($entity_type_id, TRUE);
$entity = entity_create($entity_type_id, array('type' => $this->field->getTargetBundle()));
$field_translations = array();
$available_langcodes = array_keys($this->container->get('language_manager')->getLanguages());
$entity->langcode->value = reset($available_langcodes);
foreach ($available_langcodes as $langcode) {
$field_translations[$langcode] = $this->_generateTestFieldValues($this->fieldStorage->getCardinality());
$entity->getTranslation($langcode)->{$this->fieldName}->setValue($field_translations[$langcode]);
}
// Save and reload the field translations.
$entity = $this->entitySaveReload($entity);
// Check if the correct values were saved/loaded.
foreach ($field_translations as $langcode => $items) {
$result = TRUE;
foreach ($items as $delta => $item) {
$result = $result && $item['value'] == $entity->getTranslation($langcode)->{$this->fieldName}[$delta]->value;
}
$this->assertTrue($result, format_string('%language translation correctly handled.', array('%language' => $langcode)));
}
// Test default values.
$field_name_default = Unicode::strtolower($this->randomMachineName() . '_field_name');
$field_storage_definition = $this->fieldStorageDefinition;
$field_storage_definition['field_name'] = $field_name_default;
$field_storage = entity_create('field_storage_config', $field_storage_definition);
$field_storage->save();
$field_definition = $this->fieldDefinition;
$field_definition['field_storage'] = $field_storage;
$field_definition['default_value'] = array(array('value' => rand(1, 127)));
$field = entity_create('field_config', $field_definition);
$field->save();
$translation_langcodes = array_slice($available_langcodes, 0, 2);
asort($translation_langcodes);
$translation_langcodes = array_values($translation_langcodes);
$values = array('type' => $field->getTargetBundle(), 'langcode' => $translation_langcodes[0]);
$entity = entity_create($entity_type_id, $values);
foreach ($translation_langcodes as $langcode) {
$values[$this->fieldName][$langcode] = $this->_generateTestFieldValues($this->fieldStorage->getCardinality());
$entity->getTranslation($langcode, FALSE)->{$this->fieldName}->setValue($values[$this->fieldName][$langcode]);
}
$field_langcodes = array_keys($entity->getTranslationLanguages());
sort($field_langcodes);
$this->assertEqual($translation_langcodes, $field_langcodes, 'Missing translations did not get a default value.');
// @todo Test every translation once the Entity Translation API allows for
// multilingual defaults.
$langcode = $entity->language()->getId();
$this->assertEqual($entity->getTranslation($langcode)->{$field_name_default}->getValue(), $field->default_value, format_string('Default value correctly populated for language %language.', array('%language' => $langcode)));
// Check that explicit empty values are not overridden with default values.
foreach (array(NULL, array()) as $empty_items) {
$values = array('type' => $field->getTargetBundle(), 'langcode' => $translation_langcodes[0]);
$entity = entity_create($entity_type_id, $values);
foreach ($translation_langcodes as $langcode) {
$values[$this->fieldName][$langcode] = $this->_generateTestFieldValues($this->fieldStorage->getCardinality());
$entity->getTranslation($langcode)->{$this->fieldName}->setValue($values[$this->fieldName][$langcode]);
$entity->getTranslation($langcode)->{$field_name_default}->setValue($empty_items);
$values[$field_name_default][$langcode] = $empty_items;
}
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$this->assertEqual($entity->getTranslation($langcode)->{$field_name_default}->getValue(), $empty_items, format_string('Empty value correctly populated for language %language.', array('%language' => $langcode)));
}
}
}
/**
* Tests field access.
*
* Regression test to verify that fieldAccess() can be called while only
* passing the required parameters.
*
* @see https://www.drupal.org/node/2404739
*/
public function testFieldAccess() {
$access_control_handler = \Drupal::entityManager()->getAccessControlHandler($this->entityType);
$this->assertTrue($access_control_handler->fieldAccess('view', $this->field));
}
}

View file

@ -0,0 +1,135 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\TranslationWebTest.
*/
namespace Drupal\field\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests multilanguage fields logic that require a full environment.
*
* @group field
*/
class TranslationWebTest extends FieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'field_test', 'entity_test');
/**
* The name of the field to use in this test.
*
* @var string
*/
protected $fieldName;
/**
* The name of the entity type to use in this test.
*
* @var string
*/
protected $entityTypeId = 'entity_test_mulrev';
/**
* The field storage to use in this test.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* The field to use in this test.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
protected function setUp() {
parent::setUp();
$this->fieldName = Unicode::strtolower($this->randomMachineName() . '_field_name');
$field_storage = array(
'field_name' => $this->fieldName,
'entity_type' => $this->entityTypeId,
'type' => 'test_field',
'cardinality' => 4,
);
entity_create('field_storage_config', $field_storage)->save();
$this->fieldStorage = FieldStorageConfig::load($this->entityTypeId . '.' . $this->fieldName);
$field = array(
'field_storage' => $this->fieldStorage,
'bundle' => $this->entityTypeId,
);
entity_create('field_config', $field)->save();
$this->field = FieldConfig::load($this->entityTypeId . '.' . $field['bundle'] . '.' . $this->fieldName);
entity_get_form_display($this->entityTypeId, $this->entityTypeId, 'default')
->setComponent($this->fieldName)
->save();
for ($i = 0; $i < 3; ++$i) {
ConfigurableLanguage::create(array(
'id' => 'l' . $i,
'label' => $this->randomString(),
))->save();
}
}
/**
* Tests field translations when creating a new revision.
*/
function testFieldFormTranslationRevisions() {
$web_user = $this->drupalCreateUser(array('view test entity', 'administer entity_test content'));
$this->drupalLogin($web_user);
// Prepare the field translations.
field_test_entity_info_translatable($this->entityTypeId, TRUE);
$entity = entity_create($this->entityTypeId);
$available_langcodes = array_flip(array_keys($this->container->get('language_manager')->getLanguages()));
$field_name = $this->fieldStorage->getName();
// Store the field translations.
ksort($available_langcodes);
$entity->langcode->value = key($available_langcodes);
foreach ($available_langcodes as $langcode => $value) {
$entity->getTranslation($langcode)->{$field_name}->value = $value + 1;
}
$entity->save();
// Create a new revision.
$edit = array(
"{$field_name}[0][value]" => $entity->{$field_name}->value,
'revision' => TRUE,
);
$this->drupalPostForm($this->entityTypeId . '/manage/' . $entity->id(), $edit, t('Save'));
// Check translation revisions.
$this->checkTranslationRevisions($entity->id(), $entity->getRevisionId(), $available_langcodes);
$this->checkTranslationRevisions($entity->id(), $entity->getRevisionId() + 1, $available_langcodes);
}
/**
* Check if the field translation attached to the entity revision identified
* by the passed arguments were correctly stored.
*/
private function checkTranslationRevisions($id, $revision_id, $available_langcodes) {
$field_name = $this->fieldStorage->getName();
$entity = entity_revision_load($this->entityTypeId, $revision_id);
foreach ($available_langcodes as $langcode => $value) {
$passed = $entity->getTranslation($langcode)->{$field_name}->value == $value + 1;
$this->assertTrue($passed, format_string('The @language translation for revision @revision was correctly stored', array('@language' => $langcode, '@revision' => $entity->getRevisionId())));
}
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Views\FieldTestBase.
*/
/**
* @TODO
* - Test on a generic entity not on a node.
*
* What has to be tested:
* - Make sure that every wanted field is added to the according entity type.
* - Make sure the joins are done correctly.
* - Use basic fields and make sure that the full wanted object is built.
* - Use relationships between different entity types, for example node and
* the node author(user).
*/
namespace Drupal\field\Tests\Views;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Tests\ViewTestData;
/**
* Provides some helper methods for testing fieldapi integration into views.
*/
abstract class FieldTestBase extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'field_test_views');
/**
* Stores the field definitions used by the test.
*
* @var array
*/
public $fieldStorages;
/**
* Stores the fields of the field storage. They have the same keys as the
* field storages.
*
* @var array
*/
public $fields;
protected function setUp() {
parent::setUp();
// Ensure the page node type exists.
entity_create('node_type', array(
'type' => 'page',
'name' => 'page',
))->save();
ViewTestData::createTestViews(get_class($this), array('field_test_views'));
}
function setUpFieldStorages($amount = 3, $type = 'string') {
// Create three fields.
$field_names = array();
for ($i = 0; $i < $amount; $i++) {
$field_names[$i] = 'field_name_' . $i;
$this->fieldStorages[$i] = entity_create('field_storage_config', array(
'field_name' => $field_names[$i],
'entity_type' => 'node',
'type' => $type,
));
$this->fieldStorages[$i]->save();
}
return $field_names;
}
function setUpFields($bundle = 'page') {
foreach ($this->fieldStorages as $key => $field_storage) {
$this->fields[$key] = entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => $bundle,
));
$this->fields[$key]->save();
}
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Views\FieldUITest.
*/
namespace Drupal\field\Tests\Views;
use Drupal\views\Views;
/**
* Tests the UI of the field field handler.
*
* @group field
* @see \Drupal\field\Plugin\views\field\Field
*/
class FieldUITest extends FieldTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view_fieldapi');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui');
/**
* A user with the 'administer views' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(array('administer views'));
$this->drupalLogin($this->account);
$this->setUpFieldStorages(1, 'text');
$this->setUpFields();
}
/**
* Tests basic field handler settings in the UI.
*/
public function testHandlerUI() {
$url = "admin/structure/views/nojs/handler/test_view_fieldapi/default/field/field_name_0";
$this->drupalGet($url);
// Tests the available formatter options.
$result = $this->xpath('//select[@id=:id]/option', array(':id' => 'edit-options-type'));
$options = array_map(function($item) {
return (string) $item->attributes()->value[0];
}, $result);
// @todo Replace this sort by assertArray once it's in.
sort($options, SORT_STRING);
$this->assertEqual($options, array('text_default', 'text_trimmed'), 'The text formatters for a simple text field appear as expected.');
$this->drupalPostForm(NULL, array('options[type]' => 'text_trimmed'), t('Apply'));
$this->drupalGet($url);
$this->assertOptionSelected('edit-options-type', 'text_trimmed');
$random_number = rand(100, 400);
$this->drupalPostForm(NULL, array('options[settings][trim_length]' => $random_number), t('Apply'));
$this->drupalGet($url);
$this->assertFieldByName('options[settings][trim_length]', $random_number, 'The formatter setting got saved.');
// Save the view and test whether the settings are saved.
$this->drupalPostForm('admin/structure/views/view/test_view_fieldapi', array(), t('Save'));
$view = Views::getView('test_view_fieldapi');
$view->initHandlers();
$this->assertEqual($view->field['field_name_0']->options['type'], 'text_trimmed');
$this->assertEqual($view->field['field_name_0']->options['settings']['trim_length'], $random_number);
// Ensure that the view depends on the field storage.
$dependencies = \Drupal::service('config.manager')->findConfigEntityDependents('config', [$this->fieldStorages[0]->getConfigDependencyName()]);
$this->assertTrue(isset($dependencies['views.view.test_view_fieldapi']), 'The view is dependent on the field storage.');
}
/**
* Tests the basic field handler form when aggregation is enabled.
*/
public function testHandlerUIAggregation() {
// Enable aggregation.
$edit = array('group_by' => '1');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view_fieldapi/default/group_by', $edit, t('Apply'));
$url = "admin/structure/views/nojs/handler/test_view_fieldapi/default/field/field_name_0";
$this->drupalGet($url);
$this->assertResponse(200);
// Test the click sort column options.
// Tests the available formatter options.
$result = $this->xpath('//select[@id=:id]/option', array(':id' => 'edit-options-click-sort-column'));
$options = array_map(function($item) {
return (string) $item->attributes()->value[0];
}, $result);
sort($options, SORT_STRING);
$this->assertEqual($options, array('format', 'value'), 'The expected sort field options were found.');
}
}

View file

@ -0,0 +1,314 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Views\HandlerFieldFieldTest.
*/
namespace Drupal\field\Tests\Views;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
/**
* Tests the field itself of the Field integration.
*
* @group field
* @TODO
* Check a entity-type with bundles
* Check a entity-type without bundles
* Check locale:disabled, locale:enabled and locale:enabled with another language
* Check revisions
*/
class HandlerFieldFieldTest extends FieldTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('node', 'field_test');
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view_fieldapi');
/**
* Test nodes.
*
* @var \Drupal\node\NodeInterface[]
*/
public $nodes;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Setup basic fields.
$this->setUpFieldStorages(3);
// Setup a field with cardinality > 1.
$this->fieldStorages[3] = entity_create('field_storage_config', array(
'field_name' => 'field_name_3',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
));
$this->fieldStorages[3]->save();
// Setup a field that will have no value.
$this->fieldStorages[4] = entity_create('field_storage_config', array(
'field_name' => 'field_name_4',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
));
$this->fieldStorages[4]->save();
// Setup a text field.
$this->fieldStorages[5] = entity_create('field_storage_config', array(
'field_name' => 'field_name_5',
'entity_type' => 'node',
'type' => 'text',
));
$this->fieldStorages[5]->save();
// Setup a text field with access control.
// @see field_test_entity_field_access()
$this->fieldStorages[6] = entity_create('field_storage_config', array(
'field_name' => 'field_no_view_access',
'entity_type' => 'node',
'type' => 'text',
));
$this->fieldStorages[6]->save();
$this->setUpFields();
// Create some nodes.
$this->nodes = array();
for ($i = 0; $i < 3; $i++) {
$edit = array('type' => 'page');
foreach (array(0, 1, 2, 5) as $key) {
$field_storage = $this->fieldStorages[$key];
$edit[$field_storage->getName()][0]['value'] = $this->randomMachineName(8);
}
// Add a hidden value for the no-view field.
$edit[$this->fieldStorages[6]->getName()][0]['value'] = 'ssh secret squirrel';
for ($j = 0; $j < 5; $j++) {
$edit[$this->fieldStorages[3]->getName()][$j]['value'] = $this->randomMachineName(8);
}
// Set this field to be empty.
$edit[$this->fieldStorages[4]->getName()] = array(array('value' => NULL));
$this->nodes[$i] = $this->drupalCreateNode($edit);
}
$this->container->get('views.views_data')->clear();
}
/**
* Sets up the testing view with random field data.
*
* @param \Drupal\views\ViewExecutable $view
* The view to add field data to.
*/
protected function prepareView(ViewExecutable $view) {
$view->storage->invalidateCaches();
$view->initDisplay();
foreach ($this->fieldStorages as $field_storage) {
$field_name = $field_storage->getName();
$view->display_handler->options['fields'][$field_name]['id'] = $field_name;
$view->display_handler->options['fields'][$field_name]['table'] = 'node__' . $field_name;
$view->display_handler->options['fields'][$field_name]['field'] = $field_name;
}
}
public function testFieldRender() {
$this->_testSimpleFieldRender();
$this->_testInaccessibleFieldRender();
$this->_testFormatterSimpleFieldRender();
$this->_testMultipleFieldRender();
}
public function _testSimpleFieldRender() {
$view = Views::getView('test_view_fieldapi');
$this->prepareView($view);
$this->executeView($view);
// Tests that the rendered fields match the actual value of the fields.
for ($i = 0; $i < 3; $i++) {
for ($key = 0; $key < 2; $key++) {
$field_name = $this->fieldStorages[$key]->getName();
$rendered_field = $view->style_plugin->getField($i, $field_name);
$expected_field = $this->nodes[$i]->$field_name->value;
$this->assertEqual($rendered_field, $expected_field);
}
}
}
public function _testInaccessibleFieldRender() {
$view = Views::getView('test_view_fieldapi');
$this->prepareView($view);
$this->executeView($view);
// Check that the field handler for the hidden field is correctly removed
// from the display.
// @see https://www.drupal.org/node/2382931
$this->assertFalse(array_key_exists('field_no_view_access', $view->field));
// Check that the access-denied field is not visible.
for ($i = 0; $i < 3; $i++) {
$field_name = $this->fieldStorages[6]->getName();
$rendered_field = $view->style_plugin->getField($i, $field_name);
$this->assertFalse($rendered_field, 'Hidden field not rendered');
}
}
/**
* Tests that fields with formatters runs as expected.
*/
public function _testFormatterSimpleFieldRender() {
$view = Views::getView('test_view_fieldapi');
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$this->fieldStorages[5]->getName()]['type'] = 'text_trimmed';
$view->displayHandlers->get('default')->options['fields'][$this->fieldStorages[5]->getName()]['settings'] = array(
'trim_length' => 3,
);
$this->executeView($view);
// Make sure that the formatter works as expected.
// @TODO: actually there should be a specific formatter.
for ($i = 0; $i < 2; $i++) {
$rendered_field = $view->style_plugin->getField($i, $this->fieldStorages[5]->getName());
$this->assertEqual(strlen(html_entity_decode($rendered_field)), 3);
}
}
public function _testMultipleFieldRender() {
$view = Views::getView('test_view_fieldapi');
$field_name = $this->fieldStorages[3]->getName();
// Test delta limit.
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3;
$this->executeView($view);
for ($i = 0; $i < 3; $i++) {
$rendered_field = $view->style_plugin->getField($i, $field_name);
$items = array();
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
$pure_items = array_splice($pure_items, 0, 3);
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited.');
}
// Test that an empty field is rendered without error.
$view->style_plugin->getField(4, $this->fieldStorages[4]->getName());
$view->destroy();
// Test delta limit + offset
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_offset'] = 1;
$this->executeView($view);
for ($i = 0; $i < 3; $i++) {
$rendered_field = $view->style_plugin->getField($i, $field_name);
$items = array();
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
$pure_items = array_splice($pure_items, 1, 3);
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited and the offset is correct.');
}
$view->destroy();
// Test delta limit + reverse.
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_offset'] = 0;
$view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_reversed'] = TRUE;
$this->executeView($view);
for ($i = 0; $i < 3; $i++) {
$rendered_field = $view->style_plugin->getField($i, $field_name);
$items = array();
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
array_splice($pure_items, 0, -3);
$pure_items = array_reverse($pure_items);
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited and they are reversed.');
}
$view->destroy();
// Test delta first last.
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 0;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_first_last'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_reversed'] = FALSE;
$this->executeView($view);
for ($i = 0; $i < 3; $i++) {
$rendered_field = $view->style_plugin->getField($i, $field_name);
$items = array();
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
$items[] = $pure_items[0]['value'];
$items[] = $pure_items[4]['value'];
$this->assertEqual($rendered_field, implode(', ', $items), 'Items are limited to first and last.');
}
$view->destroy();
// Test delta limit + custom separator.
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_first_last'] = FALSE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3;
$view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['separator'] = ':';
$this->executeView($view);
for ($i = 0; $i < 3; $i++) {
$rendered_field = $view->style_plugin->getField($i, $field_name);
$items = array();
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
$pure_items = array_splice($pure_items, 0, 3);
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(':', $items), 'The amount of items is limited and the custom separator is correct.');
}
$view->destroy();
// Test separator with HTML, ensure it is escaped.
$this->prepareView($view);
$view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE;
$view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3;
$view->displayHandlers->get('default')->options['fields'][$field_name]['separator'] = '<h2>test</h2>';
$this->executeView($view);
for ($i = 0; $i < 3; $i++) {
$rendered_field = $view->style_plugin->getField($i, $field_name);
$items = [];
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
$pure_items = array_splice($pure_items, 0, 3);
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode('<h2>test</h2>', $items), 'The custom separator is correctly escaped.');
}
$view->destroy();
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\WidgetPluginManagerTest.
*/
namespace Drupal\field\Tests;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\WidgetPluginManager;
/**
* Tests the field widget manager.
*
* @group field
*/
class WidgetPluginManagerTest extends FieldUnitTestBase {
/**
* Tests that the widget definitions alter hook works.
*/
function testWidgetDefinitionAlter() {
$widget_definition = \Drupal::service('plugin.manager.field.widget')->getDefinition('test_field_widget_multiple');
// Test if hook_field_widget_info_alter is being called.
$this->assertTrue(in_array('test_field', $widget_definition['field_types']), "The 'test_field_widget_multiple' widget is enabled for the 'test_field' field type in field_test_field_widget_info_alter().");
}
/**
* Tests that getInstance falls back on default if current is not applicable.
*
* @see \Drupal\field\Tests\FormatterPluginManagerTest::testNotApplicableFallback()
*/
public function testNotApplicableFallback() {
/** @var WidgetPluginManager $widget_plugin_manager */
$widget_plugin_manager = \Drupal::service('plugin.manager.field.widget');
$base_field_definition = BaseFieldDefinition::create('test_field')
// Set a name that will make isApplicable() return TRUE.
->setName('field_multiwidgetfield');
$widget_options = array(
'field_definition' => $base_field_definition,
'form_mode' => 'default',
'configuration' => array(
'type' => 'test_field_widget_multiple',
),
);
$instance = $widget_plugin_manager->getInstance($widget_options);
$this->assertEqual($instance->getPluginId(), 'test_field_widget_multiple');
// Now do the same but with machine name field_onewidgetfield, because that
// makes isApplicable() return FALSE.
$base_field_definition->setName('field_onewidgetfield');
$instance = $widget_plugin_manager->getInstance($widget_options);
// Instance should be default widget.
$this->assertNotEqual($instance->getPluginId(), 'test_field_widget_multiple');
$this->assertEqual($instance->getPluginId(), 'test_field_widget');
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\reEnableModuleFieldTest.
*/
namespace Drupal\field\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the behavior of a field module after being disabled and re-enabled.
*
* @group field
*/
class reEnableModuleFieldTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'field',
'node',
// We use telephone module instead of test_field because test_field is
// hidden and does not display on the admin/modules page.
'telephone'
);
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
$this->drupalLogin($this->drupalCreateUser(array(
'create article content',
'edit own article content',
)));
}
/**
* Test the behavior of a field module after being disabled and re-enabled.
*
* @see field_system_info_alter()
*/
function testReEnabledField() {
// Add a telephone field to the article content type.
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'field_telephone',
'entity_type' => 'node',
'type' => 'telephone',
));
$field_storage->save();
entity_create('field_config', array(
'field_storage' => $field_storage,
'bundle' => 'article',
'label' => 'Telephone Number',
))->save();
entity_get_form_display('node', 'article', 'default')
->setComponent('field_telephone', array(
'type' => 'telephone_default',
'settings' => array(
'placeholder' => '123-456-7890',
),
))
->save();
entity_get_display('node', 'article', 'default')
->setComponent('field_telephone', array(
'type' => 'telephone_link',
'weight' => 1,
))
->save();
// Display the article node form and verify the telephone widget is present.
$this->drupalGet('node/add/article');
$this->assertFieldByName("field_telephone[0][value]", '', 'Widget found.');
// Submit an article node with a telephone field so data exist for the
// field.
$edit = array(
'title[0][value]' => $this->randomMachineName(),
'field_telephone[0][value]' => "123456789",
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertRaw('<a href="tel:123456789">');
// Test that the module can't be uninstalled from the UI while there is data
// for it's fields.
$admin_user = $this->drupalCreateUser(array('access administration pages', 'administer modules'));
$this->drupalLogin($admin_user);
$this->drupalGet('admin/modules/uninstall');
$this->assertText('Fields type(s) in use');
$field_storage->delete();
$this->drupalGet('admin/modules/uninstall');
$this->assertText('Fields pending deletion');
$this->cronRun();
$this->assertNoText('Fields type(s) in use');
$this->assertNoText('Fields pending deletion');
}
}

View file

@ -0,0 +1,7 @@
field.formatter.settings.field_plugins_test_text_formatter:
type: field.formatter.settings.text_trimmed
label: 'Test text formatter display format settings'
field.widget.settings.field_plugins_test_text_widget:
type: field.widget.settings.text_textfield
label: 'Test text field widget settings'

View file

@ -0,0 +1,8 @@
name: 'Field Plugins Test'
type: module
description: 'Support module for the field and entity display tests.'
core: 8.x
package: Testing
version: VERSION
dependencies:
- text

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\field_plugins_test\Plugin\Field\FieldFormatter\TestTextTrimmedFormatter.
*/
namespace Drupal\field_plugins_test\Plugin\Field\FieldFormatter;
use Drupal\text\Plugin\Field\FieldFormatter\TextTrimmedFormatter;
/**
* Plugin implementation of the 'field_plugins_test_text_formatter' formatter.
*
* @FieldFormatter(
* id = "field_plugins_test_text_formatter",
* label = @Translation("Test Trimmed"),
* field_types = {
* "text",
* "text_long",
* "text_with_summary"
* },
* quickedit = {
* "editor" = "form"
* }
* )
*/
class TestTextTrimmedFormatter extends TextTrimmedFormatter {
}

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\field_plugins_test\Plugin\Field\FieldWidget\TestTextfieldWidget.
*/
namespace Drupal\field_plugins_test\Plugin\Field\FieldWidget;
use Drupal\text\Plugin\Field\FieldWidget\TextfieldWidget;
/**
* Plugin implementation of the 'field_plugins_test_text_widget' widget.
*
* @FieldWidget(
* id = "field_plugins_test_text_widget",
* label = @Translation("Test Text field"),
* field_types = {
* "text",
* "string"
* },
* )
*/
class TestTextfieldWidget extends TextfieldWidget {
}

View file

@ -0,0 +1,119 @@
field.formatter.settings.field_test_default:
type: mapping
label: 'Field test default display format settings'
mapping:
test_formatter_setting:
type: string
label: 'Test setting'
field.formatter.settings.field_test_multiple:
type: mapping
label: 'Multiple field test display format settings'
mapping:
test_formatter_setting_multiple:
type: string
label: 'Test setting'
alter:
type: boolean
label: 'Test altering'
field.formatter.settings.field_empty_setting:
type: mapping
label: 'Empty setting field display format settings'
mapping:
field_empty_setting:
type: string
label: 'Test setting'
field.formatter.settings.field_test_with_prepare_view:
type: mapping
label: 'Field prepare step display format settings'
mapping:
test_formatter_setting_additional:
type: string
label: 'Test setting'
field.widget.settings.test_field_widget:
type: mapping
label: 'Test field widget settings'
mapping:
test_widget_setting:
type: string
label: 'Test setting'
field.widget.settings.test_field_widget_multiple:
type: mapping
label: 'Test multiple field widget settings'
mapping:
test_widget_setting_multiple:
type: string
label: 'Test setting'
field.storage_settings.test_field:
type: mapping
label: 'Test field storage settings'
mapping:
test_field_storage_setting:
type: string
label: 'Test field storage setting'
changeable:
type: string
label: 'A changeable field storage setting'
unchangeable:
type: string
label: 'An unchangeable field storage setting'
config_data_from_storage_setting:
type: boolean
label: 'Test FieldItemInterface::storageSettingsToConfigData()'
field.storage_settings.test_field_with_dependencies:
type: field.storage_settings.test_field
label: 'Test field with dependencies storage settings'
field.storage_settings.hidden_test_field:
type: field.storage_settings.test_field
label: 'Hidden test field storage settings'
field.storage_settings.test_field_with_preconfigured_options:
type: field.storage_settings.test_field
label: 'Test field with preconfigured options storage settings'
field.field_settings.test_field:
type: mapping
label: 'Test field field settings'
mapping:
test_field_setting:
type: string
label: 'Test field setting'
config_data_from_field_setting:
type: boolean
label: 'Test FieldItemInterface::fieldSettingsToConfigData()'
field.field_settings.test_field_with_dependencies:
type: field.field_settings.test_field
label: 'Test field with dependencies field settings'
field.field_settings.hidden_test_field:
type: field.field_settings.test_field
label: 'Hidden test field field settings'
field.field_settings.test_field_with_preconfigured_options:
type: field.field_settings.test_field
label: 'Test field with preconfigured settings'
field.value.test_field:
type: mapping
label: 'Default value'
mapping:
value:
type: label
label: 'Value'
field.formatter.third_party.field_test:
type: mapping
label: 'Field test entity display third party setting'
mapping:
foo:
type: string
label: 'Test setting'

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Defines an entity type.
*/
/**
* Implements hook_entity_type_alter().
*/
function field_test_entity_type_alter(array &$entity_types) {
/** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
foreach (field_test_entity_info_translatable() as $entity_type => $translatable) {
$entity_types[$entity_type]->set('translatable', $translatable);
}
}
/**
* Helper function to enable entity translations.
*/
function field_test_entity_info_translatable($entity_type_id = NULL, $translatable = NULL) {
$stored_value = &drupal_static(__FUNCTION__, array());
if (isset($entity_type_id)) {
$entity_manager = \Drupal::entityManager();
$original = $entity_manager->getDefinition($entity_type_id);
$stored_value[$entity_type_id] = $translatable;
if ($translatable != $original->isTranslatable()) {
$entity_manager->clearCachedDefinitions();
$entity_type = $entity_manager->getDefinition($entity_type_id);
$entity_manager->onEntityTypeUpdate($entity_type, $original);
}
}
return $stored_value;
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Defines a field type and its formatters and widgets.
*/
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_field_widget_info_alter().
*/
function field_test_field_widget_info_alter(&$info) {
$info['test_field_widget_multiple']['field_types'][] = 'test_field';
$info['test_field_widget_multiple']['field_types'][] = 'test_field_with_preconfigured_options';
}
/**
* Implements hook_field_storage_config_update_forbid().
*/
function field_test_field_storage_config_update_forbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage) {
if ($field_storage->getType() == 'test_field' && $field_storage->getSetting('unchangeable') != $prior_field_storage->getSetting('unchangeable')) {
throw new FieldStorageDefinitionUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'");
}
}
/**
* Sample 'default value' callback.
*/
function field_test_default_value(FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
return array(array('value' => 99));
}
/**
* Implements hook_entity_field_access().
*/
function field_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
if ($field_definition->getName() == "field_no_{$operation}_access") {
return AccessResult::forbidden();
}
// Only grant view access to test_view_field fields when the user has
// 'view test_view_field content' permission.
if ($field_definition->getName() == 'test_view_field' && $operation == 'view') {
return AccessResult::forbiddenIf(!$account->hasPermission('view test_view_field content'))->cachePerPermissions();
}
return AccessResult::allowed();
}

View file

@ -0,0 +1,8 @@
name: 'Field API Test'
type: module
description: 'Support module for the Field API tests.'
core: 8.x
package: Testing
version: VERSION
dependencies:
- entity_test

View file

@ -0,0 +1,170 @@
<?php
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* @file
* Helper module for the Field API tests.
*
* The module defines
* - an entity type (field_test.entity.inc)
* - a field type and its formatters and widgets (field_test.field.inc)
* - a field storage backend (field_test.storage.inc)
*
* The main field_test.module file implements generic hooks and provides some
* test helper functions
*/
require_once __DIR__ . '/field_test.entity.inc';
require_once __DIR__ . '/field_test.field.inc';
/**
* Store and retrieve keyed data for later verification by unit tests.
*
* This function is a simple in-memory key-value store with the
* distinction that it stores all values for a given key instead of
* just the most recently set value. field_test module hooks call
* this function to record their arguments, keyed by hook name. The
* unit tests later call this function to verify that the correct
* hooks were called and were passed the correct arguments.
*
* This function ignores all calls until the first time it is called
* with $key of NULL. Each time it is called with $key of NULL, it
* erases all previously stored data from its internal cache, but also
* returns the previously stored data to the caller. A typical usage
* scenario is:
*
* @code
* // calls to field_test_memorize() here are ignored
*
* // turn on memorization
* field_test_memorize();
*
* // call some Field API functions that invoke field_test hooks
* entity_create('field_storage_config', $field_definition)->save();
*
* // retrieve and reset the memorized hook call data
* $mem = field_test_memorize();
*
* // make sure hook_field_storage_config_create() is invoked correctly
* assertEqual(count($mem['field_test_field_storage_config_create']), 1);
* assertEqual($mem['field_test_field_storage_config_create'][0], array($field));
* @endcode
*
* @param $key
* The key under which to store to $value, or NULL as described above.
* @param $value
* A value to store for $key.
* @return
* An array mapping each $key to an array of each $value passed in
* for that key.
*/
function field_test_memorize($key = NULL, $value = NULL) {
$memorize = &drupal_static(__FUNCTION__, NULL);
if (!isset($key)) {
$return = $memorize;
$memorize = array();
return $return;
}
if (is_array($memorize)) {
$memorize[$key][] = $value;
}
}
/**
* Memorize calls to field_test_field_storage_config_create().
*/
function field_test_field_storage_config_create(FieldStorageConfigInterface $field_storage) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
}
/**
* Implements hook_entity_display_build_alter().
*/
function field_test_entity_display_build_alter(&$output, $context) {
$display_options = $context['display']->getComponent('test_field');
if (isset($display_options['settings']['alter'])) {
$output['test_field'][] = array('#markup' => 'field_test_entity_display_build_alter');
}
if (isset($output['test_field'])) {
$output['test_field'][] = array('#markup' => 'entity language is ' . $context['entity']->language()->getId());
}
}
/**
* Implements hook_field_widget_form_alter().
*/
function field_test_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
$field_definition = $context['items']->getFieldDefinition();
switch ($field_definition->getName()) {
case 'alter_test_text':
drupal_set_message('Field size: ' . $context['widget']->getSetting('size'));
break;
case 'alter_test_options':
drupal_set_message('Widget type: ' . $context['widget']->getPluginId());
break;
}
// Set a message if this is for the form displayed to set default value for
// the field.
if ($context['default']) {
drupal_set_message('From hook_field_widget_form_alter(): Default form is true.');
}
}
/**
* Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'.
*
* @see \Drupal\system\Tests\Entity\EntityFieldQueryTest::testTablePrefixing()
*/
function field_test_query_efq_table_prefixing_test_alter(&$query) {
// Add an additional join onto the entity base table. This will cause an
// exception if the EFQ does not properly prefix the base table.
$query->join('entity_test','et2','%alias.id = entity_test.id');
}
/**
* Implements hook_query_TAG_alter() for tag 'efq_metadata_test'.
*
* @see \Drupal\system\Tests\Entity\EntityQueryTest::testMetaData()
*/
function field_test_query_efq_metadata_test_alter(&$query) {
global $efq_test_metadata;
$efq_test_metadata = $query->getMetadata('foo');
}
/**
* Implements hook_entity_extra_field_info_alter().
*/
function field_test_entity_extra_field_info_alter(&$info) {
// Remove all extra fields from the 'no_fields' content type;
unset($info['node']['no_fields']);
}
/**
* Implements hook_entity_bundle_field_info_alter().
*/
function field_test_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
if (($field_name = \Drupal::state()->get('field_test_set_constraint', FALSE)) && $entity_type->id() == 'entity_test' && $bundle == 'entity_test' && !empty($fields[$field_name])) {
$fields[$field_name]->setPropertyConstraints('value', [
'Range' => [
'min' => 0,
'max' => 32,
],
]);
}
if (($field_name = \Drupal::state()->get('field_test_add_constraint', FALSE)) && $entity_type->id() == 'entity_test' && $bundle == 'entity_test' && !empty($fields[$field_name])) {
$fields[$field_name]->addPropertyConstraints('value', [
'Range' => [
'min' => 0,
'max' => 32,
],
]);
}
}

View file

@ -0,0 +1,6 @@
view test_view_field content:
title: 'View test field content'
description: 'View published test_view_field content.'
administer field_test content:
title: 'Administer field_test content'
description: 'Manage field_test content'

View file

@ -0,0 +1,13 @@
field_test.entity_nested_form:
path: '/test-entity/nested/{entity_1}/{entity_2}'
defaults:
_title: 'Nested entity form'
_form: '\Drupal\field_test\Form\NestedEntityTestForm'
options:
parameters:
entity_1:
type: 'entity:entity_test'
entity_2:
type: 'entity:entity_test'
requirements:
_permission: 'administer entity_test content'

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Form\NestedEntityTestForm.
*/
namespace Drupal\field_test\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
/**
* Provides a form for field_test routes.
*/
class NestedEntityTestForm extends FormBase {
/**
* {@inheritdoc]
*/
public function getFormId() {
return 'field_test_entity_nested_form';
}
/**
* {@inheritdoc]
*/
public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity_1 = NULL, EntityInterface $entity_2 = NULL) {
// First entity.
$form_state->set('entity_1', $entity_1);
$form_display_1 = EntityFormDisplay::collectRenderDisplay($entity_1, 'default');
$form_state->set('form_display_1', $form_display_1);
$form_display_1->buildForm($entity_1, $form, $form_state);
// Second entity.
$form_state->set('entity_2', $entity_2);
$form_display_2 = EntityFormDisplay::collectRenderDisplay($entity_2, 'default');
$form_state->set('form_display_2', $form_display_2);
$form['entity_2'] = array(
'#type' => 'details',
'#title' => t('Second entity'),
'#tree' => TRUE,
'#parents' => array('entity_2'),
'#weight' => 50,
);
$form_display_2->buildForm($entity_2, $form['entity_2'], $form_state);
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#weight' => 100,
);
return $form;
}
/**
* {@inheritdoc]
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$entity_1 = $form_state->get('entity_1');
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display_1 */
$form_display_1 = $form_state->get('form_display_1');
$form_display_1->extractFormValues($entity_1, $form, $form_state);
$form_display_1->validateFormValues($entity_1, $form, $form_state);
$entity_2 = $form_state->get('entity_2');
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display_2 */
$form_display_2 = $form_state->get('form_display_2');
$form_display_2->extractFormValues($entity_2, $form['entity_2'], $form_state);
$form_display_2->validateFormValues($entity_2, $form['entity_2'], $form_state);
}
/**
* {@inheritdoc]
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\EntityInterface $entity_1 */
$entity_1 = $form_state->get('entity_1');
$entity_1->save();
/** @var \Drupal\Core\Entity\EntityInterface $entity_2 */
$entity_2 = $form_state->get('entity_2');
$entity_2->save();
drupal_set_message($this->t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->id(), '@id_2' => $entity_2->id())));
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldApplicableFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the 'field_test_applicable' formatter.
*
* It is applicable to test_field fields unless their name is 'deny_applicable'.
*
* @FieldFormatter(
* id = "field_test_applicable",
* label = @Translation("Applicable"),
* description = @Translation("Applicable formatter"),
* field_types = {
* "test_field"
* },
* weight = 15,
* )
*/
class TestFieldApplicableFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getName() != 'deny_applicable';
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
return array('#markup' => 'Nothing to see here');
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldDefaultFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'field_test_default' formatter.
*
* @FieldFormatter(
* id = "field_test_default",
* label = @Translation("Default"),
* description = @Translation("Default formatter"),
* field_types = {
* "test_field",
* "test_field_with_preconfigured_options"
* },
* weight = 1
* )
*/
class TestFieldDefaultFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'test_formatter_setting' => 'dummy test string',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['test_formatter_setting'] = array(
'#title' => t('Setting'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => $this->getSetting('test_formatter_setting'),
'#required' => TRUE,
);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$summary[] = t('@setting: @value', array('@setting' => 'test_formatter_setting', '@value' => $this->getSetting('test_formatter_setting')));
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
foreach ($items as $delta => $item) {
$elements[$delta] = array('#markup' => $this->getSetting('test_formatter_setting') . '|' . $item->value);
}
return $elements;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldEmptyFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the 'field_empty_test' formatter.
*
* @FieldFormatter(
* id = "field_empty_test",
* label = @Translation("Field empty test"),
* field_types = {
* "test_field",
* },
* weight = -5
* )
*/
class TestFieldEmptyFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'test_empty_string' => '**EMPTY FIELD**',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
if ($items->isEmpty()) {
// For fields with no value, just add the configured "empty" value.
$elements[0] = array('#markup' => $this->getSetting('test_empty_string'));
}
else {
foreach ($items as $delta => $item) {
// This formatter only needs to output raw for testing.
$elements[$delta] = array('#markup' => $item->value);
}
}
return $elements;
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldEmptySettingFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'field_empty_setting' formatter.
*
* @FieldFormatter(
* id = "field_empty_setting",
* label = @Translation("Field empty setting"),
* field_types = {
* "test_field",
* },
* weight = -1
* )
*/
class TestFieldEmptySettingFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'field_empty_setting' => '',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['field_empty_setting'] = array(
'#title' => t('Setting'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => $this->getSetting('field_empty_setting'),
'#required' => TRUE,
);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$setting = $this->getSetting('field_empty_setting');
if (!empty($setting)) {
$summary[] = t('Default empty setting now has a value.');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
if (!empty($items)) {
foreach ($items as $delta => $item) {
$elements[$delta] = array('#markup' => $this->getSetting('field_empty_setting'));
}
}
return $elements;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldMultipleFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'field_test_multiple' formatter.
*
* @FieldFormatter(
* id = "field_test_multiple",
* label = @Translation("Multiple"),
* description = @Translation("Multiple formatter"),
* field_types = {
* "test_field",
* "test_field_with_preconfigured_options"
* },
* weight = 5
* )
*/
class TestFieldMultipleFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'test_formatter_setting_multiple' => 'dummy test string',
'alter' => FALSE,
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['test_formatter_setting_multiple'] = array(
'#title' => t('Setting'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => $this->getSetting('test_formatter_setting_multiple'),
'#required' => TRUE,
);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$summary[] = t('@setting: @value', array('@setting' => 'test_formatter_setting_multiple', '@value' => $this->getSetting('test_formatter_setting_multiple')));
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
if (!empty($items)) {
$array = array();
foreach ($items as $delta => $item) {
$array[] = $delta . ':' . $item->value;
}
$elements[0] = array('#markup' => $this->getSetting('test_formatter_setting_multiple') . '|' . implode('|', $array));
}
return $elements;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
*
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldNoSettingsFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the 'field_no_settings' formatter.
*
* @FieldFormatter(
* id = "field_no_settings",
* label = @Translation("Field no settings"),
* field_types = {
* "test_field",
* },
* weight = -10
* )
*/
class TestFieldNoSettingsFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
foreach ($items as $delta => $item) {
// This formatter only needs to output raw for testing.
$elements[$delta] = array('#markup' => $item->value);
}
return $elements;
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldFormatter\TestFieldPrepareViewFormatter.
*/
namespace Drupal\field_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'field_test_with_prepare_view' formatter.
*
* @FieldFormatter(
* id = "field_test_with_prepare_view",
* label = @Translation("With prepare step"),
* description = @Translation("Tests prepareView() method"),
* field_types = {
* "test_field"
* },
* weight = 10
* )
*/
class TestFieldPrepareViewFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'test_formatter_setting_additional' => 'dummy test string',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['test_formatter_setting_additional'] = array(
'#title' => t('Setting'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => $this->getSetting('test_formatter_setting_additional'),
'#required' => TRUE,
);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$summary[] = t('@setting: @value', array('@setting' => 'test_formatter_setting_additional', '@value' => $this->getSetting('test_formatter_setting_additional')));
return $summary;
}
/**
* {@inheritdoc}
*/
public function prepareView(array $entities_items) {
foreach ($entities_items as $items) {
foreach ($items as $item) {
// Don't add anything on empty values.
if (!$item->isEmpty()) {
$item->additional_formatter_value = $item->value + 1;
}
}
}
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
foreach ($items as $delta => $item) {
$elements[$delta] = array('#markup' => $this->getSetting('test_formatter_setting_additional') . '|' . $item->value . '|' . $item->additional_formatter_value);
}
return $elements;
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldType\HiddenTestItem.
*/
namespace Drupal\field_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Defines the 'hidden_test' entity field item.
*
* @FieldType(
* id = "hidden_test_field",
* label = @Translation("Hidden from UI test field"),
* description = @Translation("Dummy hidden field type used for tests."),
* no_ui = TRUE,
* default_widget = "test_field_widget",
* default_formatter = "field_test_default"
* )
*/
class HiddenTestItem extends TestItem {
}

View file

@ -0,0 +1,176 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldType\TestItem.
*/
namespace Drupal\field_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\Field\FieldItemBase;
/**
* Defines the 'test_field' entity field item.
*
* @FieldType(
* id = "test_field",
* label = @Translation("Test field"),
* description = @Translation("Dummy field type used for tests."),
* default_widget = "test_field_widget",
* default_formatter = "field_test_default"
* )
*/
class TestItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return array(
'test_field_storage_setting' => 'dummy test string',
'changeable' => 'a changeable field storage setting',
'unchangeable' => 'an unchangeable field storage setting',
) + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return array(
'test_field_setting' => 'dummy test string',
) + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('integer')
->setLabel(t('Test integer value'))
->setRequired(TRUE);
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return array(
'columns' => array(
'value' => array(
'type' => 'int',
'size' => 'medium',
),
),
'indexes' => array(
'value' => array('value'),
),
);
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$form['test_field_storage_setting'] = array(
'#type' => 'textfield',
'#title' => t('Field test field storage setting'),
'#default_value' => $this->getSetting('test_field_storage_setting'),
'#required' => FALSE,
'#description' => t('A dummy form element to simulate field storage setting.'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$form['test_field_setting'] = array(
'#type' => 'textfield',
'#title' => t('Field test field setting'),
'#default_value' => $this->getSetting('test_field_setting'),
'#required' => FALSE,
'#description' => t('A dummy form element to simulate field setting.'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function delete() {
// Reports that delete() method is executed for testing purposes.
field_test_memorize('field_test_field_delete', array($this->getEntity()));
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints = parent::getConstraints();
$constraints[] = $constraint_manager->create('ComplexData', array(
'value' => array(
'TestField' => array(
'value' => -1,
'message' => t('%name does not accept the value @value.', array('%name' => $this->getFieldDefinition()->getLabel(), '@value' => -1)),
)
),
));
return $constraints;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->value);
}
/**
* {@inheritdoc}
*/
public static function storageSettingsToConfigData(array $settings) {
$settings['config_data_from_storage_setting'] = 'TRUE';
unset($settings['storage_setting_from_config_data']);
return $settings;
}
/**
* {@inheritdoc}
*/
public static function storageSettingsFromConfigData(array $settings) {
$settings['storage_setting_from_config_data'] = 'TRUE';
unset($settings['config_data_from_storage_setting']);
return $settings;
}
/**
* {@inheritdoc}
*/
public static function fieldSettingsToConfigData(array $settings) {
$settings['config_data_from_field_setting'] = 'TRUE';
unset($settings['field_setting_from_config_data']);
return $settings;
}
/**
* {@inheritdoc}
*/
public static function fieldSettingsFromConfigData(array $settings) {
$settings['field_setting_from_config_data'] = 'TRUE';
unset($settings['config_data_from_field_setting']);
return $settings;
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldType\TestItemWithDependencies.
*/
namespace Drupal\field_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
* Defines the 'test_field_with_dependencies' entity field item.
*
* @FieldType(
* id = "test_field_with_dependencies",
* label = @Translation("Test field with dependencies"),
* description = @Translation("Dummy field type used for tests."),
* default_widget = "test_field_widget",
* default_formatter = "field_test_default",
* config_dependencies = {
* "module" = {
* "test_module"
* }
* }
* )
*/
class TestItemWithDependencies extends TestItem {
/**
* {@inheritdoc}
*/
public static function calculateDependencies(FieldDefinitionInterface $field_definition) {
return ['content' => ['node:article:uuid']];
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldType\TestItemWithPreconfiguredOptions.
*/
namespace Drupal\field_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
/**
* Defines the 'test_field_with_preconfigured_options' entity field item.
*
* @FieldType(
* id = "test_field_with_preconfigured_options",
* label = @Translation("Test field with preconfigured options"),
* description = @Translation("Dummy field type used for tests."),
* default_widget = "test_field_widget",
* default_formatter = "field_test_default"
* )
*/
class TestItemWithPreconfiguredOptions extends TestItem implements PreconfiguredFieldUiOptionsInterface {
/**
* {@inheritdoc}
*/
public static function getPreconfiguredOptions() {
return [
'custom_options' => [
'label' => t('All custom options'),
'category' => t('Custom category'),
'field_storage_config' => [
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'settings' => [
'test_field_storage_setting' => 'preconfigured_storage_setting'
],
],
'field_config' => [
'required' => TRUE,
'settings' => [
'test_field_setting' => 'preconfigured_field_setting',
],
],
'entity_form_display' => [
'type' => 'test_field_widget_multiple',
],
'entity_view_display' => [
'type' => 'field_test_multiple',
],
],
];
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldWidget\TestFieldWidget.
*/
namespace Drupal\field_test\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'test_field_widget' widget.
*
* @FieldWidget(
* id = "test_field_widget",
* label = @Translation("Test widget"),
* field_types = {
* "test_field",
* "hidden_test_field",
* "test_field_with_preconfigured_options"
* },
* weight = -10
* )
*/
class TestFieldWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'test_widget_setting' => 'dummy test string',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['test_widget_setting'] = array(
'#type' => 'textfield',
'#title' => t('Field test field widget setting'),
'#description' => t('A dummy form element to simulate field widget setting.'),
'#default_value' => $this->getSetting('test_widget_setting'),
'#required' => FALSE,
);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$summary[] = t('@setting: @value', array('@setting' => 'test_widget_setting', '@value' => $this->getSetting('test_widget_setting')));
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element += array(
'#type' => 'textfield',
'#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : '',
);
return array('value' => $element);
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
return $element['value'];
}
}

View file

@ -0,0 +1,109 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldWidget\TestFieldWidgetMultiple.
*/
namespace Drupal\field_test\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'test_field_widget_multiple' widget.
*
* The 'field_types' entry is left empty, and is populated through
* hook_field_widget_info_alter().
*
* @see field_test_field_widget_info_alter()
*
* @FieldWidget(
* id = "test_field_widget_multiple",
* label = @Translation("Test widget - multiple"),
* multiple_values = TRUE,
* weight = 10
* )
*/
class TestFieldWidgetMultiple extends WidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'test_widget_setting_multiple' => 'dummy test string',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['test_widget_setting_multiple'] = array(
'#type' => 'textfield',
'#title' => t('Field test field widget setting'),
'#description' => t('A dummy form element to simulate field widget setting.'),
'#default_value' => $this->getSetting('test_widget_setting_multiple'),
'#required' => FALSE,
);
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$summary[] = t('@setting: @value', array('@setting' => 'test_widget_setting_multiple', '@value' => $this->getSetting('test_widget_setting_multiple')));
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$values = array();
foreach ($items as $item) {
$values[] = $item->value;
}
$element += array(
'#type' => 'textfield',
'#default_value' => implode(', ', $values),
'#element_validate' => array(array(get_class($this), 'multipleValidate')),
);
return $element;
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
return $element;
}
/**
* Element validation helper.
*/
public static function multipleValidate($element, FormStateInterface $form_state) {
$values = array_map('trim', explode(',', $element['#value']));
$items = array();
foreach ($values as $value) {
$items[] = array('value' => $value);
}
$form_state->setValueForElement($element, $items);
}
/**
* {@inheritdoc}
* Used in \Drupal\entity_reference\Tests\EntityReferenceAdminTest::testAvailableFormatters().
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
// Returns FALSE if machine name of the field equals field_onewidgetfield.
return $field_definition->getName() != "field_onewidgetfield";
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Validation\Constraint\TestFieldConstraint.
*/
namespace Drupal\field_test\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraints\NotEqualTo;
/**
* Checks if a value is not equal.
*
* @Constraint(
* id = "TestField",
* label = @Translation("Test Field", context = "Validation"),
* type = { "integer" }
* )
*/
class TestFieldConstraint extends NotEqualTo {
/**
* {@inheritdoc}
*/
public function getRequiredOptions() {
return array('value');
}
/**
* {@inheritdoc}
*/
public function validatedBy() {
return '\Symfony\Component\Validator\Constraints\NotEqualToValidator';
}
}

View file

@ -0,0 +1,15 @@
id: entity_test.entity_test.field_test_import
langcode: en
field_name: field_test_import
entity_type: entity_test
bundle: entity_test
label: 'Test import field'
description: ''
required: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: text
dependencies:
config:
- field.storage.entity_test.field_test_import

View file

@ -0,0 +1,15 @@
id: entity_test.entity_test.field_test_import_2
langcode: en
field_name: field_test_import_2
entity_type: entity_test
bundle: entity_test
label: 'Test import field 2 on entity_test bundle'
description: ''
required: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: text
dependencies:
config:
- field.storage.entity_test.field_test_import_2

View file

@ -0,0 +1,15 @@
id: entity_test.test_bundle.field_test_import_2
langcode: en
field_name: field_test_import_2
entity_type: entity_test
bundle: test_bundle
label: 'Test import field 2 on test bundle'
description: ''
required: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: text
dependencies:
config:
- field.storage.entity_test.field_test_import_2

View file

@ -0,0 +1,19 @@
id: entity_test.field_test_import
langcode: en
field_name: field_test_import
entity_type: entity_test
type: text
settings:
max_length: 255
module: text
locked: false
cardinality: 1
translatable: false
indexes:
format:
- format
dependencies:
module:
- entity_test
- text
persist_with_no_fields: false

View file

@ -0,0 +1,19 @@
id: entity_test.field_test_import_2
langcode: en
field_name: field_test_import_2
entity_type: entity_test
type: text
settings:
max_length: 255
module: text
locked: false
cardinality: 1
translatable: false
indexes:
format:
- format
dependencies:
module:
- entity_test
- text
persist_with_no_fields: false

View file

@ -0,0 +1,6 @@
name: 'Field API configuration tests'
type: module
description: 'Support module for the Field API configuration tests.'
core: 8.x
package: Testing
version: VERSION

View file

@ -0,0 +1,16 @@
id: entity_test.entity_test.field_test_import_staging
uuid: ea711065-6940-47cd-813d-618f64095481
langcode: en
field_name: field_test_import_staging
entity_type: entity_test
bundle: entity_test
label: 'Import from staging'
description: ''
required: '0'
default_value: { }
default_value_callback: ''
settings: { }
field_type: text
dependencies:
config:
- field.storage.entity_test.field_test_import_staging

View file

@ -0,0 +1,16 @@
id: entity_test.test_bundle.field_test_import_staging_2
uuid: f07794a2-d7cc-45b6-b40d-13cf021b5552
langcode: en
field_name: field_test_import_staging_2
entity_type: entity_test
bundle: test_bundle
label: 'Test import field 2 on test bundle'
description: ''
required: '0'
default_value: { }
default_value_callback: ''
settings: { }
field_type: text
dependencies:
config:
- field.storage.entity_test.field_test_import_staging_2

Some files were not shown because too many files have changed in this diff Show more