diff --git a/core/core.services.yml b/core/core.services.yml index be51f03..306ae95 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -184,7 +184,7 @@ services: arguments: [path_alias_whitelist, '@cache.default', '@lock', '@state', '@path.alias_storage'] path.alias_manager: class: Drupal\Core\Path\AliasManager - arguments: ['@path.alias_storage', '@path.alias_whitelist', '@language_manager', '@cache.data'] + arguments: ['@path.alias_storage', '@path.alias_whitelist', '@language_manager'] http_client: class: Drupal\Core\Http\Client tags: @@ -393,6 +393,9 @@ services: arguments: ['@router.builder', '@lock'] tags: - { name: event_subscriber } + path.alias_manager.cached: + class: Drupal\Core\CacheDecorator\AliasManagerCacheDecorator + arguments: ['@path.alias_manager', '@cache.data'] path.alias_storage: class: Drupal\Core\Path\AliasStorage arguments: ['@database', '@module_handler'] @@ -579,14 +582,13 @@ services: arguments: ['@csrf_token'] maintenance_mode_subscriber: class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber - arguments: ['@state', '@config.factory', '@string_translation', '@url_generator', '@current_user'] tags: - { name: event_subscriber } path_subscriber: class: Drupal\Core\EventSubscriber\PathSubscriber tags: - { name: event_subscriber } - arguments: ['@path.alias_manager', '@path_processor_manager'] + arguments: ['@path.alias_manager.cached', '@path_processor_manager'] legacy_request_subscriber: class: Drupal\Core\EventSubscriber\LegacyRequestSubscriber tags: diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index b125ae8..50dc8f3 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -1943,8 +1943,6 @@ function _current_path($path = NULL) { * The component specified by $index, or NULL if the specified component was * not found. If called without arguments, it returns an array containing all * the components of the current path. - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. */ function arg($index = NULL, $path = NULL) { // Even though $arguments doesn't need to be resettable for any functional diff --git a/core/includes/common.inc b/core/includes/common.inc index 349449a..65aef09 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -887,7 +887,7 @@ function l($text, $path, array $options = array()) { // Add a "data-drupal-link-system-path" attribute to let the // drupal.active-link library know the path in a standardized manner. if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) { - $variables['options']['attributes']['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager')->getPathByAlias($path); + $variables['options']['attributes']['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getPathByAlias($path); } } diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 46de223..27fb20c 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -103,6 +103,26 @@ */ /** + * @defgroup menu_status_codes Menu status codes + * @{ + * Status codes for menu callbacks. + */ + +/** + * Internal menu status code -- Menu item inaccessible because site is offline. + */ +const MENU_SITE_OFFLINE = 4; + +/** + * Internal menu status code -- Everything is working fine. + */ +const MENU_SITE_ONLINE = 5; + +/** + * @} End of "defgroup menu_status_codes". + */ + +/** * @defgroup menu_tree_parameters Menu tree parameters * @{ * Parameters for a menu tree. @@ -945,6 +965,47 @@ function _menu_set_expanded_menus() { \Drupal::state()->set('menu_expanded', $names); } + +/** + * Checks whether the site is in maintenance mode. + * + * This function will log the current user out and redirect to front page + * if the current user has no 'access site in maintenance mode' permission. + * + * @param $check_only + * If this is set to TRUE, the function will perform the access checks and + * return the site offline status, but not log the user out or display any + * messages. + * + * @return + * FALSE if the site is not in maintenance mode, the user login page is + * displayed, or the user has the 'access site in maintenance mode' + * permission. TRUE for anonymous users not being on the login page when the + * site is in maintenance mode. + */ +function _menu_site_is_offline($check_only = FALSE) { + // Check if site is in maintenance mode. + if (\Drupal::state()->get('system.maintenance_mode')) { + if (user_access('access site in maintenance mode')) { + // Ensure that the maintenance mode message is displayed only once + // (allowing for page redirects) and specifically suppress its display on + // the maintenance mode settings page. + if (!$check_only && current_path() != 'admin/config/development/maintenance') { + if (user_access('administer site configuration')) { + drupal_set_message(t('Operating in maintenance mode. Go online.', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE); + } + else { + drupal_set_message(t('Operating in maintenance mode.'), 'status', FALSE); + } + } + } + else { + return TRUE; + } + } + return FALSE; +} + /** * @} End of "defgroup menu". */ diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 2b06429..e44f13d 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1233,7 +1233,7 @@ function template_preprocess_links(&$variables) { // Add a "data-drupal-link-system-path" attribute to let the // drupal.active-link library know the path in a standardized manner. - $li_attributes['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager')->getPathByAlias($path); + $li_attributes['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getPathByAlias($path); } $item['link'] = $link_element; @@ -1984,9 +1984,8 @@ function template_preprocess_html(&$variables) { $body_classes[] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; $variables['attributes']['class'] = $body_classes; - $path_args = explode('/', current_path()); // Populate the body classes. - if ($suggestions = theme_get_suggestions($path_args, 'page', '-')) { + if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { foreach ($suggestions as $suggestion) { if ($suggestion != 'page-front') { // Add current suggestion to page classes to make it possible to theme @@ -2068,6 +2067,9 @@ function template_preprocess_html(&$variables) { * inside "modules/system/page.html.twig". Look in there for the full list of * variables. * + * Uses the arg() function to generate a series of page template suggestions + * based on the current path. + * * @see drupal_render_page() */ function template_preprocess_page(&$variables) { @@ -2184,7 +2186,7 @@ function template_preprocess_page(&$variables) { * additional suggestions or classes on the path of the current page. * * @param $args - * An array of path arguments. + * An array of path arguments, such as from function arg(). * @param $base * A string identifying the base 'thing' from which more specific suggestions * are derived. For example, 'page' or 'html'. diff --git a/core/lib/Drupal/Core/CacheDecorator/AliasManagerCacheDecorator.php b/core/lib/Drupal/Core/CacheDecorator/AliasManagerCacheDecorator.php new file mode 100644 index 0000000..8bcbc08 --- /dev/null +++ b/core/lib/Drupal/Core/CacheDecorator/AliasManagerCacheDecorator.php @@ -0,0 +1,128 @@ +aliasManager = $alias_manager; + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public function setCacheKey($key) { + $this->cacheKey = $key; + } + + /** + * {@inheritdoc} + * + * Cache an array of the system paths available on each page. We assume + * that aliases will be needed for the majority of these paths during + * subsequent requests, and load them in a single query during path alias + * lookup. + */ + public function writeCache() { + $path_lookups = $this->getPathLookups(); + // Check if the system paths for this page were loaded from cache in this + // request to avoid writing to cache on every request. + if ($this->cacheNeedsWriting && !empty($path_lookups) && !empty($this->cacheKey)) { + // Set the path cache to expire in 24 hours. + $expire = REQUEST_TIME + (60 * 60 * 24); + $this->cache->set($this->cacheKey, $path_lookups, $expire); + } + } + + /** + * {@inheritdoc} + */ + public function getPathByAlias($alias, $langcode = NULL) { + $path = $this->aliasManager->getPathByAlias($alias, $langcode); + // We need to pass on the list of previously cached system paths for this + // key to the alias manager for use in subsequent lookups. + $cached = $this->cache->get($path); + $cached_paths = array(); + if ($cached) { + $cached_paths = $cached->data; + $this->cacheNeedsWriting = FALSE; + } + $this->preloadPathLookups($cached_paths); + return $path; + } + + /** + * {@inheritdoc} + */ + public function getAliasByPath($path, $langcode = NULL) { + return $this->aliasManager->getAliasByPath($path, $langcode); + } + + /** + * {@inheritdoc} + */ + public function getPathLookups() { + return $this->aliasManager->getPathLookups(); + } + + /** + * {@inheritdoc} + */ + public function preloadPathLookups(array $path_list) { + $this->aliasManager->preloadPathLookups($path_list); + } + + /** + * {@inheritdoc} + */ + public function cacheClear($source = NULL) { + $this->cache->delete($this->cacheKey); + $this->aliasManager->cacheClear($source); + } +} diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index 2214894..a650fce 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -7,14 +7,19 @@ namespace Drupal\Core\Entity; +use Drupal\Component\Utility\String; use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Database; use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; +use Drupal\Core\Entity\Sql\DefaultTableMapping; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\Language; use Drupal\field\FieldConfigUpdateForbiddenException; -use Drupal\field\FieldConfigInterface; -use Drupal\field\FieldInstanceConfigInterface; use Drupal\field\Entity\FieldConfig; +use Drupal\field\FieldInstanceConfigInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -24,8 +29,28 @@ * * This class can be used as-is by most simple entity types. Entity types * requiring special handling can extend the class. + * + * The class uses \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler + * internally in order to automatically generate the database schema based on + * the defined base fields. Entity types can override + * ContentEntityDatabaseStorage::getSchema() to customize the generated + * schema; e.g., to add additional indexes. */ -class ContentEntityDatabaseStorage extends ContentEntityStorageBase { +class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface { + + /** + * The storage field definitions for this entity type. + * + * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] + */ + protected $fieldStorageDefinitions; + + /** + * The mapping of field columns to SQL tables. + * + * @var \Drupal\Core\Entity\Sql\TableMappingInterface + */ + protected $tableMapping; /** * Name of entity's revision database table field, if it supports revisions. @@ -37,6 +62,20 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase { protected $revisionKey = FALSE; /** + * The entity langcode key. + * + * @var string|bool + */ + protected $langcodeKey = FALSE; + + /** + * The base table of the entity. + * + * @var string + */ + protected $baseTable; + + /** * The table that stores revisions, if the entity supports revisions. * * @var string @@ -72,6 +111,13 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase { protected $entityManager; /** + * The entity schema handler. + * + * @var \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface + */ + protected $schemaHandler; + + /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { @@ -97,24 +143,193 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa $this->database = $database; $this->entityManager = $entity_manager; + $this->fieldStorageDefinitions = $entity_manager->getBaseFieldDefinitions($entity_type->id()); - // Check if the entity type supports UUIDs. - $this->uuidKey = $this->entityType->getKey('uuid'); + // @todo Remove table names from the entity type definition in + // https://drupal.org/node/2232465 + $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId; - if ($this->entityType->isRevisionable()) { - $this->revisionKey = $this->entityType->getKey('revision'); - $this->revisionTable = $this->entityType->getRevisionTable(); + $revisionable = $this->entityType->isRevisionable(); + if ($revisionable) { + $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id'; + $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision'; } + // @todo Remove the data table check once all entity types are using + // entity query and we have a views data controller. See: + // - https://drupal.org/node/2068325 + // - https://drupal.org/node/1740492 + $translatable = $this->entityType->isTranslatable() && $this->entityType->getDataTable(); + if ($translatable) { + $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data'; + $this->langcodeKey = $this->entityType->getKey('langcode') ?: 'langcode'; + $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode') ?: 'default_langcode'; + } + if ($revisionable && $translatable) { + $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision'; + } + } + + /** + * Returns the base table name. + * + * @return string + * The table name. + */ + public function getBaseTable() { + return $this->baseTable; + } + + /** + * Returns the revision table name. + * + * @return string|false + * The table name or FALSE if it is not available. + */ + public function getRevisionTable() { + return $this->revisionTable; + } + + /** + * Returns the data table name. + * + * @return string|false + * The table name or FALSE if it is not available. + */ + public function getDataTable() { + return $this->dataTable; + } + + /** + * Returns the revision data table name. + * + * @return string|false + * The table name or FALSE if it is not available. + */ + public function getRevisionDataTable() { + return $this->revisionDataTable; + } + + /** + * {@inheritdoc} + */ + public function getSchema() { + return $this->schemaHandler()->getSchema(); + } - // Check if the entity type has a dedicated table for fields. - if ($data_table = $this->entityType->getDataTable()) { - $this->dataTable = $data_table; - // Entity types having both revision and translation support should always - // define a revision data table. - if ($this->revisionTable && $revision_data_table = $this->entityType->getRevisionDataTable()) { - $this->revisionDataTable = $revision_data_table; + /** + * Gets the schema handler for this storage controller. + * + * @return \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler + * The schema handler. + */ + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this); + } + return $this->schemaHandler; + } + + /** + * {@inheritdoc} + */ + public function getTableMapping() { + if (!isset($this->tableMapping)) { + + $definitions = array_filter($this->fieldStorageDefinitions, function (FieldDefinitionInterface $definition) { + // @todo Remove the check for FieldDefinitionInterface::isMultiple() when + // multiple-value base fields are supported in + // https://drupal.org/node/2248977. + return !$definition->isComputed() && !$definition->hasCustomStorage() && !$definition->isMultiple(); + }); + $this->tableMapping = new DefaultTableMapping($definitions); + + $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey))); + $all_fields = array_keys($definitions); + $revisionable_fields = array_keys(array_filter($definitions, function (FieldStorageDefinitionInterface $definition) { + return $definition->isRevisionable(); + })); + // Make sure the key fields come first in the list of fields. + $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields)); + + // Nodes have all three of these fields, while custom blocks only have + // log. + // @todo Provide automatic definitions for revision metadata fields in + // https://drupal.org/node/2248983. + // @todo Rename 'log' to 'revision_log' in + // https://drupal.org/node/2248991. + $revision_metadata_fields = array_intersect(array('revision_timestamp', 'revision_uid', 'log'), $all_fields); + + $revisionable = $this->entityType->isRevisionable(); + // @todo Remove the data table check once all entity types are using + // entity query and we have a views data controller. See: + // - https://drupal.org/node/2068325 + // - https://drupal.org/node/1740492 + $translatable = $this->entityType->getDataTable() && $this->entityType->isTranslatable(); + if (!$revisionable && !$translatable) { + // The base layout stores all the base field values in the base table. + $this->tableMapping->setFieldNames($this->baseTable, $all_fields); + } + elseif ($revisionable && !$translatable) { + // The revisionable layout stores all the base field values in the base + // table, except for revision metadata fields. Revisionable fields + // denormalized in the base table but also stored in the revision table + // together with the entity ID and the revision ID as identifiers. + $this->tableMapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields)); + $revision_key_fields = array($this->idKey, $this->revisionKey); + $this->tableMapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); + } + elseif (!$revisionable && $translatable) { + // Multilingual layouts store key field values in the base table. The + // other base field values are stored in the data table, no matter + // whether they are translatable or not. The data table holds also a + // denormalized copy of the bundle field value to allow for more + // performant queries. This means that only the UUID is not stored on + // the data table. + $this->tableMapping + ->setFieldNames($this->baseTable, $key_fields) + ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey)))) + // Add the denormalized 'default_langcode' field to the mapping. Its + // value is identical to the query expression + // "base_table.langcode = data_table.langcode" + ->setExtraColumns($this->dataTable, array('default_langcode')); + } + elseif ($revisionable && $translatable) { + // The revisionable multilingual layout stores key field values in the + // base table, except for language, which is stored in the revision + // table along with revision metadata. The revision data table holds + // data field values for all the revisionable fields and the data table + // holds the data field values for all non-revisionable fields. The data + // field values of revisionable fields are denormalized in the data + // table, as well. + $this->tableMapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey)))); + + // Like in the multilingual, non-revisionable case the UUID is not + // in the data table. Additionally, do not store revision metadata + // fields in the data table. + $data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields)); + $this->tableMapping + ->setFieldNames($this->dataTable, $data_fields) + // Add the denormalized 'default_langcode' field to the mapping. Its + // value is identical to the query expression + // "base_langcode = data_table.langcode" where "base_langcode" is + // the language code of the default revision. + ->setExtraColumns($this->dataTable, array('default_langcode')); + + $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields); + $this->tableMapping->setFieldNames($this->revisionTable, $revision_base_fields); + + $revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey); + $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields); + $this->tableMapping + ->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)) + // Add the denormalized 'default_langcode' field to the mapping. Its + // value is identical to the query expression + // "revision_table.langcode = data_table.langcode". + ->setExtraColumns($this->revisionDataTable, array('default_langcode')); } } + + return $this->tableMapping; } /** @@ -215,13 +430,14 @@ protected function attachPropertyData(array &$entities) { } $data = $query->execute(); - $field_definitions = \Drupal::entityManager()->getBaseFieldDefinitions($this->entityTypeId); + + $table_mapping = $this->getTableMapping(); $translations = array(); if ($this->revisionDataTable) { - $data_column_names = array_flip(array_diff(drupal_schema_fields_sql($this->entityType->getRevisionDataTable()), drupal_schema_fields_sql($this->entityType->getBaseTable()))); + $data_fields = array_diff_key($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable)); } else { - $data_column_names = array_flip(drupal_schema_fields_sql($this->entityType->getDataTable())); + $data_fields = $table_mapping->getFieldNames($this->dataTable); } foreach ($data as $values) { @@ -232,23 +448,16 @@ protected function attachPropertyData(array &$entities) { $langcode = empty($values['default_langcode']) ? $values['langcode'] : Language::LANGCODE_DEFAULT; $translations[$id][$langcode] = TRUE; - foreach (array_keys($field_definitions) as $field_name) { - // Handle columns named directly after the field. - if (isset($data_column_names[$field_name])) { - $entities[$id][$field_name][$langcode] = $values[$field_name]; + + foreach ($data_fields as $field_name) { + $columns = $table_mapping->getColumnNames($field_name); + // Do not key single-column fields by property name. + if (count($columns) == 1) { + $entities[$id][$field_name][$langcode] = $values[reset($columns)]; } else { - // @todo Change this logic to be based on a mapping of field - // definition properties (translatability, revisionability) in - // https://drupal.org/node/2144631. - foreach ($data_column_names as $data_column_name) { - // Handle columns named [field_name]__[column_name], for which we - // need to look through all column names from the table that start - // with the name of the field. - if (($data_field_name = strstr($data_column_name, '__', TRUE)) && $data_field_name === $field_name) { - $property_name = substr($data_column_name, strpos($data_column_name, '__') + 2); - $entities[$id][$field_name][$langcode][$property_name] = $values[$data_column_name]; - } + foreach ($columns as $property_name => $column_name) { + $entities[$id][$field_name][$langcode][$property_name] = $values[$column_name]; } } } @@ -352,11 +561,12 @@ protected function buildQuery($ids, $revision_id = FALSE) { } // Add fields from the {entity} table. - $entity_fields = drupal_schema_fields_sql($this->entityType->getBaseTable()); + $table_mapping = $this->getTableMapping(); + $entity_fields = $table_mapping->getAllColumns($this->baseTable); if ($this->revisionTable) { // Add all fields from the {entity_revision} table. - $entity_revision_fields = drupal_schema_fields_sql($this->entityType->getRevisionTable()); + $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable); $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields); // The ID field is provided by entity, so remove it. unset($entity_revision_fields[$this->idKey]); @@ -477,7 +687,12 @@ protected function doSave($id, EntityInterface $entity) { $is_new = $entity->isNew(); if (!$is_new) { if ($entity->isDefaultRevision()) { - $return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey); + $this->database + ->update($this->baseTable) + ->fields((array) $record) + ->condition($this->idKey, $record->{$this->idKey}) + ->execute(); + $return = SAVED_UPDATED; } else { // @todo, should a different value be returned when saving an entity @@ -485,13 +700,13 @@ protected function doSave($id, EntityInterface $entity) { $return = FALSE; } if ($this->revisionTable) { - $record->{$this->revisionKey} = $this->saveRevision($entity); + $entity->{$this->revisionKey}->value = $this->saveRevision($entity); } if ($this->dataTable) { $this->savePropertyData($entity); } if ($this->revisionDataTable) { - $this->savePropertyData($entity, 'revision_data_table'); + $this->savePropertyData($entity, $this->revisionDataTable); } if ($this->revisionTable) { $entity->setNewRevision(FALSE); @@ -502,7 +717,17 @@ protected function doSave($id, EntityInterface $entity) { // Ensure the entity is still seen as new after assigning it an id, // while storing its data. $entity->enforceIsNew(); - $return = drupal_write_record($this->entityType->getBaseTable(), $record); + $insert_id = $this->database + ->insert($this->baseTable, array('return' => Database::RETURN_INSERT_ID)) + ->fields((array) $record) + ->execute(); + // Even if this is a new entity the ID key might have been set, in which + // case we should not override the provided ID. An empty value for the + // ID is interpreted as NULL and thus overridden. + if (empty($record->{$this->idKey})) { + $record->{$this->idKey} = $insert_id; + } + $return = SAVED_NEW; $entity->{$this->idKey}->value = (string) $record->{$this->idKey}; if ($this->revisionTable) { $entity->setNewRevision(); @@ -512,8 +737,9 @@ protected function doSave($id, EntityInterface $entity) { $this->savePropertyData($entity); } if ($this->revisionDataTable) { - $this->savePropertyData($entity, 'revision_data_table'); + $this->savePropertyData($entity, $this->revisionDataTable); } + $entity->enforceIsNew(FALSE); if ($this->revisionTable) { $entity->setNewRevision(FALSE); @@ -543,13 +769,14 @@ protected function has($id, EntityInterface $entity) { * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity object. - * @param string $table_key - * (optional) The entity key identifying the target table. Defaults to - * 'data_table'. + * @param string $table_name + * (optional) The table name to save to. Defaults to the data table. */ - protected function savePropertyData(EntityInterface $entity, $table_key = 'data_table') { - $table_name = $this->entityType->get($table_key); - $revision = $table_key != 'data_table'; + protected function savePropertyData(EntityInterface $entity, $table_name = NULL) { + if (!isset($table_name)) { + $table_name = $this->dataTable; + } + $revision = $table_name != $this->dataTable; if (!$revision || !$entity->isNewRevision()) { $key = $revision ? $this->revisionKey : $this->idKey; @@ -564,7 +791,7 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_ foreach ($entity->getTranslationLanguages() as $langcode => $language) { $translation = $entity->getTranslation($langcode); - $record = $this->mapToDataStorageRecord($translation, $table_key); + $record = $this->mapToDataStorageRecord($translation, $table_name); $values = (array) $record; $query ->fields(array_keys($values)) @@ -589,63 +816,43 @@ protected function invokeHook($hook, EntityInterface $entity) { * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity object. - * @param string $table_key - * (optional) The entity key identifying the target table. Defaults to - * 'base_table'. + * @param string $table_name + * (optional) The table name to map records to. Defaults to the base table. * * @return \stdClass * The record to store. */ - protected function mapToStorageRecord(ContentEntityInterface $entity, $table_key = 'base_table') { + protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) { + if (!isset($table_name)) { + $table_name = $this->baseTable; + } + $record = new \stdClass(); - $values = array(); - $schema = drupal_get_schema($this->entityType->get($table_key)); - $is_new = $entity->isNew(); + $table_mapping = $this->getTableMapping(); + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { - $multi_column_fields = array(); - foreach (drupal_schema_fields_sql($this->entityType->get($table_key)) as $name) { - // Check for fields which store data in multiple columns and process them - // separately. - if ($field = strstr($name, '__', TRUE)) { - $multi_column_fields[$field] = TRUE; - continue; + if (empty($this->fieldStorageDefinitions[$field_name])) { + throw new EntityStorageException(String::format('Table mapping contains invalid field %field.', array('%field' => $field_name))); } - $values[$name] = NULL; - if ($entity->hasField($name)) { - // Only the first field item is stored. - $field_item = $entity->get($name)->first(); - $main_property = $entity->getFieldDefinition($name)->getMainPropertyName(); - if ($main_property && isset($field_item->$main_property)) { - // If the field has a main property, store the value of that. - $values[$name] = $field_item->$main_property; + $definition = $this->fieldStorageDefinitions[$field_name]; + $columns = $table_mapping->getColumnNames($field_name); + + foreach ($columns as $column_name => $schema_name) { + // If there is no main property and only a single column, get all + // properties from the first field item and assume that they will be + // stored serialized. + // @todo Give field types more control over this behavior in + // https://drupal.org/node/2232427. + if (!$definition->getMainPropertyName() && count($columns) == 1) { + $value = $entity->$field_name->first()->getValue(); } - elseif (!$main_property) { - // If there is no main property, get all properties from the first - // field item and assume that they will be stored serialized. - // @todo Give field types more control over this behavior in - // https://drupal.org/node/2232427. - $values[$name] = $field_item->getValue(); + else { + $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL; } - } - } - - // Handle fields that store multiple properties and match each property name - // to its schema column name. - foreach (array_keys($multi_column_fields) as $field_name) { - $field_items = $entity->get($field_name); - $field_value = $field_items->getValue(); - foreach (array_keys($field_items->getFieldDefinition()->getColumns()) as $field_schema_column) { - if (isset($schema['fields'][$field_name . '__' . $field_schema_column])) { - $values[$field_name . '__' . $field_schema_column] = isset($field_value[0][$field_schema_column]) ? $field_value[0][$field_schema_column] : NULL; + if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) { + $value = serialize($value); } - } - } - - foreach ($values as $field_name => $value) { - // If we are creating a new entity, we must not populate the record with - // NULL values otherwise defaults would not be applied. - if (isset($value) || !$is_new) { - $record->$field_name = drupal_schema_get_field_value($schema['fields'][$field_name], $value); + $record->$schema_name = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value); } } @@ -657,15 +864,17 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_key * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity object. - * @param string $table_key - * (optional) The entity key identifying the target table. Defaults to - * 'data_table'. + * @param string $table_name + * (optional) The table name to map records to. Defaults to the data table. * * @return \stdClass * The record to store. */ - protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = 'data_table') { - $record = $this->mapToStorageRecord($entity, $table_key); + protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) { + if (!isset($table_name)) { + $table_name = $this->dataTable; + } + $record = $this->mapToStorageRecord($entity, $table_name); $record->langcode = $entity->language()->id; $record->default_langcode = intval($record->langcode == $entity->getUntranslated()->language()->id); return $record; @@ -681,12 +890,20 @@ protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = * The revision id. */ protected function saveRevision(EntityInterface $entity) { - $record = $this->mapToStorageRecord($entity, 'revision_table'); + $record = $this->mapToStorageRecord($entity, $this->revisionTable); $entity->preSaveRevision($this, $record); if ($entity->isNewRevision()) { - drupal_write_record($this->revisionTable, $record); + $insert_id = $this->database + ->insert($this->revisionTable, array('return' => Database::RETURN_INSERT_ID)) + ->fields((array) $record) + ->execute(); + // Even if this is a new revsision, the revision ID key might have been + // set in which case we should not override the provided revision ID. + if (!isset($record->{$this->revisionKey})) { + $record->{$this->revisionKey} = $insert_id; + } if ($entity->isDefaultRevision()) { $this->database->update($this->entityType->getBaseTable()) ->fields(array($this->revisionKey => $record->{$this->revisionKey})) @@ -695,7 +912,11 @@ protected function saveRevision(EntityInterface $entity) { } } else { - drupal_write_record($this->revisionTable, $record, $this->revisionKey); + $this->database + ->update($this->revisionTable) + ->fields((array) $record) + ->condition($this->revisionKey, $record->{$this->revisionKey}) + ->execute(); } // Make sure to update the new revision key for the entity. @@ -732,7 +953,7 @@ protected function doLoadFieldItems($entities, $age) { foreach ($bundles as $bundle => $v) { foreach ($this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle) as $field_name => $instance) { if ($instance instanceof FieldInstanceConfigInterface) { - $fields[$field_name] = $instance->getField(); + $fields[$field_name] = $instance; } } } @@ -801,9 +1022,8 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { if (!($instance instanceof FieldInstanceConfigInterface)) { continue; } - $field = $instance->getField(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + $table_name = static::_fieldTableName($instance); + $revision_name = static::_fieldRevisionTableName($instance); // Delete and insert, rather than update, in case a value was added. if ($update) { @@ -823,13 +1043,13 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { // Prepare the multi-insert query. $do_insert = FALSE; $columns = array('entity_id', 'revision_id', 'bundle', 'delta', 'langcode'); - foreach ($field->getColumns() as $column => $attributes) { - $columns[] = static::_fieldColumnName($field, $column); + foreach ($instance->getColumns() as $column => $attributes) { + $columns[] = static::_fieldColumnName($instance, $column); } $query = $this->database->insert($table_name)->fields($columns); $revision_query = $this->database->insert($revision_name)->fields($columns); - $langcodes = $field->isTranslatable() ? $translation_langcodes : array($default_langcode); + $langcodes = $instance->isTranslatable() ? $translation_langcodes : array($default_langcode); foreach ($langcodes as $langcode) { $delta_count = 0; $items = $entity->getTranslation($langcode)->get($field_name); @@ -844,15 +1064,15 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { 'delta' => $delta, 'langcode' => $langcode, ); - foreach ($field->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($field, $column); + foreach ($instance->getColumns() as $column => $attributes) { + $column_name = static::_fieldColumnName($instance, $column); // Serialize the value if specified in the column schema. $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column; } $query->values($record); $revision_query->values($record); - if ($field->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $field->getCardinality()) { + if ($instance->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $instance->getCardinality()) { break; } } @@ -878,9 +1098,8 @@ protected function doDeleteFieldItems(EntityInterface $entity) { if (!($instance instanceof FieldInstanceConfigInterface)) { continue; } - $field = $instance->getField(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + $table_name = static::_fieldTableName($instance); + $revision_name = static::_fieldRevisionTableName($instance); $this->database->delete($table_name) ->condition('entity_id', $entity->id()) ->execute(); @@ -900,7 +1119,7 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) { if (!($instance instanceof FieldInstanceConfigInterface)) { continue; } - $revision_name = static::_fieldRevisionTableName($instance->getField()); + $revision_name = static::_fieldRevisionTableName($instance); $this->database->delete($revision_name) ->condition('entity_id', $entity->id()) ->condition('revision_id', $vid) @@ -912,8 +1131,8 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function onFieldCreate(FieldConfigInterface $field) { - $schema = $this->_fieldSqlSchema($field); + public function onFieldCreate(FieldStorageDefinitionInterface $storage_definition) { + $schema = $this->_fieldSqlSchema($storage_definition); foreach ($schema as $name => $table) { $this->database->schema()->createTable($name, $table); } @@ -922,10 +1141,10 @@ public function onFieldCreate(FieldConfigInterface $field) { /** * {@inheritdoc} */ - public function onFieldUpdate(FieldConfigInterface $field) { - $original = $field->original; + public function onFieldUpdate(FieldStorageDefinitionInterface $storage_definition) { + $original = $storage_definition->original; - if (!$field->hasData()) { + if (!$storage_definition->hasData()) { // There is no data. Re-create the tables completely. if ($this->database->supportsTransactionalDDL()) { @@ -939,7 +1158,7 @@ public function onFieldUpdate(FieldConfigInterface $field) { foreach ($original_schema as $name => $table) { $this->database->schema()->dropTable($name, $table); } - $schema = $this->_fieldSqlSchema($field); + $schema = $this->_fieldSqlSchema($storage_definition); foreach ($schema as $name => $table) { $this->database->schema()->createTable($name, $table); } @@ -961,7 +1180,7 @@ public function onFieldUpdate(FieldConfigInterface $field) { } } else { - if ($field->getColumns() != $original->getColumns()) { + if ($storage_definition->getColumns() != $original->getColumns()) { throw new FieldConfigUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); } // There is data, so there are no column changes. Drop all the prior @@ -970,33 +1189,33 @@ public function onFieldUpdate(FieldConfigInterface $field) { $table = static::_fieldTableName($original); $revision_table = static::_fieldRevisionTableName($original); - $schema = $field->getSchema(); + $schema = $storage_definition->getSchema(); $original_schema = $original->getSchema(); foreach ($original_schema['indexes'] as $name => $columns) { if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($field, $name); + $real_name = static::_fieldIndexName($storage_definition, $name); $this->database->schema()->dropIndex($table, $real_name); $this->database->schema()->dropIndex($revision_table, $real_name); } } - $table = static::_fieldTableName($field); - $revision_table = static::_fieldRevisionTableName($field); + $table = static::_fieldTableName($storage_definition); + $revision_table = static::_fieldRevisionTableName($storage_definition); foreach ($schema['indexes'] as $name => $columns) { if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($field, $name); + $real_name = static::_fieldIndexName($storage_definition, $name); $real_columns = array(); foreach ($columns as $column_name) { // Indexes can be specified as either a column name or an array with // column name and length. Allow for either case. if (is_array($column_name)) { $real_columns[] = array( - static::_fieldColumnName($field, $column_name[0]), + static::_fieldColumnName($storage_definition, $column_name[0]), $column_name[1], ); } else { - $real_columns[] = static::_fieldColumnName($field, $column_name); + $real_columns[] = static::_fieldColumnName($storage_definition, $column_name); } } $this->database->schema()->addIndex($table, $real_name, $real_columns); @@ -1009,20 +1228,18 @@ public function onFieldUpdate(FieldConfigInterface $field) { /** * {@inheritdoc} */ - public function onFieldDelete(FieldConfigInterface $field) { + public function onFieldDelete(FieldStorageDefinitionInterface $storage_definition) { // Mark all data associated with the field for deletion. - $table = static::_fieldTableName($field); - $revision_table = static::_fieldRevisionTableName($field); + $table = static::_fieldTableName($storage_definition); + $revision_table = static::_fieldRevisionTableName($storage_definition); $this->database->update($table) ->fields(array('deleted' => 1)) ->execute(); // Move the table to a unique name while the table contents are being // deleted. - $deleted_field = clone $field; - $deleted_field->deleted = TRUE; - $new_table = static::_fieldTableName($deleted_field); - $revision_new_table = static::_fieldRevisionTableName($deleted_field); + $new_table = static::_fieldTableName($storage_definition, TRUE); + $revision_new_table = static::_fieldRevisionTableName($storage_definition, TRUE); $this->database->schema()->renameTable($table, $new_table); $this->database->schema()->renameTable($revision_table, $revision_new_table); } @@ -1030,17 +1247,16 @@ public function onFieldDelete(FieldConfigInterface $field) { /** * {@inheritdoc} */ - public function onInstanceDelete(FieldInstanceConfigInterface $instance) { - $field = $instance->getField(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + public function onInstanceDelete(FieldDefinitionInterface $field_definition) { + $table_name = static::_fieldTableName($field_definition); + $revision_name = static::_fieldRevisionTableName($field_definition); $this->database->update($table_name) ->fields(array('deleted' => 1)) - ->condition('bundle', $instance->bundle) + ->condition('bundle', $field_definition->bundle) ->execute(); $this->database->update($revision_name) ->fields(array('deleted' => 1)) - ->condition('bundle', $instance->bundle) + ->condition('bundle', $field_definition->bundle) ->execute(); } @@ -1053,9 +1269,8 @@ public function onBundleRename($bundle, $bundle_new) { // using the old bundle name. $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'include_deleted' => TRUE)); foreach ($instances as $instance) { - $field = $instance->getField(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + $table_name = static::_fieldTableName($instance); + $revision_name = static::_fieldRevisionTableName($instance); $this->database->update($table_name) ->fields(array('bundle' => $bundle_new)) ->condition('bundle', $bundle) @@ -1070,54 +1285,109 @@ public function onBundleRename($bundle, $bundle_new) { /** * {@inheritdoc} */ - protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) { - $field = $instance->getField(); - $table_name = static::_fieldTableName($field); + protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) { + // Check whether the whole field storage definition is gone, or just some + // bundle fields. + $is_deleted = !array_key_exists($field_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); + $table_name = static::_fieldTableName($field_definition, $is_deleted); $query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)) - ->condition('entity_id', $entity->id()) - ->orderBy('delta'); - foreach ($field->getColumns() as $column_name => $data) { - $query->addField('t', static::_fieldColumnName($field, $column_name), $column_name); + ->fields('t') + ->condition('bundle', $field_definition->bundle) + ->orderBy('entity_id') + ->orderBy('revision_id') + ->orderBy('delta') + ->range(0, $batch_size); + + // Create a map of field data table column names to field column names. + $column_map = array(); + foreach ($field_definition->getColumns() as $column_name => $data) { + $column_map[static::_fieldColumnName($field_definition, $column_name)] = $column_name; + } + + $entities = array(); + $items_by_entity = array(); + foreach ($query->execute() as $row) { + if (!isset($entities[$row['revision_id']])) { + // Create entity with the right revision id and entity id combination. + $row['entity_type'] = $this->entityTypeId; + // @todo: Replace this by an entity object created via an entity + // factory, see https://drupal.org/node/1867228. + $entities[$row['revision_id']] = _field_create_entity_from_ids((object) $row); + } + $item = array(); + foreach ($column_map as $db_column => $field_column) { + $item[$field_column] = $row[$db_column]; + } + $items_by_entity[$row['revision_id']][] = $item; + } + // Create field item objects and return. + foreach ($items_by_entity as $revision_id => $values) { + $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entities[$revision_id]); } - return $query->execute()->fetchAll(); + return $items_by_entity; } /** * {@inheritdoc} */ - public function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) { - $field = $instance->getField(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { + $is_deleted = !array_key_exists($field_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); + $table_name = static::_fieldTableName($field_definition, $is_deleted); + $revision_name = static::_fieldRevisionTableName($field_definition, $is_deleted); + $revision_id = $entity->getRevisionId() !== NULL ? $entity->getRevisionId() : $entity->id(); $this->database->delete($table_name) - ->condition('entity_id', $entity->id()) + ->condition('revision_id', $revision_id) ->execute(); $this->database->delete($revision_name) - ->condition('entity_id', $entity->id()) + ->condition('revision_id', $revision_id) ->execute(); } /** * {@inheritdoc} */ - public function onFieldPurge(FieldConfigInterface $field) { - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { + $table_name = static::_fieldTableName($storage_definition, TRUE); + $revision_name = static::_fieldRevisionTableName($storage_definition, TRUE); $this->database->schema()->dropTable($table_name); $this->database->schema()->dropTable($revision_name); } /** + * {@inheritdoc} + */ + public function countFieldData($storage_definition, $as_bool = FALSE) { + $is_deleted = !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); + $table_name = static::_fieldTableName($storage_definition, $is_deleted); + + $query = $this->database->select($table_name, 't'); + $or = $query->orConditionGroup(); + foreach ($storage_definition->getColumns() as $column_name => $data) { + $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name)); + } + // If we are performing the query just to check if the field has data + // limit the number of rows. + if ($as_bool) { + $query->range(0, 1); + } + $count = $query->countQuery()->execute()->fetchField(); + return $as_bool ? (bool) $count : (int) $count; + } + + /** * Gets the SQL table schema. * * @private Calling this function circumvents the entity system and is * strongly discouraged. This function is not considered part of the public * API and modules relying on it might break even in minor releases. * - * @param \Drupal\field\FieldConfigInterface $field - * The field object + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. * @param array $schema * The field schema array. Mandatory for upgrades, omit otherwise. + * @param bool $deleted + * (optional) Whether the schema of the table holding the values of a + * deleted field should be returned. * * @return array * The same as a hook_schema() implementation for the data and the @@ -1125,17 +1395,20 @@ public function onFieldPurge(FieldConfigInterface $field) { * * @see hook_schema() */ - public static function _fieldSqlSchema(FieldConfigInterface $field, array $schema = NULL) { - if ($field->deleted) { - $description_current = "Data storage for deleted field {$field->uuid()} ({$field->entity_type}, {$field->getName()})."; - $description_revision = "Revision archive storage for deleted field {$field->uuid()} ({$field->entity_type}, {$field->getName()})."; + public static function _fieldSqlSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) { + if ($storage_definition->deleted && !$deleted) { + throw new \Exception("Field schema for deleted field to be returned."); + } + if ($deleted) { + $description_current = "Data storage for deleted field {$storage_definition->getUniqueStorageIdentifier()} ({$storage_definition->getTargetEntityTypeId()}, {$storage_definition->getName()})."; + $description_revision = "Revision archive storage for deleted field {$storage_definition->getUniqueStorageIdentifier()} ({$storage_definition->getTargetEntityTypeId()}, {$storage_definition->getName()})."; } else { - $description_current = "Data storage for {$field->entity_type} field {$field->getName()}."; - $description_revision = "Revision archive storage for {$field->entity_type} field {$field->getName()}."; + $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; } - $entity_type_id = $field->entity_type; + $entity_type_id = $storage_definition->getTargetEntityTypeId(); $entity_manager = \Drupal::entityManager(); $entity_type = $entity_manager->getDefinition($entity_type_id); $definitions = $entity_manager->getBaseFieldDefinitions($entity_type_id); @@ -1223,39 +1496,39 @@ public static function _fieldSqlSchema(FieldConfigInterface $field, array $schem ); if (!$schema) { - $schema = $field->getSchema(); + $schema = $storage_definition->getSchema(); } // Add field columns. foreach ($schema['columns'] as $column_name => $attributes) { - $real_name = static::_fieldColumnName($field, $column_name); + $real_name = static::_fieldColumnName($storage_definition, $column_name); $current['fields'][$real_name] = $attributes; } // Add indexes. foreach ($schema['indexes'] as $index_name => $columns) { - $real_name = static::_fieldIndexName($field, $index_name); + $real_name = static::_fieldIndexName($storage_definition, $index_name); foreach ($columns as $column_name) { // Indexes can be specified as either a column name or an array with // column name and length. Allow for either case. if (is_array($column_name)) { $current['indexes'][$real_name][] = array( - static::_fieldColumnName($field, $column_name[0]), + static::_fieldColumnName($storage_definition, $column_name[0]), $column_name[1], ); } else { - $current['indexes'][$real_name][] = static::_fieldColumnName($field, $column_name); + $current['indexes'][$real_name][] = static::_fieldColumnName($storage_definition, $column_name); } } } // Add foreign keys. foreach ($schema['foreign keys'] as $specifier => $specification) { - $real_name = static::_fieldIndexName($field, $specifier); + $real_name = static::_fieldIndexName($storage_definition, $specifier); $current['foreign keys'][$real_name]['table'] = $specification['table']; foreach ($specification['columns'] as $column_name => $referenced) { - $sql_storage_column = static::_fieldColumnName($field, $column_name); + $sql_storage_column = static::_fieldColumnName($storage_definition, $column_name); $current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; } } @@ -1268,8 +1541,8 @@ public static function _fieldSqlSchema(FieldConfigInterface $field, array $schem $revision['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; return array( - static::_fieldTableName($field) => $current, - static::_fieldRevisionTableName($field) => $revision, + static::_fieldTableName($storage_definition) => $current, + static::_fieldRevisionTableName($storage_definition) => $revision, ); } @@ -1283,23 +1556,26 @@ public static function _fieldSqlSchema(FieldConfigInterface $field, array $schem * support. Always call entity_load() before using the data found in the * table. * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $is_deleted + * (optional) Whether the table name holding the values of a deleted field + * should be returned. * * @return string * A string containing the generated name for the database table. - * */ - static public function _fieldTableName(FieldConfigInterface $field) { - if ($field->deleted) { + static public function _fieldTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { // When a field is a deleted, the table is renamed to // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the uuid and return the - // first 10 characters so we end up with a short unique ID. - return "field_deleted_data_" . substr(hash('sha256', $field->uuid()), 0, 10); + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); } else { - return static::_generateFieldTableName($field, FALSE); + return static::_generateFieldTableName($storage_definition, FALSE); } } @@ -1313,22 +1589,26 @@ static public function _fieldTableName(FieldConfigInterface $field) { * support. Always call entity_load() before using the data found in the * table. * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $is_deleted + * (optional) Whether the table name holding the values of a deleted field + * should be returned. * * @return string * A string containing the generated name for the database table. */ - static public function _fieldRevisionTableName(FieldConfigInterface $field) { - if ($field->deleted) { + static public function _fieldRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { // When a field is a deleted, the table is renamed to // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the uuid and return the - // first 10 characters so we end up with a short unique ID. - return "field_deleted_revision_" . substr(hash('sha256', $field->uuid()), 0, 10); + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); } else { - return static::_generateFieldTableName($field, TRUE); + return static::_generateFieldTableName($storage_definition, TRUE); } } @@ -1338,17 +1618,17 @@ static public function _fieldRevisionTableName(FieldConfigInterface $field) { * The method accounts for a maximum table name length of 64 characters, and * takes care of disambiguation. * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. * @param bool $revision * TRUE for revision table, FALSE otherwise. * * @return string * The final table name. */ - static protected function _generateFieldTableName(FieldConfigInterface $field, $revision) { + static protected function _generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) { $separator = $revision ? '_revision__' : '__'; - $table_name = $field->entity_type . $separator . $field->name; + $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName(); // Limit the string to 48 characters, keeping a 16 characters margin for db // prefixes. if (strlen($table_name) > 48) { @@ -1356,8 +1636,8 @@ static protected function _generateFieldTableName(FieldConfigInterface $field, $ // field UUID. $separator = $revision ? '_r__' : '__'; // Truncate to the same length for the current and revision tables. - $entity_type = substr($field->entity_type, 0, 34); - $field_hash = substr(hash('sha256', $field->uuid), 0, 10); + $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34); + $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); $table_name = $entity_type . $separator . $field_hash; } return $table_name; @@ -1370,8 +1650,8 @@ static protected function _generateFieldTableName(FieldConfigInterface $field, $ * strongly discouraged. This function is not considered part of the public * API and modules relying on it might break even in minor releases. * - * @param \Drupal\field\FieldConfigInterface $field - * The field structure + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. * @param string $index * The name of the index. * @@ -1379,8 +1659,8 @@ static protected function _generateFieldTableName(FieldConfigInterface $field, $ * A string containing a generated index name for a field data table that is * unique among all other fields. */ - static public function _fieldIndexName(FieldConfigInterface $field, $index) { - return $field->getName() . '_' . $index; + static public function _fieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { + return $storage_definition->getName() . '_' . $index; } /** @@ -1393,8 +1673,8 @@ static public function _fieldIndexName(FieldConfigInterface $field, $index) { * support. Always call entity_load() before using the data found in the * table. * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. * @param string $column * The name of the column. * @@ -1402,8 +1682,8 @@ static public function _fieldIndexName(FieldConfigInterface $field, $index) { * A string containing a generated column name for a field data table that is * unique among all other fields. */ - static public function _fieldColumnName(FieldConfigInterface $field, $column) { - return in_array($column, FieldConfig::getReservedColumns()) ? $column : $field->getName() . '_' . $column; + static public function _fieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) { + return in_array($column, FieldConfig::getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column; } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 98dbd2f..12aeca3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php @@ -8,7 +8,7 @@ namespace Drupal\Core\Entity; use Drupal\Core\Entity\Query\QueryException; -use Drupal\field\FieldInstanceConfigInterface; +use Drupal\Core\Field\FieldDefinitionInterface; /** * Defines a null entity storage. @@ -109,13 +109,14 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) { + protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) { + return array(); } /** * {@inheritdoc} */ - protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) { + protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { } /** @@ -130,4 +131,11 @@ protected function doSave($id, EntityInterface $entity) { protected function has($id, EntityInterface $entity) { } + /** + * {@inheritdoc} + */ + public function countFieldData($storage_definition, $as_bool = FALSE) { + return $as_bool ? FALSE : 0; + } + } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index b75e9bd..946b033 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -8,9 +8,10 @@ namespace Drupal\Core\Entity; use Drupal\Component\Utility\String; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\Field\PrepareCacheInterface; -use Drupal\field\FieldConfigInterface; use Drupal\field\FieldInstanceConfigInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -278,32 +279,32 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function onFieldCreate(FieldConfigInterface $field) { } + public function onFieldCreate(FieldStorageDefinitionInterface $storage_definition) { } /** * {@inheritdoc} */ - public function onFieldUpdate(FieldConfigInterface $field) { } + public function onFieldUpdate(FieldStorageDefinitionInterface $storage_definition) { } /** * {@inheritdoc} */ - public function onFieldDelete(FieldConfigInterface $field) { } + public function onFieldDelete(FieldStorageDefinitionInterface $storage_definition) { } /** * {@inheritdoc} */ - public function onInstanceCreate(FieldInstanceConfigInterface $instance) { } + public function onInstanceCreate(FieldDefinitionInterface $field_definition) { } /** * {@inheritdoc} */ - public function onInstanceUpdate(FieldInstanceConfigInterface $instance) { } + public function onInstanceUpdate(FieldDefinitionInterface $field_definition) { } /** * {@inheritdoc} */ - public function onInstanceDelete(FieldInstanceConfigInterface $instance) { } + public function onInstanceDelete(FieldDefinitionInterface $field_definition) { } /** * {@inheritdoc} @@ -323,45 +324,47 @@ public function onBundleDelete($bundle) { } /** * {@inheritdoc} */ - public function onFieldItemsPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) { - if ($values = $this->readFieldItemsToPurge($entity, $instance)) { - $items = \Drupal::typedDataManager()->create($instance, $values, $instance->getName(), $entity); + public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) { + $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size); + $count = 0; + foreach ($items_by_entity as $items) { $items->delete(); + $this->purgeFieldItems($items->getEntity(), $field_definition); + $count += $items->count(); } - $this->purgeFieldItems($entity, $instance); + return $count; } /** - * Reads values to be purged for a single field of a single entity. + * Reads values to be purged for a single field. * * This method is called during field data purge, on fields for which * onFieldDelete() or onFieldInstanceDelete() has previously run. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param \Drupal\field\FieldInstanceConfigInterface $instance - * The field instance. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition. + * @param $batch_size + * The maximum number of field data records to purge before returning. * - * @return array - * The field values, in their canonical array format (numerically indexed - * array of items, each item being a property/value array). + * @return \Drupal\Core\Field\FieldItemListInterface[] + * An array of field item lists, keyed by entity revision id. */ - abstract protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance); + abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size); /** - * Removes field data from storage during purge. + * Removes field items from storage per entity during purge. * - * @param EntityInterface $entity - * The entity whose values are being purged. - * @param FieldInstanceConfigInterface $instance + * @param ContentEntityInterface $entity + * The entity revision, whose values are being purged. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * The field whose values are bing purged. */ - abstract protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance); + abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition); /** * {@inheritdoc} */ - public function onFieldPurge(FieldConfigInterface $field) { } + public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { } /** * Checks translation statuses and invoke the related hooks if needed. diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 557ddf3..cc7b316 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -555,14 +555,18 @@ protected static function invalidateTagsOnDelete(array $entities) { * Acts on entities of which this entity is a bundle entity type. */ protected function onUpdateBundleEntity() { - // If this entity is a bundle entity type of another entity type, and we're - // updating an existing entity, and that other entity type has a view - // builder class, then invalidate the render cache of entities for which - // this entity is a bundle. $bundle_of = $this->getEntityType()->getBundleOf(); - $entity_manager = \Drupal::entityManager(); - if ($bundle_of !== FALSE && $entity_manager->hasController($bundle_of, 'view_builder')) { - $entity_manager->getViewBuilder($bundle_of)->resetCache(); + if ($bundle_of !== FALSE) { + // If this entity is a bundle entity type of another entity type, and we're + // updating an existing entity, and that other entity type has a view + // builder class, then invalidate the render cache of entities for which + // this entity is a bundle. + $entity_manager = $this->entityManager(); + if ($entity_manager->hasController($bundle_of, 'view_builder')) { + $entity_manager->getViewBuilder($bundle_of)->resetCache(); + } + // Entity bundle field definitions may depend on bundle settings. + $entity_manager->clearCachedFieldDefinitions(); } } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index 5d24441..f10eba9 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -76,6 +76,7 @@ public function __construct(EntityTypeInterface $entity_type) { $this->entityTypeId = $entity_type->id(); $this->entityType = $entity_type; $this->idKey = $this->entityType->getKey('id'); + $this->uuidKey = $this->entityType->getKey('uuid'); $this->entityClass = $this->entityType->getClass(); } diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php index 2998c21..5245d8f 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php @@ -7,68 +7,68 @@ namespace Drupal\Core\Entity; -use Drupal\field\FieldConfigInterface; -use Drupal\field\FieldInstanceConfigInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; interface FieldableEntityStorageInterface extends EntityStorageInterface { /** - * Allows reaction to the creation of a configurable field. + * Allows reaction to the creation of a field. * - * @param \Drupal\field\FieldConfigInterface $field + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field being created. */ - public function onFieldCreate(FieldConfigInterface $field); + public function onFieldCreate(FieldStorageDefinitionInterface $storage_definition); /** - * Allows reaction to the update of a configurable field. + * Allows reaction to the update of a field. * - * @param \Drupal\field\FieldConfigInterface $field + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field being updated. */ - public function onFieldUpdate(FieldConfigInterface $field); + public function onFieldUpdate(FieldStorageDefinitionInterface $storage_definition); /** - * Allows reaction to the deletion of a configurable field. + * Allows reaction to the deletion of a field. * * Stored values should not be wiped at once, but marked as 'deleted' so that * they can go through a proper purge process later on. * - * @param \Drupal\field\FieldConfigInterface $field + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field being deleted. * * @see fieldPurgeData() */ - public function onFieldDelete(FieldConfigInterface $field); + public function onFieldDelete(FieldStorageDefinitionInterface $storage_definition); /** - * Allows reaction to the creation of a configurable field instance. + * Allows reaction to the creation of a field. * - * @param \Drupal\field\FieldInstanceConfigInterface $instance - * The instance being created. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field being created. */ - public function onInstanceCreate(FieldInstanceConfigInterface $instance); + public function onInstanceCreate(FieldDefinitionInterface $field_definition); /** - * Allows reaction to the update of a configurable field instance. + * Allows reaction to the update of a field. * - * @param \Drupal\field\FieldInstanceConfigInterface $instance - * The instance being updated. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field being updated. */ - public function onInstanceUpdate(FieldInstanceConfigInterface $instance); + public function onInstanceUpdate(FieldDefinitionInterface $field_definition); /** - * Allows reaction to the deletion of a configurable field instance. + * Allows reaction to the deletion of a field. * * Stored values should not be wiped at once, but marked as 'deleted' so that * they can go through a proper purge process later on. * - * @param \Drupal\field\FieldInstanceConfigInterface $instance - * The instance being deleted. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field being deleted. * * @see fieldPurgeData() */ - public function onInstanceDelete(FieldInstanceConfigInterface $instance); + public function onInstanceDelete(FieldDefinitionInterface $field_definition); /** * Allows reaction to a bundle being created. @@ -81,8 +81,7 @@ public function onBundleCreate($bundle); /** * Allows reaction to a bundle being renamed. * - * This method runs before field instance definitions are updated with the new - * bundle name. + * This method runs before fields are updated with the new bundle name. * * @param string $bundle * The name of the bundle being renamed. @@ -94,7 +93,7 @@ public function onBundleRename($bundle, $bundle_new); /** * Allows reaction to a bundle being deleted. * - * This method runs before field and instance definitions are deleted. + * This method runs before fields are deleted. * * @param string $bundle * The name of the bundle being deleted. @@ -102,24 +101,39 @@ public function onBundleRename($bundle, $bundle_new); public function onBundleDelete($bundle); /** - * Purges the field data for a single field on a single entity. + * Purges a batch of field data. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The deleted field whose data is being purged. + * @param $batch_size + * The maximum number of field data records to purge before returning. + * + * @return int + * The number of field data records that have been purged. + */ + public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size); + + /** + * Determines the number of field data records of a field. * - * The entity itself is not being deleted, and it is quite possible that - * other field data will remain attached to it. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field for which to count data records. + * @param bool $as_bool + * (Optional) Optimises the query for checking whether there are any records + * or not. Defaults to FALSE. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity whose field data is being purged. - * @param \Drupal\field\FieldInstanceConfigInterface $instance - * The deleted field instance whose data is being purged. + * @return bool|int + * The number of field data records. If $as_bool parameter is TRUE then the + * value will either be TRUE or FALSE. */ - public function onFieldItemsPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance); + public function countFieldData($storage_definition, $as_bool = FALSE); /** - * Performs final cleanup after all data on all instances has been purged. + * Performs final cleanup after all data of a field has been purged. * - * @param \Drupal\field\FieldConfigInterface $instance + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field being purged. */ - public function onFieldPurge(FieldConfigInterface $field); + public function finalizePurge(FieldStorageDefinitionInterface $storage_definition); } diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index 50ab59b..c530541 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\Query\QueryException; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldConfigInterface; @@ -35,7 +36,6 @@ class Tables implements TablesInterface { */ protected $entityTables = array(); - /** * Field table array, key is table name, value is alias. * @@ -46,10 +46,18 @@ class Tables implements TablesInterface { protected $fieldTables = array(); /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** * @param \Drupal\Core\Database\Query\SelectInterface $sql_query */ public function __construct(SelectInterface $sql_query) { $this->sqlQuery = $sql_query; + $this->entityManager = \Drupal::entityManager(); } /** @@ -57,7 +65,6 @@ public function __construct(SelectInterface $sql_query) { */ public function addField($field, $type, $langcode) { $entity_type_id = $this->sqlQuery->getMetaData('entity_type'); - $entity_manager = \Drupal::entityManager(); $age = $this->sqlQuery->getMetaData('age'); // This variable ensures grouping works correctly. For example: // ->condition('tags', 2, '>') @@ -73,13 +80,13 @@ public function addField($field, $type, $langcode) { // This will contain the definitions of the last specifier seen by the // system. $propertyDefinitions = array(); - $entity_type = $entity_manager->getDefinition($entity_type_id); + $entity_type = $this->entityManager->getDefinition($entity_type_id); $field_storage_definitions = array(); // @todo Needed for menu links, make this implementation content entity // specific after https://drupal.org/node/2256521. if ($entity_type instanceof ContentEntityTypeInterface) { - $field_storage_definitions = $entity_manager->getFieldStorageDefinitions($entity_type_id); + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); } for ($key = 0; $key <= $count; $key ++) { // If there is revision support and only the current revision is being @@ -99,15 +106,7 @@ public function addField($field, $type, $langcode) { // This can either be the name of an entity base field or a configurable // field. $specifier = $specifiers[$key]; - // Normally it is a field name, but field_purge_batch() is passing in - // id:$field_id so check that first. - /* @var \Drupal\Core\Field\FieldDefinitionInterface $field */ - if (substr($specifier, 0, 3) == 'id:') { - if ($fields = entity_load_multiple_by_properties('field_config', array('uuid' => substr($specifier, 3), 'include_deleted' => TRUE))) { - $field = current($fields); - } - } - elseif (isset($field_storage_definitions[$specifier])) { + if (isset($field_storage_definitions[$specifier])) { $field = $field_storage_definitions[$specifier]; } else { @@ -154,10 +153,10 @@ public function addField($field, $type, $langcode) { $entity_tables = array(); if ($data_table = $entity_type->getDataTable()) { $this->sqlQuery->addMetaData('simple_query', FALSE); - $entity_tables[$data_table] = drupal_get_schema($data_table); + $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id); } $entity_base_table = $entity_type->getBaseTable(); - $entity_tables[$entity_base_table] = drupal_get_schema($entity_base_table); + $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id); $sql_column = $specifier; $table = $this->ensureEntityTable($index_prefix, $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables); } @@ -174,8 +173,8 @@ public function addField($field, $type, $langcode) { if (isset($propertyDefinitions[$relationship_specifier]) && $field->getPropertyDefinition('entity')->getDataType() == 'entity_reference' ) { // If it is, use the entity type. $entity_type_id = $propertyDefinitions[$relationship_specifier]->getTargetDefinition()->getEntityTypeId(); - $entity_type = $entity_manager->getDefinition($entity_type_id); - $field_storage_definitions = $entity_manager->getFieldStorageDefinitions($entity_type_id); + $entity_type = $this->entityManager->getDefinition($entity_type_id); + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); // Add the new entity base table using the table and sql column. $join_condition= '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column"; $base_table = $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition); @@ -199,8 +198,8 @@ public function addField($field, $type, $langcode) { * @throws \Drupal\Core\Entity\Query\QueryException */ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) { - foreach ($entity_tables as $table => $schema) { - if (isset($schema['fields'][$property])) { + foreach ($entity_tables as $table => $mapping) { + if (isset($mapping[$property])) { if (!isset($this->entityTables[$index_prefix . $table])) { $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode); } @@ -241,4 +240,27 @@ protected function addJoin($type, $table, $join_condition, $langcode) { return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments); } + /** + * Returns the schema for the given table. + * + * @param string $table + * The table name. + * + * @return array|bool + * The table field mapping for the given table or FALSE if not available. + */ + protected function getTableMapping($table, $entity_type_id) { + $storage = $this->entityManager->getStorage($entity_type_id); + // @todo Stop calling drupal_get_schema() once menu links are converted + // to the Entity Field API. See https://drupal.org/node/1842858. + $schema = drupal_get_schema($table); + if (!$schema && $storage instanceof SqlEntityStorageInterface) { + $mapping = $storage->getTableMapping()->getAllColumns($table); + } + else { + $mapping = array_keys($schema['fields']); + } + return array_flip($mapping); + } + } diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php new file mode 100644 index 0000000..5e7c0e5 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -0,0 +1,518 @@ +entityType = $entity_type; + $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id()); + $this->storage = $storage; + } + + /** + * {@inheritdoc} + */ + public function getSchema() { + // Prepare basic information about the entity type. + $tables = $this->getTables(); + + if (!isset($this->schema[$this->entityType->id()])) { + // Initialize the table schema. + $schema[$tables['base_table']] = $this->initializeBaseTable(); + if (isset($tables['revision_table'])) { + $schema[$tables['revision_table']] = $this->initializeRevisionTable(); + } + if (isset($tables['data_table'])) { + $schema[$tables['data_table']] = $this->initializeDataTable(); + } + if (isset($tables['revision_data_table'])) { + $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable(); + } + + $table_mapping = $this->storage->getTableMapping(); + foreach ($table_mapping->getTableNames() as $table_name) { + // Add the schema from field definitions. + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + $column_names = $table_mapping->getColumnNames($field_name); + $this->addFieldSchema($schema[$table_name], $field_name, $column_names); + } + + // Add the schema for extra fields. + foreach ($table_mapping->getExtraColumns($table_name) as $column_name) { + if ($column_name == 'default_langcode') { + $this->addDefaultLangcodeSchema($schema[$table_name]); + } + } + } + + // Process tables after having gathered field information. + $this->processBaseTable($schema[$tables['base_table']]); + if (isset($tables['revision_table'])) { + $this->processRevisionTable($schema[$tables['revision_table']]); + } + if (isset($tables['data_table'])) { + $this->processDataTable($schema[$tables['data_table']]); + } + if (isset($tables['revision_data_table'])) { + $this->processRevisionDataTable($schema[$tables['revision_data_table']]); + } + + $this->schema[$this->entityType->id()] = $schema; + } + + return $this->schema[$this->entityType->id()]; + } + + /** + * Gets a list of entity type tables. + * + * @return array + * A list of entity type tables, keyed by table key. + */ + protected function getTables() { + return array_filter(array( + 'base_table' => $this->storage->getBaseTable(), + 'revision_table' => $this->storage->getRevisionTable(), + 'data_table' => $this->storage->getDataTable(), + 'revision_data_table' => $this->storage->getRevisionDataTable(), + )); + } + + /** + * Returns the schema for a single field definition. + * + * @param array $schema + * The table schema to add the field schema to, passed by reference. + * @param string $field_name + * The name of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + */ + protected function addFieldSchema(array &$schema, $field_name, array $column_mapping) { + $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema(); + $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription(); + + foreach ($column_mapping as $field_column_name => $schema_field_name) { + $column_schema = $field_schema['columns'][$field_column_name]; + + $schema['fields'][$schema_field_name] = $column_schema; + $schema['fields'][$schema_field_name]['description'] = $field_description; + // Only entity keys are required. + $keys = $this->entityType->getKeys() + array('langcode' => 'langcode'); + // The label is an entity key, but label fields are not necessarily + // required. + // Because entity ID and revision ID are both serial fields in the base + // and revision table respectively, the revision ID is not known yet, when + // inserting data into the base table. Instead the revision ID in the base + // table is updated after the data has been inserted into the revision + // table. For this reason the revision ID field cannot be marked as NOT + // NULL. + unset($keys['label'], $keys['revision']); + // Key fields may not be NULL. + if (in_array($field_name, $keys)) { + $schema['fields'][$schema_field_name]['not null'] = TRUE; + } + } + + if (!empty($field_schema['indexes'])) { + $indexes = $this->getFieldIndexes($field_name, $field_schema, $column_mapping); + $schema['indexes'] = array_merge($schema['indexes'], $indexes); + } + + if (!empty($field_schema['unique keys'])) { + $unique_keys = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping); + $schema['unique keys'] = array_merge($schema['unique keys'], $unique_keys); + } + + if (!empty($field_schema['foreign keys'])) { + $foreign_keys = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping); + $schema['foreign keys'] = array_merge($schema['foreign keys'], $foreign_keys); + } + } + + /** + * Returns an index schema array for a given field. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the indexes. + */ + protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) { + return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes'); + } + + /** + * Returns a unique key schema array for a given field. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the unique keys. + */ + protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) { + return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys'); + } + + /** + * Returns field schema data for the given key. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * @param string $schema_key + * The type of schema data. Either 'indexes' or 'unique keys'. + * + * @return array + * The schema definition for the specified key. + */ + protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) { + $data = array(); + + foreach ($field_schema[$schema_key] as $key => $columns) { + // To avoid clashes with entity-level indexes or unique keys we use + // "{$entity_type_id}_field__" as a prefix instead of just + // "{$entity_type_id}__". We additionally namespace the specifier by the + // field name to avoid clashes when multiple fields of the same type are + // added to an entity type. + $entity_type_id = $this->entityType->id(); + $real_key = "{$entity_type_id}_field__{$field_name}__{$key}"; + foreach ($columns as $column) { + // Allow for indexes and unique keys to specified as an array of column + // name and length. + if (is_array($column)) { + list($column_name, $length) = $column; + $data[$real_key][] = array($column_mapping[$column_name], $length); + } + else { + $data[$real_key][] = $column_mapping[$column]; + } + } + } + + return $data; + } + + /** + * Returns field foreign keys. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the foreign keys. + */ + protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) { + $foreign_keys = array(); + + foreach ($field_schema['foreign keys'] as $specifier => $specification) { + // To avoid clashes with entity-level foreign keys we use + // "{$entity_type_id}_field__" as a prefix instead of just + // "{$entity_type_id}__". We additionally namespace the specifier by the + // field name to avoid clashes when multiple fields of the same type are + // added to an entity type. + $entity_type_id = $this->entityType->id(); + $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}"; + $foreign_keys[$real_specifier]['table'] = $specification['table']; + foreach ($specification['columns'] as $column => $referenced) { + $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced; + } + } + + return $foreign_keys; + } + + /** + * Returns the schema for the 'default_langcode' metadata field. + * + * @param array $schema + * The table schema to add the field schema to, passed by reference. + * + * @return array + * A schema field array for the 'default_langcode' metadata field. + */ + protected function addDefaultLangcodeSchema(&$schema) { + $schema['fields']['default_langcode'] = array( + 'description' => 'Boolean indicating whether field values are in the default entity language.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 1, + ); + } + + /** + * Initializes common information for a base table. + * + * @return array + * A partial schema array for the base table. + */ + protected function initializeBaseTable() { + $entity_type_id = $this->entityType->id(); + + $schema = array( + 'description' => "The base table for $entity_type_id entities.", + 'primary key' => array($this->entityType->getKey('id')), + 'indexes' => array(), + 'foreign keys' => array(), + ); + + if ($this->entityType->hasKey('uuid')) { + $uuid_key = $this->entityType->getKey('uuid'); + $schema['unique keys'] = array( + $this->getEntityIndexName($uuid_key) => array($uuid_key), + ); + } + + if ($this->entityType->hasKey('revision')) { + $revision_key = $this->entityType->getKey('revision'); + $key_name = $this->getEntityIndexName($revision_key); + $schema['unique keys'][$key_name] = array($revision_key); + $schema['foreign keys'][$entity_type_id . '__revision'] = array( + 'table' => $this->storage->getRevisionTable(), + 'columns' => array($revision_key => $revision_key), + ); + } + + return $schema; + } + + /** + * Initializes common information for a revision table. + * + * @return array + * A partial schema array for the revision table. + */ + protected function initializeRevisionTable() { + $entity_type_id = $this->entityType->id(); + $id_key = $this->entityType->getKey('id'); + $revision_key = $this->entityType->getKey('revision'); + + $schema = array( + 'description' => "The revision table for $entity_type_id entities.", + 'primary key' => array($revision_key), + 'indexes' => array(), + 'foreign keys' => array( + $entity_type_id . '__revisioned' => array( + 'table' => $this->storage->getBaseTable(), + 'columns' => array($id_key => $id_key), + ), + ), + ); + + $schema['indexes'][$this->getEntityIndexName($id_key)] = array($id_key); + + return $schema; + } + + /** + * Initializes common information for a data table. + * + * @return array + * A partial schema array for the data table. + */ + protected function initializeDataTable() { + $entity_type_id = $this->entityType->id(); + $id_key = $this->entityType->getKey('id'); + + $schema = array( + 'description' => "The data table for $entity_type_id entities.", + // @todo Use the language entity key when https://drupal.org/node/2143729 + // is in. + 'primary key' => array($id_key, 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + $entity_type_id => array( + 'table' => $this->storage->getBaseTable(), + 'columns' => array($id_key => $id_key), + ), + ), + ); + + if ($this->entityType->hasKey('revision')) { + $key = $this->entityType->getKey('revision'); + $schema['indexes'][$this->getEntityIndexName($key)] = array($key); + } + + return $schema; + } + + /** + * Initializes common information for a revision data table. + * + * @return array + * A partial schema array for the revision data table. + */ + protected function initializeRevisionDataTable() { + $entity_type_id = $this->entityType->id(); + $id_key = $this->entityType->getKey('id'); + $revision_key = $this->entityType->getKey('revision'); + + $schema = array( + 'description' => "The revision data table for $entity_type_id entities.", + // @todo Use the language entity key when https://drupal.org/node/2143729 + // is in. + 'primary key' => array($revision_key, 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + $entity_type_id => array( + 'table' => $this->storage->getBaseTable(), + 'columns' => array($id_key => $id_key), + ), + $entity_type_id . '__revision' => array( + 'table' => $this->storage->getRevisionTable(), + 'columns' => array($revision_key => $revision_key), + ) + ), + ); + + return $schema; + } + + /** + * Processes the gathered schema for a base table. + * + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processBaseTable(array &$schema) { + $this->processIdentifierSchema($schema, $this->entityType->getKey('id')); + } + + /** + * Processes the gathered schema for a base table. + * + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processRevisionTable(array &$schema) { + $this->processIdentifierSchema($schema, $this->entityType->getKey('revision')); + } + + /** + * Processes the gathered schema for a base table. + * + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processDataTable(array &$schema) { + } + + /** + * Processes the gathered schema for a base table. + * + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processRevisionDataTable(array &$schema) { + } + + /** + * Processes the specified entity key. + * + * @param array $schema + * The table schema, passed by reference. + * @param string $key + * The entity key name. + */ + protected function processIdentifierSchema(&$schema, $key) { + if ($schema['fields'][$key]['type'] == 'int') { + $schema['fields'][$key]['type'] = 'serial'; + } + unset($schema['fields'][$key]['default']); + } + + /** + * Returns the name to be used for the given entity index. + * + * @param string $index + * The index column name. + * + * @return string + * The index name. + */ + protected function getEntityIndexName($index) { + return $this->entityType->id() . '__' . $index; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php new file mode 100644 index 0000000..a38b82c --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php @@ -0,0 +1,14 @@ +fieldStorageDefinitions = $storage_definitions; + } + + /** + * {@inheritdoc} + */ + public function getTableNames() { + return array_unique(array_merge(array_keys($this->fieldNames), array_keys($this->extraColumns))); + } + + /** + * {@inheritdoc} + */ + public function getAllColumns($table_name) { + if (!isset($this->allColumns[$table_name])) { + $this->allColumns[$table_name] = array(); + + foreach ($this->getFieldNames($table_name) as $field_name) { + $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name))); + } + + $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name)); + } + return $this->allColumns[$table_name]; + } + + /** + * {@inheritdoc} + */ + public function getFieldNames($table_name) { + if (isset($this->fieldNames[$table_name])) { + return $this->fieldNames[$table_name]; + } + return array(); + } + + /** + * {@inheritdoc} + */ + public function getColumnNames($field_name) { + if (!isset($this->columnMapping[$field_name])) { + $column_names = array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()); + if (count($column_names) == 1) { + $this->columnMapping[$field_name] = array(reset($column_names) => $field_name); + } + else { + $this->columnMapping[$field_name] = array(); + foreach ($column_names as $column_name) { + $this->columnMapping[$field_name][$column_name] = $field_name . '__' . $column_name; + } + } + } + return $this->columnMapping[$field_name]; + } + + /** + * Adds field columns for a table to the table mapping. + * + * @param string $table_name + * The name of the table to add the field column for. + * @param string[] $field_names + * A list of field names to add the columns for. + * + * @return $this + */ + public function setFieldNames($table_name, array $field_names) { + $this->fieldNames[$table_name] = $field_names; + // Force the re-computation of the column list. + unset($this->allColumns[$table_name]); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getExtraColumns($table_name) { + if (isset($this->extraColumns[$table_name])) { + return $this->extraColumns[$table_name]; + } + return array(); + } + + /** + * Adds a extra columns for a table to the table mapping. + * + * @param string $table_name + * The name of table to add the extra columns for. + * @param string[] $column_names + * The list of column names. + * + * @return $this + */ + public function setExtraColumns($table_name, array $column_names) { + $this->extraColumns[$table_name] = $column_names; + // Force the re-computation of the column list. + unset($this->allColumns[$table_name]); + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php new file mode 100644 index 0000000..8c4e3d8 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php @@ -0,0 +1,26 @@ +state = $state; - $this->config = $config_factory; - $this->stringTranslation = $translation; - $this->urlGenerator = $url_generator; - $this->account = $account; - } - /** * Determine whether the page is configured to be offline. * @@ -109,7 +30,7 @@ public function __construct(StateInterface $state, ConfigFactoryInterface $confi public function onKernelRequestDetermineSiteStatus(GetResponseEvent $event) { // Check if the site is offline. $request = $event->getRequest(); - $is_offline = $this->isSiteInMaintenance($request) ? static::SITE_OFFLINE : static::SITE_ONLINE; + $is_offline = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE; $request->attributes->set('_maintenance', $is_offline); } @@ -123,55 +44,17 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) { $request = $event->getRequest(); $response = $event->getResponse(); // Continue if the site is online and the response is not a redirection. - if ($request->attributes->get('_maintenance') != static::SITE_ONLINE && !($response instanceof RedirectResponse)) { + if ($request->attributes->get('_maintenance') != MENU_SITE_ONLINE && !($response instanceof RedirectResponse)) { // Deliver the 503 page. drupal_maintenance_theme(); - $content = Xss::filterAdmin(String::format($this->config->get('system.maintenance')->get('message'), array( - '@site' => $this->config->get('system.site')->get('name'), + $content = Xss::filterAdmin(String::format(\Drupal::config('system.maintenance')->get('message'), array( + '@site' => \Drupal::config('system.site')->get('name'), ))); $content = DefaultHtmlPageRenderer::renderPage($content, t('Site under maintenance')); $response = new Response('Service unavailable', 503); $response->setContent($content); $event->setResponse($response); } - - $can_access_maintenance = $this->account->hasPermission('access site in maintenance mode'); - $is_maintenance = $this->state->get('system.maintenance_mode'); - // Ensure that the maintenance mode message is displayed only once - // (allowing for page redirects) and specifically suppress its display on - // the maintenance mode settings page. - $is_maintenance_route = $request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'system.site_maintenance_mode'; - if ($is_maintenance && $can_access_maintenance && !$is_maintenance_route) { - if ($this->account->hasPermission('administer site configuration')) { - $this->drupalSetMessage($this->t('Operating in maintenance mode. Go online.', array('@url' => $this->urlGenerator->generate('system.site_maintenance_mode'))), 'status', FALSE); - } - else { - $this->drupalSetMessage($this->t('Operating in maintenance mode.'), 'status', FALSE); - } - } - } - - /** - * Checks whether the site is in maintenance mode. - * - * @return bool - * FALSE if the site is not in maintenance mode - */ - protected function isSiteInMaintenance() { - // Check if site is in maintenance mode. - if ($this->state->get('system.maintenance_mode')) { - if (!$this->account->hasPermission('access site in maintenance mode')) { - return TRUE; - } - } - return FALSE; - } - - /** - * Wraps the drupal_set_message function. - */ - protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) { - return drupal_set_message($message, $type, $repeat); } /** @@ -184,5 +67,4 @@ static function getSubscribedEvents() { $events[KernelEvents::REQUEST][] = array('onKernelRequestMaintenance', 30); return $events; } - } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 62cdd06..87d0a04 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -11,6 +11,7 @@ use Drupal\Component\Serialization\Yaml; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -799,6 +800,22 @@ public function install(array $module_list, $enable_dependencies = TRUE) { } drupal_set_installed_schema_version($module, $version); + // Install any entity schemas belonging to the module. + $entity_manager = \Drupal::entityManager(); + $schema = \Drupal::database()->schema(); + foreach ($entity_manager->getDefinitions() as $entity_type) { + if ($entity_type->getProvider() == $module) { + $storage = $entity_manager->getStorage($entity_type->id()); + if ($storage instanceof EntitySchemaProviderInterface) { + foreach ($storage->getSchema() as $table_name => $table_schema) { + if (!$schema->tableExists($table_name)) { + $schema->createTable($table_name, $table_schema); + } + } + } + } + } + // Record the fact that it was installed. $modules_installed[] = $module; @@ -891,6 +908,22 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove all configuration belonging to the module. \Drupal::service('config.manager')->uninstall('module', $module); + // Remove any entity schemas belonging to the module. + $entity_manager = \Drupal::entityManager(); + $schema = \Drupal::database()->schema(); + foreach ($entity_manager->getDefinitions() as $entity_type) { + if ($entity_type->getProvider() == $module) { + $storage = $entity_manager->getStorage($entity_type->id()); + if ($storage instanceof EntitySchemaProviderInterface) { + foreach ($storage->getSchema() as $table_name => $table_schema) { + if ($schema->tableExists($table_name)) { + $schema->dropTable($table_name); + } + } + } + } + } + // Remove the schema. drupal_uninstall_schema($module); diff --git a/core/lib/Drupal/Core/Field/FieldDefinition.php b/core/lib/Drupal/Core/Field/FieldDefinition.php index cfeccf8..a7837a0 100644 --- a/core/lib/Drupal/Core/Field/FieldDefinition.php +++ b/core/lib/Drupal/Core/Field/FieldDefinition.php @@ -543,4 +543,12 @@ public function setCustomStorage($custom_storage) { return $this; } + /** + * {@inheritdoc} + */ + public function getUniqueStorageIdentifier() { + return $this->getTargetEntityTypeId() . '-' . $this->getName(); + } + + } diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php index aa1d90e..b8961dc 100644 --- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php @@ -285,4 +285,10 @@ public function getProvider(); */ public function hasCustomStorage(); + /** + * @todo + * @return string + */ + public function getUniqueStorageIdentifier(); + } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index 1697c16..a36c98c 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Field\Plugin\Field\FieldType; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\FieldItemBase; @@ -114,7 +115,9 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) 'target_id' => array( 'description' => 'The ID of the target entity.', 'type' => 'varchar', - 'length' => '255', + // If the target entities act as bundles for another entity type, + // their IDs should not exceed the maximum length for bundles. + 'length' => $target_type_info->getBundleOf() ? EntityTypeInterface::BUNDLE_MAX_LENGTH : 255, ), ); } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php index 2bafa8b..2d3fa30 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php @@ -41,6 +41,9 @@ public static function defaultInstanceSettings() { 'max' => '', 'prefix' => '', 'suffix' => '', + // Valid size property values include: 'tiny', 'small', 'medium', 'normal' + // and 'big'. + 'size' => 'normal', ) + parent::defaultInstanceSettings(); } @@ -91,6 +94,9 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) 'not null' => FALSE, // Expose the 'unsigned' setting in the field item schema. 'unsigned' => $field_definition->getSetting('unsigned'), + // Expose the 'size' setting in the field item schema. For instance, + // supply 'big' as a value to produce a 'bigint' type. + 'size' => $field_definition->getSetting('size'), ), ), ); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php index 693e4ce..dbef995 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php @@ -52,7 +52,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) return array( 'columns' => array( 'value' => array( - 'type' => 'text', + 'type' => 'varchar', + 'length' => (int) $field_definition->getSetting('max_length'), 'not null' => TRUE, ), ), diff --git a/core/lib/Drupal/Core/Path/AliasManager.php b/core/lib/Drupal/Core/Path/AliasManager.php index e3b686b..55d2ddc 100644 --- a/core/lib/Drupal/Core/Path/AliasManager.php +++ b/core/lib/Drupal/Core/Path/AliasManager.php @@ -7,13 +7,10 @@ namespace Drupal\Core\Path; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\CacheDecorator\CacheDecoratorInterface; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; -use Drupal\Core\Language\LanguageManagerInterface; -class AliasManager implements AliasManagerInterface, CacheDecoratorInterface { +class AliasManager implements AliasManagerInterface { /** * The alias storage service. @@ -23,27 +20,6 @@ class AliasManager implements AliasManagerInterface, CacheDecoratorInterface { protected $storage; /** - * Cache backend service. - * - * @var \Drupal\Core\Cache\CacheBackendInterface; - */ - protected $cache; - - /** - * The cache key to use when caching paths. - * - * @var string - */ - protected $cacheKey; - - /** - * Whether the cache needs to be written. - * - * @var boolean - */ - protected $cacheNeedsWriting = FALSE; - - /** * Language manager for retrieving the default langcode when none is specified. * * @var \Drupal\Core\Language\LanguageManager @@ -88,12 +64,12 @@ class AliasManager implements AliasManagerInterface, CacheDecoratorInterface { /** * Holds an array of previously looked up paths for the current request path. * - * This will only get populated if a cache key has been set, which for example - * happens if the alias manager is used in the context of a request. + * This will only ever get populated if the alias manager is being used in + * the context of a request. * * @var array */ - protected $preloadedPathLookups = FALSE; + protected $preloadedPathLookups = array(); /** * Constructs an AliasManager. @@ -102,52 +78,13 @@ class AliasManager implements AliasManagerInterface, CacheDecoratorInterface { * The alias storage service. * @param \Drupal\Core\Path\AliasWhitelistInterface $whitelist * The whitelist implementation to use. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * @param \Drupal\Core\Language\LanguageManager $language_manager * The language manager. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache - * Cache backend. */ - public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) { + public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManager $language_manager) { $this->storage = $storage; $this->languageManager = $language_manager; $this->whitelist = $whitelist; - $this->cache = $cache; - } - - /** - * {@inheritdoc} - */ - public function setCacheKey($key) { - // Prefix the cache key to avoid clashes with other caches. - $this->cacheKey = 'preload-paths:' . $key; - } - - /** - * {@inheritdoc} - * - * Cache an array of the paths available on each page. We assume that aliases - * will be needed for the majority of these paths during subsequent requests, - * and load them in a single query during path alias lookup. - */ - public function writeCache() { - // Check if the paths for this page were loaded from cache in this request - // to avoid writing to cache on every request. - if ($this->cacheNeedsWriting && !empty($this->cacheKey)) { - // Start with the preloaded path lookups, so that cached entries for other - // languages will not be lost. - $path_lookups_lookups = $this->preloadedPathLookups ?: array(); - foreach ($this->lookupMap as $langcode => $lookups) { - $path_lookups[$langcode] = array_keys($lookups); - if (!empty($this->noAlias[$langcode])) { - $path_lookups[$langcode] = array_merge($path_lookups[$langcode], array_keys($this->noAlias[$langcode])); - } - } - - if (!empty($path_lookups)) { - $twenty_four_hours_four_hours = 60 * 60 * 24; - $this->cache->set($this->cacheKey, $path_lookups, REQUEST_TIME + $twenty_four_hours); - } - } } /** @@ -173,14 +110,12 @@ public function getPathByAlias($alias, $langcode = NULL) { // Look for path in storage. if ($path = $this->storage->lookupPathSource($alias, $langcode)) { $this->lookupMap[$langcode][$path] = $alias; - $this->cacheNeedsWriting = TRUE; return $path; } // We can't record anything into $this->lookupMap because we didn't find any // paths for this alias. Thus cache to $this->noPath. $this->noPath[$langcode][$alias] = TRUE; - return $alias; } @@ -206,21 +141,11 @@ public function getAliasByPath($path, $langcode = NULL) { if (empty($this->langcodePreloaded[$langcode])) { $this->langcodePreloaded[$langcode] = TRUE; $this->lookupMap[$langcode] = array(); - - // Load the cached paths that should be used for preloading. This only - // happens if a cache key has been set. - if ($this->preloadedPathLookups === FALSE) { - $this->preloadedPathLookups = array(); - if ($this->cacheKey && $cached = $this->cache->get($this->cacheKey)) { - $this->preloadedPathLookups = $cached->data; - } - } - // Load paths from cache. - if (!empty($this->preloadedPathLookups[$langcode])) { - $this->lookupMap[$langcode] = $this->storage->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode); + if (!empty($this->preloadedPathLookups)) { + $this->lookupMap[$langcode] = $this->storage->preloadPathAlias($this->preloadedPathLookups, $langcode); // Keep a record of paths with no alias to avoid querying twice. - $this->noAlias[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode]))); + $this->noAlias[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups, array_keys($this->lookupMap[$langcode]))); } } @@ -237,14 +162,12 @@ public function getAliasByPath($path, $langcode = NULL) { // Try to load alias from storage. if ($alias = $this->storage->lookupPathAlias($path, $langcode)) { $this->lookupMap[$langcode][$path] = $alias; - $this->cacheNeedsWriting = TRUE; return $alias; } // We can't record anything into $this->lookupMap because we didn't find any // aliases for this path. Thus cache to $this->noAlias. $this->noAlias[$langcode][$path] = TRUE; - $this->cacheNeedsWriting = TRUE; return $path; } @@ -264,24 +187,41 @@ public function cacheClear($source = NULL) { $this->noAlias = array(); $this->langcodePreloaded = array(); $this->preloadedPathLookups = array(); - $this->cache->delete($this->cacheKey); $this->pathAliasWhitelistRebuild($source); } /** + * Implements \Drupal\Core\Path\AliasManagerInterface::getPathLookups(). + */ + public function getPathLookups() { + $current = current($this->lookupMap); + if ($current) { + return array_keys($current); + } + return array(); + } + + /** + * Implements \Drupal\Core\Path\AliasManagerInterface::preloadPathLookups(). + */ + public function preloadPathLookups(array $path_list) { + $this->preloadedPathLookups = $path_list; + } + + /** * Rebuild the path alias white list. * - * @param string $path - * An optional path for which an alias is being inserted. + * @param $source + * An optional system path for which an alias is being inserted. * * @return * An array containing a white list of path aliases. */ - protected function pathAliasWhitelistRebuild($path = NULL) { - // When paths are inserted, only rebuild the whitelist if the path has a top - // level component which is not already in the whitelist. - if (!empty($path)) { - if ($this->whitelist->get(strtok($path, '/'))) { + protected function pathAliasWhitelistRebuild($source = NULL) { + // When paths are inserted, only rebuild the whitelist if the system path + // has a top level component which is not already in the whitelist. + if (!empty($source)) { + if ($this->whitelist->get(strtok($source, '/'))) { return; } } diff --git a/core/lib/Drupal/Core/Path/AliasManagerInterface.php b/core/lib/Drupal/Core/Path/AliasManagerInterface.php index 2338846..d37db97 100644 --- a/core/lib/Drupal/Core/Path/AliasManagerInterface.php +++ b/core/lib/Drupal/Core/Path/AliasManagerInterface.php @@ -36,6 +36,23 @@ public function getPathByAlias($alias, $langcode = NULL); public function getAliasByPath($path, $langcode = NULL); /** + * Returns an array of system paths that have been looked up. + * + * @return array + * An array of all system paths that have been looked up during the current + * request. + */ + public function getPathLookups(); + + /** + * Preload a set of paths for bulk alias lookups. + * + * @param $path_list + * An array of system paths. + */ + public function preloadPathLookups(array $path_list); + + /** * Clear internal caches in alias manager. * * @param $source diff --git a/core/modules/aggregator/aggregator.install b/core/modules/aggregator/aggregator.install index e4bef0e..2b755e1 100644 --- a/core/modules/aggregator/aggregator.install +++ b/core/modules/aggregator/aggregator.install @@ -22,179 +22,3 @@ function aggregator_requirements($phase) { } return $requirements; } - -/** - * Implements hook_schema(). - */ -function aggregator_schema() { - $schema['aggregator_feed'] = array( - 'description' => 'Stores feeds to be parsed by the aggregator.', - 'fields' => array( - 'fid' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique feed ID.', - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'title' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Title of the feed.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this feed.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'url' => array( - 'type' => 'text', - 'not null' => TRUE, - 'description' => 'URL to the feed.', - ), - 'refresh' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'How often to check for new feed items, in seconds.', - ), - 'checked' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Last time feed was checked for new items, as Unix timestamp.', - ), - 'queued' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Time when this feed was queued for refresh, 0 if not queued.', - ), - 'link' => array( - 'type' => 'text', - 'not null' => TRUE, - 'description' => 'The parent website of the feed; comes from the element in the feed.', - ), - 'description' => array( - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - 'description' => "The parent website's description; comes from the element in the feed.", - ), - 'image' => array( - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - 'description' => 'An image representing the feed.', - ), - 'hash' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Calculated hash of the feed data, used for validating cache.', - ), - 'etag' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Entity tag HTTP response header, used for validating cache.', - ), - 'modified' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'When the feed was last modified, as a Unix timestamp.', - ), - ), - 'primary key' => array('fid'), - 'indexes' => array( - 'url' => array(array('url', 255)), - 'queued' => array('queued'), - ), - 'unique keys' => array( - 'title' => array('title'), - ), - ); - - $schema['aggregator_item'] = array( - 'description' => 'Stores the individual items imported from feeds.', - 'fields' => array( - 'iid' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique ID for feed item.', - ), - 'fid' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The {aggregator_feed}.fid to which this item belongs.', - ), - 'title' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Title of the feed item.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this feed item.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'link' => array( - 'type' => 'text', - 'not null' => TRUE, - 'description' => 'Link to the feed item.', - ), - 'author' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Author of the feed item.', - ), - 'description' => array( - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - 'description' => 'Body of the feed item.', - ), - 'timestamp' => array( - 'type' => 'int', - 'not null' => FALSE, - 'description' => 'Posted date of the feed item, as a Unix timestamp.', - ), - 'guid' => array( - 'type' => 'text', - 'not null' => TRUE, - 'description' => 'Unique identifier for the feed item.', - ) - ), - 'primary key' => array('iid'), - 'indexes' => array( - 'fid' => array('fid'), - 'timestamp' => array('timestamp'), - ), - 'foreign keys' => array( - 'aggregator_feed' => array( - 'table' => 'aggregator_feed', - 'columns' => array('fid' => 'fid'), - ), - ), - ); - - return $schema; -} diff --git a/core/modules/aggregator/src/Entity/Feed.php b/core/modules/aggregator/src/Entity/Feed.php index 24430b9..4222152 100644 --- a/core/modules/aggregator/src/Entity/Feed.php +++ b/core/modules/aggregator/src/Entity/Feed.php @@ -153,7 +153,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('URL')) ->setDescription(t('The fully-qualified URL of the feed.')) ->setRequired(TRUE) - ->setSetting('max_length', NULL) ->setDisplayOptions('form', array( 'type' => 'uri', 'weight' => -3, @@ -176,11 +175,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['checked'] = FieldDefinition::create('timestamp') ->setLabel(t('Checked')) - ->setDescription(t('Last time feed was checked for new items, as Unix timestamp.')); + ->setDescription(t('Last time feed was checked for new items, as Unix timestamp.')) + ->setSetting('default_value', 0); $fields['queued'] = FieldDefinition::create('timestamp') ->setLabel(t('Queued')) - ->setDescription(t('Time when this feed was queued for refresh, 0 if not queued.')); + ->setDescription(t('Time when this feed was queued for refresh, 0 if not queued.')) + ->setSetting('default_value', 0); $fields['link'] = FieldDefinition::create('uri') ->setLabel(t('Link')) diff --git a/core/modules/aggregator/src/Entity/Item.php b/core/modules/aggregator/src/Entity/Item.php index bb9e0aa..162bb19 100644 --- a/core/modules/aggregator/src/Entity/Item.php +++ b/core/modules/aggregator/src/Entity/Item.php @@ -71,8 +71,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Author')) ->setDescription(t('The author of the feed item.')); - // @todo Convert to a text field in https://drupal.org/node/2149845. - $fields['description'] = FieldDefinition::create('string') + $fields['description'] = FieldDefinition::create('string_long') ->setLabel(t('Description')) ->setDescription(t('The body of the feed item.')); @@ -81,7 +80,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(t('Posted date of the feed item, as a Unix timestamp.')); // @todo Convert to a real UUID field in https://drupal.org/node/2149851. - $fields['guid'] = FieldDefinition::create('string') + $fields['guid'] = FieldDefinition::create('string_long') ->setLabel(t('GUID')) ->setDescription(t('Unique identifier for the feed item.')); diff --git a/core/modules/aggregator/src/FeedForm.php b/core/modules/aggregator/src/FeedForm.php index 675627c..1770f03 100644 --- a/core/modules/aggregator/src/FeedForm.php +++ b/core/modules/aggregator/src/FeedForm.php @@ -66,7 +66,12 @@ public function save(array $form, array &$form_state) { $feed->save(); if ($insert) { drupal_set_message($this->t('The feed %feed has been updated.', array('%feed' => $feed->label()))); - $form_state['redirect_route'] = $feed->urlInfo('canonical'); + if (arg(0) == 'admin') { + $form_state['redirect_route']['route_name'] = 'aggregator.admin_overview'; + } + else { + $form_state['redirect_route'] = $feed->urlInfo('canonical'); + } } else { watchdog('aggregator', 'Feed %feed added.', array('%feed' => $feed->label()), WATCHDOG_NOTICE, l($this->t('View'), 'admin/config/services/aggregator')); diff --git a/core/modules/aggregator/src/FeedStorage.php b/core/modules/aggregator/src/FeedStorage.php index 5824240..da784f5 100644 --- a/core/modules/aggregator/src/FeedStorage.php +++ b/core/modules/aggregator/src/FeedStorage.php @@ -21,6 +21,29 @@ class FeedStorage extends ContentEntityDatabaseStorage implements FeedStorageInt /** * {@inheritdoc} */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['aggregator_feed']['fields']['url']['not null'] = TRUE; + $schema['aggregator_feed']['fields']['queued']['not null'] = TRUE; + $schema['aggregator_feed']['fields']['title']['not null'] = TRUE; + + $schema['aggregator_feed']['indexes'] += array( + 'aggregator_feed__url' => array(array('url', 255)), + 'aggregator_feed__queued' => array('queued'), + ); + $schema['aggregator_feed']['unique keys'] += array( + 'aggregator_feed__title' => array('title'), + ); + + return $schema; + } + + /** + * {@inheritdoc} + */ public function getFeedDuplicates(FeedInterface $feed) { $query = \Drupal::entityQuery('aggregator_feed'); diff --git a/core/modules/aggregator/src/Form/FeedDeleteForm.php b/core/modules/aggregator/src/Form/FeedDeleteForm.php index 26c6423..eacabe9 100644 --- a/core/modules/aggregator/src/Form/FeedDeleteForm.php +++ b/core/modules/aggregator/src/Form/FeedDeleteForm.php @@ -43,7 +43,12 @@ public function submit(array $form, array &$form_state) { $this->entity->delete(); watchdog('aggregator', 'Feed %feed deleted.', array('%feed' => $this->entity->label())); drupal_set_message($this->t('The feed %feed has been deleted.', array('%feed' => $this->entity->label()))); - $form_state['redirect_route'] = new Url('aggregator.sources'); + if (arg(0) == 'admin') { + $form_state['redirect_route'] = $this->getCancelRoute(); + } + else { + $form_state['redirect_route'] = new Url('aggregator.sources'); + } } } diff --git a/core/modules/aggregator/src/ItemStorage.php b/core/modules/aggregator/src/ItemStorage.php index bedea41..f2b4aa6 100644 --- a/core/modules/aggregator/src/ItemStorage.php +++ b/core/modules/aggregator/src/ItemStorage.php @@ -22,6 +22,29 @@ class ItemStorage extends ContentEntityDatabaseStorage implements ItemStorageInt /** * {@inheritdoc} */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['aggregator_item']['fields']['timestamp']['not null'] = TRUE; + + $schema['aggregator_item']['indexes'] += array( + 'aggregator_item__timestamp' => array('timestamp'), + ); + $schema['aggregator_item']['foreign keys'] += array( + 'aggregator_item__aggregator_feed' => array( + 'table' => 'aggregator_feed', + 'columns' => array('fid' => 'fid'), + ), + ); + + return $schema; + } + + /** + * {@inheritdoc} + */ public function getItemCount(FeedInterface $feed) { $query = \Drupal::entityQuery('aggregator_item') ->condition('fid', $feed->id()) diff --git a/core/modules/aggregator/src/Tests/FeedParserTest.php b/core/modules/aggregator/src/Tests/FeedParserTest.php index 96ee61f..5d4199f 100644 --- a/core/modules/aggregator/src/Tests/FeedParserTest.php +++ b/core/modules/aggregator/src/Tests/FeedParserTest.php @@ -87,7 +87,7 @@ function testHtmlEntitiesSample() { function testRedirectFeed() { // Simulate a typo in the URL to force a curl exception. $invalid_url = url('aggregator/redirect', array('absolute' => TRUE)); - $feed = entity_create('aggregator_feed', array('url' => $invalid_url)); + $feed = entity_create('aggregator_feed', array('url' => $invalid_url, 'title' => $this->randomName())); $feed->save(); $feed->refreshItems(); diff --git a/core/modules/aggregator/src/Tests/Views/IntegrationTest.php b/core/modules/aggregator/src/Tests/Views/IntegrationTest.php index 3f4d7b5..bd8f17c 100644 --- a/core/modules/aggregator/src/Tests/Views/IntegrationTest.php +++ b/core/modules/aggregator/src/Tests/Views/IntegrationTest.php @@ -21,7 +21,7 @@ class IntegrationTest extends ViewUnitTestBase { * * @var array */ - public static $modules = array('aggregator', 'aggregator_test_views', 'system', 'entity', 'field'); + public static $modules = array('aggregator', 'aggregator_test_views', 'system', 'entity', 'field', 'options'); /** * Views used by this test. @@ -55,7 +55,8 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->installSchema('aggregator', array('aggregator_item', 'aggregator_feed')); + $this->installEntitySchema('aggregator_item'); + $this->installEntitySchema('aggregator_feed'); ViewTestData::createTestViews(get_class($this), array('aggregator_test_views')); diff --git a/core/modules/block/custom_block/custom_block.install b/core/modules/block/custom_block/custom_block.install deleted file mode 100644 index 6f85a6a..0000000 --- a/core/modules/block/custom_block/custom_block.install +++ /dev/null @@ -1,125 +0,0 @@ - 'Stores contents of custom-made blocks.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => "The block's {custom_block}.id.", - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'info' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Block description.', - ), - // Defaults to NULL in order to avoid a brief period of potential - // deadlocks on the index. - 'revision_id' => array( - 'description' => 'The current {block_custom_revision}.revision_id version identifier.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - ), - 'type' => array( - 'description' => 'The type of this custom block.', - 'type' => 'varchar', - 'length' => EntityTypeInterface::BUNDLE_MAX_LENGTH, - 'not null' => TRUE, - 'default' => '', - ), - 'changed' => array( - 'description' => 'The Unix timestamp when the custom block was most recently saved.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this node.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'primary key' => array('id'), - 'indexes' => array( - 'block_custom_type' => array('type'), - ), - 'unique keys' => array( - 'revision_id' => array('revision_id'), - 'uuid' => array('uuid'), - 'info' => array('info'), - ), - 'foreign keys' => array( - 'custom_block_revision' => array( - 'table' => 'custom_block_revision', - 'columns' => array('revision_id' => 'revision_id'), - ), - ), - ); - - $schema['custom_block_revision'] = array( - 'description' => 'Stores contents of custom-made blocks.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => "The block's {custom_block}.id.", - ), - // Defaults to NULL in order to avoid a brief period of potential - // deadlocks on the index. - 'revision_id' => array( - 'description' => 'The current version identifier.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'log' => array( - 'description' => 'The log entry explaining the changes in this version.', - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - ), - 'info' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Block description.', - ), - 'changed' => array( - 'description' => 'The Unix timestamp when the version was most recently saved.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'primary key' => array('revision_id'), - ); - return $schema; -} diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorage.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorage.php new file mode 100644 index 0000000..b7b8711 --- /dev/null +++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorage.php @@ -0,0 +1,34 @@ + array('info'), + ); + + return $schema; + } + +} diff --git a/core/modules/block/custom_block/src/Entity/CustomBlock.php b/core/modules/block/custom_block/src/Entity/CustomBlock.php index 71913b7..585026e 100644 --- a/core/modules/block/custom_block/src/Entity/CustomBlock.php +++ b/core/modules/block/custom_block/src/Entity/CustomBlock.php @@ -21,6 +21,7 @@ * label = @Translation("Custom Block"), * bundle_label = @Translation("Custom Block type"), * controllers = { + * "storage" = "Drupal\custom_block\CustomBlockStorage", * "access" = "Drupal\custom_block\CustomBlockAccessController", * "list_builder" = "Drupal\custom_block\CustomBlockListBuilder", * "view_builder" = "Drupal\custom_block\CustomBlockViewBuilder", @@ -186,7 +187,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setSetting('target_type', 'custom_block_type') ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH); - $fields['log'] = FieldDefinition::create('string') + $fields['log'] = FieldDefinition::create('string_long') ->setLabel(t('Revision log message')) ->setDescription(t('The revision log message.')) ->setRevisionable(TRUE); diff --git a/core/modules/comment/comment.install b/core/modules/comment/comment.install index 3bfbcd0..0c35aae 100644 --- a/core/modules/comment/comment.install +++ b/core/modules/comment/comment.install @@ -32,152 +32,6 @@ function comment_install() { * Implements hook_schema(). */ function comment_schema() { - $schema['comment'] = array( - 'description' => 'Stores comments and associated data.', - 'fields' => array( - 'cid' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique comment ID.', - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'pid' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The {comment}.cid to which this comment is a reply. If set to 0, this comment is not a reply to an existing comment.', - ), - 'entity_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The entity_id of the entity to which this comment is a reply.', - ), - 'entity_type' => array( - 'type' => 'varchar', - 'not null' => TRUE, - 'default' => 'node', - 'length' => 255, - 'description' => 'The entity_type of the entity to which this comment is a reply.', - ), - 'field_id' => array( - 'type' => 'varchar', - 'not null' => TRUE, - 'default' => 'node.comment', - 'length' => 255, - 'description' => 'The field_id of the field that was used to add this comment.', - ), - 'uid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.', - ), - 'subject' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The comment title.', - ), - 'hostname' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => "The author's host name.", - ), - 'created' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The time that the comment was created, as a Unix timestamp.', - ), - 'changed' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The time that the comment was last edited, as a Unix timestamp.', - ), - 'status' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 1, - 'size' => 'tiny', - 'description' => 'The published status of a comment. (0 = Not Published, 1 = Published)', - ), - 'thread' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'description' => "The alphadecimal representation of the comment's place in a thread, consisting of a base 36 string prefixed by an integer indicating its length.", - ), - 'name' => array( - 'type' => 'varchar', - 'length' => 60, - 'not null' => FALSE, - 'description' => "The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form.", - ), - 'mail' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => FALSE, - 'description' => "The comment author's e-mail address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.", - ), - 'homepage' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - 'description' => "The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.", - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this comment.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'indexes' => array( - 'comment_status_pid' => array('pid', 'status'), - 'comment_num_new' => array( - 'entity_id', - array('entity_type', 32), - array('field_id', 32), - 'status', - 'created', - 'cid', - 'thread', - ), - 'comment_uid' => array('uid'), - 'comment_entity_langcode' => array( - 'entity_id', - array('entity_type', 32), - array('field_id', 32), - 'langcode', - ), - 'comment_created' => array('created'), - ), - 'primary key' => array('cid'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), - 'foreign keys' => array( - 'comment_author' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ), - ); - $schema['comment_entity_statistics'] = array( 'description' => 'Maintains statistics of entity and comments posts to show "new" and "updated" flags.', 'fields' => array( diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php index 88a1bd4..5526d2b 100644 --- a/core/modules/comment/src/CommentStorage.php +++ b/core/modules/comment/src/CommentStorage.php @@ -127,4 +127,50 @@ public function getChildCids(array $comments) { ->fetchCol(); } + /** + * {@inheritdoc} + */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['comment']['fields']['pid']['not null'] = TRUE; + $schema['comment']['fields']['status']['not null'] = TRUE; + $schema['comment']['fields']['entity_id']['not null'] = TRUE; + $schema['comment']['fields']['field_id']['not null'] = TRUE; + $schema['comment']['fields']['created']['not null'] = TRUE; + $schema['comment']['fields']['thread']['not null'] = TRUE; + + unset($schema['comment']['indexes']['field__pid']); + unset($schema['comment']['indexes']['field__entity_id']); + $schema['comment']['indexes'] += array( + 'comment__status_pid' => array('pid', 'status'), + 'comment__num_new' => array( + 'entity_id', + array('entity_type', 32), + array('field_id', 32), + 'status', + 'created', + 'cid', + 'thread', + ), + 'comment__entity_langcode' => array( + 'entity_id', + array('entity_type', 32), + array('field_id', 32), + 'langcode', + ), + 'comment__created' => array('created'), + ); + $schema['comment']['foreign keys'] += array( + 'comment__author' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + ); + + return $schema; + } + } diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php index de10346..9b4707f 100644 --- a/core/modules/comment/src/Entity/Comment.php +++ b/core/modules/comment/src/Entity/Comment.php @@ -273,7 +273,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['status'] = FieldDefinition::create('boolean') ->setLabel(t('Publishing status')) - ->setDescription(t('A boolean indicating whether the comment is published.')); + ->setDescription(t('A boolean indicating whether the comment is published.')) + ->setSetting('default_value', TRUE); $fields['thread'] = FieldDefinition::create('string') ->setLabel(t('Thread place')) diff --git a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php index 3fe2a3f..ff8c05a 100644 --- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php +++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php @@ -47,7 +47,8 @@ public function setUp() { $current_user->setAccount($this->createUser(array(), array('access comments'))); // Install tables and config needed to render comments. - $this->installSchema('comment', array('comment', 'comment_entity_statistics')); + $this->installSchema('comment', array('comment_entity_statistics')); + $this->installEntitySchema('comment'); $this->installConfig(array('system', 'filter')); // Comment rendering generates links, so build the router. diff --git a/core/modules/comment/src/Tests/CommentValidationTest.php b/core/modules/comment/src/Tests/CommentValidationTest.php index 03a0e82..b56ed6b 100644 --- a/core/modules/comment/src/Tests/CommentValidationTest.php +++ b/core/modules/comment/src/Tests/CommentValidationTest.php @@ -38,7 +38,8 @@ public static function getInfo() { */ public function setUp() { parent::setUp(); - $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision')); + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); $this->installSchema('comment', array('comment_entity_statistics')); } diff --git a/core/modules/config/src/Tests/ConfigImportRecreateTest.php b/core/modules/config/src/Tests/ConfigImportRecreateTest.php index 3ce6b12..9afec8c 100644 --- a/core/modules/config/src/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/src/Tests/ConfigImportRecreateTest.php @@ -29,7 +29,7 @@ class ConfigImportRecreateTest extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('system', 'entity', 'field', 'text', 'node'); + public static $modules = array('system', 'entity', 'field', 'text', 'user', 'node'); public static function getInfo() { return array( @@ -42,7 +42,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('node', 'node'); + $this->installEntitySchema('node'); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); diff --git a/core/modules/config/src/Tests/ConfigImportRenameValidationTest.php b/core/modules/config/src/Tests/ConfigImportRenameValidationTest.php index fbffae7..7e8024a 100644 --- a/core/modules/config/src/Tests/ConfigImportRenameValidationTest.php +++ b/core/modules/config/src/Tests/ConfigImportRenameValidationTest.php @@ -32,7 +32,7 @@ class ConfigImportRenameValidationTest extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('system', 'node', 'field', 'text', 'entity', 'config_test'); + public static $modules = array('system', 'user', 'node', 'field', 'text', 'entity', 'config_test'); /** * {@inheritdoc} @@ -51,7 +51,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('node', 'node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( diff --git a/core/modules/dblog/dblog.libraries.yml b/core/modules/dblog/dblog.libraries.yml deleted file mode 100644 index d871d09..0000000 --- a/core/modules/dblog/dblog.libraries.yml +++ /dev/null @@ -1,5 +0,0 @@ -drupal.dblog: - version: VERSION - css: - component: - css/dblog.module.css: {} diff --git a/core/modules/dblog/dblog.module b/core/modules/dblog/dblog.module index 6333187..1eb61b0 100644 --- a/core/modules/dblog/dblog.module +++ b/core/modules/dblog/dblog.module @@ -53,6 +53,15 @@ function dblog_menu_link_defaults_alter(&$links) { } /** + * Implements hook_page_build(). + */ +function dblog_page_build(&$page) { + if (arg(0) == 'admin' && arg(1) == 'reports') { + $page['#attached']['css'][] = drupal_get_path('module', 'dblog') . '/css/dblog.module.css'; + } +} + +/** * Implements hook_cron(). * * Controls the size of the log table, paring it to 'dblog_row_limit' messages. diff --git a/core/modules/dblog/src/Controller/DbLogController.php b/core/modules/dblog/src/Controller/DbLogController.php index 1f9f48d..72ddf22 100644 --- a/core/modules/dblog/src/Controller/DbLogController.php +++ b/core/modules/dblog/src/Controller/DbLogController.php @@ -210,9 +210,6 @@ public function overview() { '#rows' => $rows, '#attributes' => array('id' => 'admin-dblog', 'class' => array('admin-dblog')), '#empty' => $this->t('No log messages available.'), - '#attached' => array( - 'library' => array('dblog/drupal.dblog'), - ), ); $build['dblog_pager'] = array('#theme' => 'pager'); @@ -286,9 +283,6 @@ public function eventDetails($event_id) { '#type' => 'table', '#rows' => $rows, '#attributes' => array('class' => array('dblog-event')), - '#attached' => array( - 'library' => array('dblog/drupal.dblog'), - ), ); } @@ -389,9 +383,6 @@ public function topLogMessages($type) { '#header' => $header, '#rows' => $rows, '#empty' => $this->t('No log messages available.'), - '#attached' => array( - 'library' => array('dblog/drupal.dblog'), - ), ); $build['dblog_top_pager'] = array('#theme' => 'pager'); diff --git a/core/modules/editor/src/Tests/EditorFileUsageTest.php b/core/modules/editor/src/Tests/EditorFileUsageTest.php index 3be50ec..184d849 100644 --- a/core/modules/editor/src/Tests/EditorFileUsageTest.php +++ b/core/modules/editor/src/Tests/EditorFileUsageTest.php @@ -31,8 +31,10 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('node', array('node', 'node_access', 'node_field_data', 'node_field_revision', 'node_revision')); - $this->installSchema('file', array('file_managed', 'file_usage')); + $this->installEntitySchema('node'); + $this->installEntitySchema('file'); + $this->installSchema('node', array('node_access')); + $this->installSchema('file', array('file_usage')); // Add text formats. $filtered_html_format = entity_create('filter_format', array( diff --git a/core/modules/entity/src/Tests/EntityDisplayTest.php b/core/modules/entity/src/Tests/EntityDisplayTest.php index 77745f2..250f5e0 100644 --- a/core/modules/entity/src/Tests/EntityDisplayTest.php +++ b/core/modules/entity/src/Tests/EntityDisplayTest.php @@ -268,7 +268,7 @@ public function testBaseFieldComponent() { */ public function testRenameDeleteBundle() { $this->enableModules(array('field_test', 'node', 'system', 'text')); - $this->installSchema('node', array('node')); + $this->installEntitySchema('node'); // Create a node bundle, display and form display object. entity_create('node_type', array('type' => 'article'))->save(); diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceFieldTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceFieldTest.php index bde0b2c..b3905bc 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceFieldTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceFieldTest.php @@ -76,7 +76,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision')); + $this->installEntitySchema('entity_test_rev'); // Setup a field and instance. entity_reference_create_instance( diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceItemTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceItemTest.php index 8c33d47..0586e7c 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceItemTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceItemTest.php @@ -52,8 +52,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('taxonomy', 'taxonomy_term_data'); - $this->installSchema('taxonomy', 'taxonomy_term_hierarchy'); + $this->installEntitySchema('taxonomy_term'); $this->vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => $this->randomName(), diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc index a19621d..fbeb80d 100644 --- a/core/modules/field/field.purge.inc +++ b/core/modules/field/field.purge.inc @@ -80,7 +80,6 @@ function field_purge_batch($batch_size, $field_uuid = NULL) { else { $instances = entity_load_multiple_by_properties('field_instance_config', array('deleted' => TRUE, 'include_deleted' => TRUE)); } - $factory = \Drupal::service('entity.query'); $info = \Drupal::entityManager()->getDefinitions(); foreach ($instances as $instance) { $entity_type = $instance->entity_type; @@ -92,36 +91,16 @@ function field_purge_batch($batch_size, $field_uuid = NULL) { continue; } - $ids = (object) array( - 'entity_type' => $entity_type, - 'bundle' => $instance->bundle, - ); - // Retrieve some entities. - $query = $factory->get($entity_type) - ->condition('id:' . $instance->getField()->uuid() . '.deleted', 1) - ->range(0, $batch_size); - // If there's no bundle key, all results will have the same bundle. - if ($bundle_key = $info[$entity_type]->getKey('bundle')) { - $query->condition($bundle_key, $ids->bundle); - } - $results = $query->execute(); - if ($results) { - foreach ($results as $revision_id => $entity_id) { - $ids->revision_id = $revision_id; - $ids->entity_id = $entity_id; - $entity = _field_create_entity_from_ids($ids); - \Drupal::entityManager()->getStorage($entity_type)->onFieldItemsPurge($entity, $instance); - $batch_size--; - } - // Only delete up to the maximum number of records. - if ($batch_size == 0) { - break; - } - } - else { + $count_purged = \Drupal::entityManager()->getStorage($entity_type)->purgeFieldData($instance, $batch_size); + if ($count_purged < $batch_size) { // No field data remains for the instance, so we can remove it. field_purge_instance($instance); } + $batch_size -= $count_purged; + // Only delete up to the maximum number of records. + if ($batch_size == 0) { + break; + } } // Retrieve all deleted fields. Any that have no instances can be purged. @@ -187,7 +166,7 @@ function field_purge_field($field) { $state->set('field.field.deleted', $deleted_fields); // Notify the storage layer. - \Drupal::entityManager()->getStorage($field->entity_type)->onFieldPurge($field); + \Drupal::entityManager()->getStorage($field->entity_type)->finalizePurge($field); // Invoke external hooks after the cache is cleared for API consistency. \Drupal::moduleHandler()->invokeAll('field_purge_field', array($field)); diff --git a/core/modules/field/src/ConfigImporterFieldPurger.php b/core/modules/field/src/ConfigImporterFieldPurger.php index db3d8b9..a83ff52 100644 --- a/core/modules/field/src/ConfigImporterFieldPurger.php +++ b/core/modules/field/src/ConfigImporterFieldPurger.php @@ -75,7 +75,8 @@ protected static function initializeSandbox(array &$context, ConfigImporter $con $context['sandbox']['field']['steps_to_delete'] = 0; $fields = static::getFieldsToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete')); foreach ($fields as $field) { - $row_count = $field->entityCount(); + $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 diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index 3f5b787..838488d 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -661,59 +661,7 @@ public static function getReservedColumns() { * TRUE if the field has data for any entity; FALSE otherwise. */ public function hasData() { - return $this->entityCount(TRUE); - } - - /** - * Determines the number of entities that have field data. - * - * @param bool $as_bool - * (Optional) Optimises query for hasData(). Defaults to FALSE. - * - * @return bool|int - * The number of entities that have field data. If $as_bool parameter is - * TRUE then the value will either be TRUE or FALSE. - */ - public function entityCount($as_bool = FALSE) { - $count = 0; - $factory = \Drupal::service('entity.query'); - $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type); - // Entity Query throws an exception if there is no base table. - if ($entity_type->getBaseTable()) { - if ($this->deleted) { - $query = $factory->get($this->entity_type) - ->condition('id:' . $this->uuid() . '.deleted', 1); - } - elseif ($this->getBundles()) { - $storage_details = $this->getSchema(); - $columns = array_keys($storage_details['columns']); - $query = $factory->get($this->entity_type); - $group = $query->orConditionGroup(); - foreach ($columns as $column) { - $group->exists($this->name . '.' . $column); - } - $query = $query->condition($group); - } - - if (isset($query)) { - $query - ->count() - ->accessCheck(FALSE); - // If we are performing the query just to check if the field has data - // limit the number of rows returned by the subquery. - if ($as_bool) { - $query->range(0, 1); - } - $count = $query->execute(); - } - } - - if ($as_bool) { - return (bool) $count; - } - else { - return (int) $count; - } + return \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); } /** @@ -790,6 +738,14 @@ public function getMainPropertyName() { } /** + * {@inheritdoc} + */ + public function getUniqueStorageIdentifier() { + return $this->uuid(); + } + + + /** * Helper to retrieve the field item class. */ protected function getFieldItemClass() { diff --git a/core/modules/field/src/Entity/FieldInstanceConfig.php b/core/modules/field/src/Entity/FieldInstanceConfig.php index f655a18..2b4ff56 100644 --- a/core/modules/field/src/Entity/FieldInstanceConfig.php +++ b/core/modules/field/src/Entity/FieldInstanceConfig.php @@ -821,4 +821,11 @@ public static function loadByName($entity_type_id, $bundle, $field_name) { return \Drupal::entityManager()->getStorage('field_instance_config')->load($entity_type_id . '.' . $bundle . '.' . $field_name); } + /** + * {@inheritdoc} + */ + public function getUniqueStorageIdentifier() { + return $this->field->uuid(); + } + } diff --git a/core/modules/field/src/Tests/BulkDeleteTest.php b/core/modules/field/src/Tests/BulkDeleteTest.php index 542139e..055aa3a 100644 --- a/core/modules/field/src/Tests/BulkDeleteTest.php +++ b/core/modules/field/src/Tests/BulkDeleteTest.php @@ -308,7 +308,7 @@ function testPurgeField() { $this->checkHooksInvocations($hooks, $actual_hooks); // Purge again to purge the instance. - field_purge_batch(0); + field_purge_batch(1); // The field still exists, not deleted. $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid, 'include_deleted' => TRUE)); @@ -340,7 +340,7 @@ function testPurgeField() { $this->assertTrue(isset($fields[$field->uuid]) && $fields[$field->uuid]->deleted, 'The field exists and is deleted'); // Purge again to purge the instance and the field. - field_purge_batch(0); + field_purge_batch(1); // The field is gone. $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid, 'include_deleted' => TRUE)); diff --git a/core/modules/field/src/Tests/FieldAttachOtherTest.php b/core/modules/field/src/Tests/FieldAttachOtherTest.php index a748939..31ba143 100644 --- a/core/modules/field/src/Tests/FieldAttachOtherTest.php +++ b/core/modules/field/src/Tests/FieldAttachOtherTest.php @@ -38,7 +38,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision')); + $this->installEntitySchema('entity_test_rev'); $this->createFieldWithInstance(); } diff --git a/core/modules/field/src/Tests/FieldAttachStorageTest.php b/core/modules/field/src/Tests/FieldAttachStorageTest.php index 0bf7732..22ba695 100644 --- a/core/modules/field/src/Tests/FieldAttachStorageTest.php +++ b/core/modules/field/src/Tests/FieldAttachStorageTest.php @@ -37,7 +37,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision')); + $this->installEntitySchema('entity_test_rev'); } /** diff --git a/core/modules/field/src/Tests/FieldDataCountTest.php b/core/modules/field/src/Tests/FieldDataCountTest.php new file mode 100644 index 0000000..5dc854a --- /dev/null +++ b/core/modules/field/src/Tests/FieldDataCountTest.php @@ -0,0 +1,111 @@ + 'Field config hasData() tests.', + 'description' => 'Tests counting field data records and the hasData() method on FieldConfig entity.', + 'group' => 'Field API', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->storage = \Drupal::entityManager()->getStorage('entity_test'); + } + + /** + * 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\FieldConfig $field */ + $field = entity_create('field_config', array( + 'name' => 'field_int', + 'entity_type' => 'entity_test', + 'type' => 'integer', + 'cardinality' => 2, + )); + $field->save(); + entity_create('field_instance_config', array( + 'entity_type' => 'entity_test', + 'field_name' => 'field_int', + 'bundle' => 'entity_test', + ))->save(); + + $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.'); + $this->assertIdentical($this->storage->countFieldData($field), 0, 'There are 0 entities with field data.'); + + // Create 1 entity without the field. + $entity = entity_create('entity_test'); + $entity->name->value = $this->randomName(); + $entity->save(); + + $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.'); + $this->assertIdentical($this->storage->countFieldData($field), 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'); + $value = mt_rand(1,99); + $value2 = mt_rand(1,99); + $entity->field_int[0]->value = $value; + $entity->field_int[1]->value = $value2; + $entity->name->value = $this->randomName(); + $entity->save(); + } + + $storage = \Drupal::entityManager()->getStorage('entity_test'); + if ($storage instanceof ContentEntityDatabaseStorage) { + // Count the actual number of rows in the field table. + $field_table_name = $storage->_fieldTableName($field); + $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->hasdata(), TRUE, 'There are entities with field data.'); + $this->assertEqual($this->storage->countFieldData($field), 24, 'There are 24 rows of field data.'); + + // Ensure the methods work on deleted fields. + $field->delete(); + $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.'); + $this->assertEqual($this->storage->countFieldData($field), 24, 'There are 24 rows of deleted field data.'); + + field_purge_batch(6); + $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.'); + $this->assertEqual($this->storage->countFieldData($field), 18, 'There are 18 rows of deleted field data.'); + } + +} diff --git a/core/modules/field/src/Tests/FieldEntityCountTest.php b/core/modules/field/src/Tests/FieldEntityCountTest.php deleted file mode 100644 index f08cee4..0000000 --- a/core/modules/field/src/Tests/FieldEntityCountTest.php +++ /dev/null @@ -1,95 +0,0 @@ - 'Field config entityCount() and hasData() tests.', - 'description' => 'Tests entityCount() and hasData() methods on FieldConfig entity.', - 'group' => 'Field API', - ); - } - - /** - * 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\FieldConfig $field */ - $field = entity_create('field_config', array( - 'name' => 'field_int', - 'entity_type' => 'entity_test', - 'type' => 'integer', - 'cardinality' => 2, - )); - $field->save(); - entity_create('field_instance_config', array( - 'entity_type' => 'entity_test', - 'field_name' => 'field_int', - 'bundle' => 'entity_test', - ))->save(); - - $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.'); - $this->assertIdentical($field->entityCount(), 0, 'There are 0 entities with field data.'); - - // Create 1 entity without the field. - $entity = entity_create('entity_test'); - $entity->name->value = $this->randomName(); - $entity->save(); - - $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.'); - $this->assertIdentical($field->entityCount(), 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'); - $value = mt_rand(1,99); - $value2 = mt_rand(1,99); - $entity->field_int[0]->value = $value; - $entity->field_int[1]->value = $value2; - $entity->name->value = $this->randomName(); - $entity->save(); - } - - $storage = \Drupal::entityManager()->getStorage('entity_test'); - if ($storage instanceof ContentEntityDatabaseStorage) { - // Count the actual number of rows in the field table. - $field_table_name = $storage->_fieldTableName($field); - $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->hasdata(), TRUE, 'There are entities with field data.'); - $this->assertEqual($field->entityCount(), 12, 'There are 12 entities with field data.'); - - // Ensure the methods work on deleted fields. - $field->delete(); - $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.'); - $this->assertEqual($field->entityCount(), 12, 'There are 12 entities with deleted field data.'); - - field_purge_batch(6); - $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.'); - $this->assertEqual($field->entityCount(), 6, 'There are 6 entities with deleted field data.'); - } - -} diff --git a/core/modules/field/src/Tests/FieldUnitTestBase.php b/core/modules/field/src/Tests/FieldUnitTestBase.php index 5717a23..457a09f 100644 --- a/core/modules/field/src/Tests/FieldUnitTestBase.php +++ b/core/modules/field/src/Tests/FieldUnitTestBase.php @@ -36,9 +36,9 @@ */ function setUp() { parent::setUp(); - $this->installSchema('entity_test', 'entity_test'); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); $this->installSchema('system', array('sequences')); - $this->installSchema('user', array('users', 'users_roles')); // Set default storage backend and configure the theme system. $this->installConfig(array('field', 'system')); diff --git a/core/modules/field/src/Tests/TranslationWebTest.php b/core/modules/field/src/Tests/TranslationWebTest.php index ba00c74..fb52e72 100644 --- a/core/modules/field/src/Tests/TranslationWebTest.php +++ b/core/modules/field/src/Tests/TranslationWebTest.php @@ -33,7 +33,7 @@ class TranslationWebTest extends FieldTestBase { * * @var string */ - protected $entity_type = 'entity_test_rev'; + protected $entity_type = 'entity_test_mulrev'; /** * The field to use in this test. @@ -78,7 +78,7 @@ function setUp() { 'bundle' => $this->entity_type, ); entity_create('field_instance_config', $instance)->save(); - $this->instance = entity_load('field_instance_config', 'entity_test.' . $instance['bundle'] . '.' . $this->field_name); + $this->instance = entity_load('field_instance_config', $this->entity_type . '.' . $instance['bundle'] . '.' . $this->field_name); entity_get_form_display($this->entity_type, $this->entity_type, 'default') ->setComponent($this->field_name) diff --git a/core/modules/file/file.install b/core/modules/file/file.install index 6bbed79..8d13c03 100644 --- a/core/modules/file/file.install +++ b/core/modules/file/file.install @@ -9,105 +9,6 @@ * Implements hook_schema(). */ function file_schema() { - $schema['file_managed'] = array( - 'description' => 'Stores information for uploaded files.', - 'fields' => array( - 'fid' => array( - 'description' => 'File ID.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'uid' => array( - 'description' => 'The {users}.uid of the user who is associated with the file.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'filename' => array( - 'description' => 'Name of the file with no path components. This may differ from the basename of the URI if the file is renamed to avoid overwriting an existing file.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'uri' => array( - 'description' => 'The URI to access the file (either local or remote).', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'binary' => TRUE, - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this file.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'filemime' => array( - 'description' => "The file's MIME type.", - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'filesize' => array( - 'description' => 'The size of the file in bytes.', - 'type' => 'int', - 'size' => 'big', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'status' => array( - 'description' => 'A field indicating the status of the file. Two status are defined in core: temporary (0) and permanent (1). Temporary files older than system.file.temporary_maximum_age will be removed during a cron run.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'tiny', - ), - 'created' => array( - 'description' => 'UNIX timestamp for when the file added.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'changed' => array( - 'description' => 'UNIX timestamp for when the file was last changed.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'indexes' => array( - 'uid' => array('uid'), - 'status' => array('status'), - 'changed' => array('changed'), - ), - 'unique keys' => array( - 'uuid' => array('uuid'), - 'uri' => array('uri'), - ), - 'primary key' => array('fid'), - 'foreign keys' => array( - 'file_owner' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ), - ); - $schema['file_usage'] = array( 'description' => 'Track where a file is used.', 'fields' => array( diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index 39f27d6..8564127 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -258,11 +258,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['filesize'] = FieldDefinition::create('integer') ->setLabel(t('File size')) ->setDescription(t('The size of the file in bytes.')) - ->setSetting('unsigned', TRUE); + ->setSetting('unsigned', TRUE) + ->setSetting('size', 'big'); - $fields['status'] = FieldDefinition::create('integer') + $fields['status'] = FieldDefinition::create('boolean') ->setLabel(t('Status')) - ->setDescription(t('The status of the file, temporary (0) and permanent (1).')); + ->setDescription(t('The status of the file, temporary (FALSE) and permanent (TRUE).')); $fields['created'] = FieldDefinition::create('created') ->setLabel(t('Created')) diff --git a/core/modules/file/src/FileStorage.php b/core/modules/file/src/FileStorage.php index c6f2ccc..9637234 100644 --- a/core/modules/file/src/FileStorage.php +++ b/core/modules/file/src/FileStorage.php @@ -38,4 +38,33 @@ public function retrieveTemporaryFiles() { ':changed' => REQUEST_TIME - DRUPAL_MAXIMUM_TEMP_FILE_AGE )); } + + /** + * {@inheritdoc} + */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['file_managed']['fields']['status']['not null'] = TRUE; + $schema['file_managed']['fields']['changed']['not null'] = TRUE; + $schema['file_managed']['fields']['uri']['not null'] = TRUE; + + // @todo There should be a 'binary' field type or setting. + $schema['file_managed']['fields']['uri']['binary'] = TRUE; + $schema['file_managed']['indexes'] += array( + 'file__status' => array('status'), + 'file__changed' => array('changed'), + ); + $schema['file_managed']['unique keys'] += array( + // FIXME We have an index size of 255, but the max URI length is 2048 so + // this might now always work. Should we replace this with a regular + // index? + 'file__uri' => array(array('uri', 255)), + ); + + return $schema; + } + } diff --git a/core/modules/file/src/Tests/FileItemTest.php b/core/modules/file/src/Tests/FileItemTest.php index 0f40a14..57b908a 100644 --- a/core/modules/file/src/Tests/FileItemTest.php +++ b/core/modules/file/src/Tests/FileItemTest.php @@ -42,8 +42,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('file', 'file_managed'); - $this->installSchema('file', 'file_usage'); + $this->installEntitySchema('file'); + $this->installSchema('file', array('file_usage')); entity_create('field_config', array( 'name' => 'file_test', diff --git a/core/modules/file/src/Tests/FileManagedUnitTestBase.php b/core/modules/file/src/Tests/FileManagedUnitTestBase.php index 8c83ac7..503490f 100644 --- a/core/modules/file/src/Tests/FileManagedUnitTestBase.php +++ b/core/modules/file/src/Tests/FileManagedUnitTestBase.php @@ -29,8 +29,9 @@ function setUp() { file_test_reset(); $this->installConfig(array('system')); - $this->installSchema('file', array('file_managed', 'file_usage')); - $this->installSchema('user', array('users', 'users_roles')); + $this->installEntitySchema('file'); + $this->installEntitySchema('user'); + $this->installSchema('file', array('file_usage')); // Make sure that a user with uid 1 exists, self::createFile() relies on // it. diff --git a/core/modules/file/src/Tests/SpaceUsedTest.php b/core/modules/file/src/Tests/SpaceUsedTest.php index a0c8e33..bfedef6 100644 --- a/core/modules/file/src/Tests/SpaceUsedTest.php +++ b/core/modules/file/src/Tests/SpaceUsedTest.php @@ -23,20 +23,40 @@ function setUp() { parent::setUp(); // Create records for a couple of users with different sizes. - $file = array('uid' => 2, 'uri' => 'public://example1.txt', 'filesize' => 50, 'status' => FILE_STATUS_PERMANENT); - db_insert('file_managed')->fields($file)->execute(); - $file = array('uid' => 2, 'uri' => 'public://example2.txt', 'filesize' => 20, 'status' => FILE_STATUS_PERMANENT); - db_insert('file_managed')->fields($file)->execute(); - $file = array('uid' => 3, 'uri' => 'public://example3.txt', 'filesize' => 100, 'status' => FILE_STATUS_PERMANENT); - db_insert('file_managed')->fields($file)->execute(); - $file = array('uid' => 3, 'uri' => 'public://example4.txt', 'filesize' => 200, 'status' => FILE_STATUS_PERMANENT); - db_insert('file_managed')->fields($file)->execute(); + $this->createFileWithSize('public://example1.txt', 50, 2); + $this->createFileWithSize('public://example2.txt', 20, 2); + $this->createFileWithSize('public://example3.txt', 100, 3); + $this->createFileWithSize('public://example4.txt', 200, 3); // Now create some non-permanent files. - $file = array('uid' => 2, 'uri' => 'public://example5.txt', 'filesize' => 1, 'status' => 0); - db_insert('file_managed')->fields($file)->execute(); - $file = array('uid' => 3, 'uri' => 'public://example6.txt', 'filesize' => 3, 'status' => 0); - db_insert('file_managed')->fields($file)->execute(); + $this->createFileWithSize('public://example5.txt', 1, 2, 0); + $this->createFileWithSize('public://example6.txt', 3, 3, 0); + } + + /** + * Creates a file with a given size. + * + * @param string $uri + * URI of the file to create. + * @param int $size + * Size of the file. + * @param int $uid + * File owner ID. + * @param int $status + * Whether the file should be permanent or temporary. + * + * @return \Drupal\Core\Entity\EntityInterface + * The file entity. + */ + protected function createFileWithSize($uri, $size, $uid, $status = FILE_STATUS_PERMANENT) { + file_put_contents($uri, $this->randomName($size)); + $file = entity_create('file', array( + 'uri' => $uri, + 'uid' => $uid, + 'status' => $status, + )); + $file->save(); + return $file; } /** diff --git a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php index 9d8f3ca..7423b70 100644 --- a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php +++ b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php @@ -30,7 +30,7 @@ function setUp() { // filter_permission() calls into url() to output a link in the description. $this->installSchema('system', 'url_alias'); - $this->installSchema('user', array('users_roles')); + $this->installEntitySchema('user'); // Install filter_test module, which ships with custom default format. $this->installConfig(array('user', 'filter_test')); diff --git a/core/modules/forum/src/Tests/Views/ForumIntegrationTest.php b/core/modules/forum/src/Tests/Views/ForumIntegrationTest.php index 65d9f1b..6e40370 100644 --- a/core/modules/forum/src/Tests/Views/ForumIntegrationTest.php +++ b/core/modules/forum/src/Tests/Views/ForumIntegrationTest.php @@ -51,7 +51,7 @@ protected function setUp() { public function testForumIntegration() { // Create a forum. $entity_manager = $this->container->get('entity.manager'); - $term = $entity_manager->getStorage('taxonomy_term')->create(array('vid' => 'forums')); + $term = $entity_manager->getStorage('taxonomy_term')->create(array('vid' => 'forums', 'name' => $this->randomName())); $term->save(); $comment_storage = $entity_manager->getStorage('comment'); diff --git a/core/modules/hal/src/Tests/EntityTest.php b/core/modules/hal/src/Tests/EntityTest.php index bfbe6f1..8bc3fed 100644 --- a/core/modules/hal/src/Tests/EntityTest.php +++ b/core/modules/hal/src/Tests/EntityTest.php @@ -38,10 +38,10 @@ function setUp() { \Drupal::service('router.builder')->rebuild(); $this->installSchema('system', array('sequences')); - $this->installSchema('node', array('node', 'node_field_data', 'node_revision', 'node_field_revision')); - $this->installSchema('comment', array('comment', 'comment_entity_statistics')); - $this->installSchema('user', array('users_roles')); - $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy')); + $this->installSchema('comment', array('comment_entity_statistics')); + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('taxonomy_term'); } /** diff --git a/core/modules/hal/src/Tests/NormalizerTestBase.php b/core/modules/hal/src/Tests/NormalizerTestBase.php index 407e4e3..06d41ea 100644 --- a/core/modules/hal/src/Tests/NormalizerTestBase.php +++ b/core/modules/hal/src/Tests/NormalizerTestBase.php @@ -62,8 +62,8 @@ function setUp() { parent::setUp(); $this->installSchema('system', array('url_alias', 'router')); - $this->installSchema('user', array('users')); - $this->installSchema('entity_test', array('entity_test')); + $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test'); $this->installConfig(array('field', 'language')); // Add English as a language. diff --git a/core/modules/image/src/Tests/ImageItemTest.php b/core/modules/image/src/Tests/ImageItemTest.php index 7803a11..e17987b 100644 --- a/core/modules/image/src/Tests/ImageItemTest.php +++ b/core/modules/image/src/Tests/ImageItemTest.php @@ -47,7 +47,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('file', array('file_managed', 'file_usage')); + $this->installEntitySchema('file'); + $this->installSchema('file', array('file_usage')); entity_create('field_config', array( 'name' => 'image_test', diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php index ad749fb..6550a0e 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -210,7 +210,7 @@ public function massageFormValues(array $values, array $form, array &$form_state // If internal links are supported, look up whether the given value is // a path alias and store the system path instead. if ($this->supportsInternalLinks() && !UrlHelper::isExternal($value['url'])) { - $parsed_url['path'] = \Drupal::service('path.alias_manager')->getPathByAlias($parsed_url['path']); + $parsed_url['path'] = \Drupal::service('path.alias_manager.cached')->getPathByAlias($parsed_url['path']); } $url = Url::createFromPath($parsed_url['path']); diff --git a/core/modules/menu_link/src/MenuLinkForm.php b/core/modules/menu_link/src/MenuLinkForm.php index d428734..c365dae 100644 --- a/core/modules/menu_link/src/MenuLinkForm.php +++ b/core/modules/menu_link/src/MenuLinkForm.php @@ -62,7 +62,7 @@ public function __construct(MenuLinkStorageInterface $menu_link_storage, AliasMa public static function create(ContainerInterface $container) { return new static( $container->get('entity.manager')->getStorage('menu_link'), - $container->get('path.alias_manager'), + $container->get('path.alias_manager.cached'), $container->get('url_generator') ); } diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateBookTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateBookTest.php index d29ad95..d7fc9a3 100644 --- a/core/modules/migrate_drupal/src/Tests/d6/MigrateBookTest.php +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateBookTest.php @@ -38,6 +38,7 @@ protected function setUp() { $entity = entity_create('node', array( 'type' => 'story', 'nid' => $i, + 'status' => TRUE, )); $entity->enforceIsNew(); $entity->save(); diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php index 98c53e9..dfaa2a3 100644 --- a/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php @@ -107,6 +107,7 @@ public function testFieldInstanceSettings() { 'prefix' => 'pref', 'suffix' => 'suf', 'unsigned' => '', + 'size' => 'normal', ); $this->assertEqual($field->getSettings(), $expected); diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateTaxonomyTermTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateTaxonomyTermTest.php index a4f5b13..a4027e6 100644 --- a/core/modules/migrate_drupal/src/Tests/d6/MigrateTaxonomyTermTest.php +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateTaxonomyTermTest.php @@ -60,11 +60,13 @@ public function testTaxonomyTerms() { 'source_vid' => 1, 'vid' => 'vocabulary_1_i_0_', 'weight' => 0, + 'parent' => array(0), ), '2' => array( 'source_vid' => 2, 'vid' => 'vocabulary_2_i_1_', 'weight' => 3, + 'parent' => array(0), ), '3' => array( 'source_vid' => 2, @@ -76,6 +78,7 @@ public function testTaxonomyTerms() { 'source_vid' => 3, 'vid' => 'vocabulary_3_i_2_', 'weight' => 6, + 'parent' => array(0), ), '5' => array( 'source_vid' => 3, @@ -98,8 +101,8 @@ public function testTaxonomyTerms() { $this->assertIdentical($term->description->value, "description of term {$tid} of vocabulary {$values['source_vid']}"); $this->assertEqual($term->vid->value, $values['vid']); $this->assertEqual($term->weight->value, $values['weight']); - if (empty($values['parent'])) { - $this->assertNull($term->parent->value); + if ($values['parent'] === array(0)) { + $this->assertEqual($term->parent->value, 0); } else { $parents = array(); diff --git a/core/modules/node/node.install b/core/modules/node/node.install index b075d93..d0d6d1c 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -39,311 +39,6 @@ function node_requirements($phase) { * Implements hook_schema(). */ function node_schema() { - $schema['node'] = array( - 'description' => 'The base table for nodes.', - 'fields' => array( - 'nid' => array( - 'description' => 'The primary identifier for a node.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - // Defaults to NULL in order to avoid a brief period of potential - // deadlocks on the index. - 'vid' => array( - 'description' => 'The current {node_field_revision}.vid version identifier.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - ), - 'type' => array( - 'description' => 'The type of this node.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'indexes' => array( - 'node_type' => array(array('type', 4)), - ), - 'unique keys' => array( - 'vid' => array('vid'), - 'uuid' => array('uuid'), - ), - 'foreign keys' => array( - 'node_revision' => array( - 'table' => 'node_revision', - 'columns' => array('vid' => 'vid'), - ), - ), - 'primary key' => array('nid'), - ); - - $schema['node_revision'] = array( - 'description' => 'Stores information about each saved version of a {node}.', - 'fields' => array( - 'nid' => array( - 'description' => 'The {node} this version belongs to.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'vid' => array( - 'description' => 'The primary identifier for this version.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'revision_uid' => array( - 'description' => 'The {users}.uid that created this version.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'log' => array( - 'description' => 'The log entry explaining the changes in this version.', - 'type' => 'text', - 'not null' => FALSE, - 'size' => 'big', - ), - 'revision_timestamp' => array( - 'description' => 'The Unix timestamp when the version was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this version.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'indexes' => array( - 'nid' => array('nid'), - 'revision_uid' => array('revision_uid'), - 'node_langcode' => array('langcode'), - ), - 'foreign keys' => array( - 'versioned_node' => array( - 'table' => 'node', - 'columns' => array('nid' => 'nid'), - ), - 'version_author' => array( - 'table' => 'users', - 'columns' => array('revision_uid' => 'uid'), - ), - ), - 'primary key' => array('vid'), - ); - - // Node field storage. - $schema['node_field_data'] = array( - 'description' => 'Data table for node base fields.', - 'fields' => array( - 'nid' => array( - 'description' => 'The primary identifier for a node.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'vid' => array( - 'description' => 'The current {node_field_revision}.vid version identifier.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'type' => array( - 'description' => 'The {node_type}.type of this node.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of these node property values.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'default_langcode' => array( - 'description' => 'Boolean indicating whether the property values are in the {language}.langcode of this node.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'title' => array( - 'description' => 'The title of this node, always treated as non-markup plain text.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'uid' => array( - 'description' => 'The {users}.uid that owns this node; initially, this is the user that created it.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'status' => array( - 'description' => 'Boolean indicating whether the node translation is published (visible to non-administrators).', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'created' => array( - 'description' => 'The Unix timestamp when the node translation was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'changed' => array( - 'description' => 'The Unix timestamp when the node translation was most recently saved.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'promote' => array( - 'description' => 'Boolean indicating whether the node translation should be displayed on the front page.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'sticky' => array( - 'description' => 'Boolean indicating whether the node translation should be displayed at the top of lists in which it appears.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'indexes' => array( - 'node_changed' => array('changed'), - 'node_created' => array('created'), - 'node_default_langcode' => array('default_langcode'), - 'node_langcode' => array('langcode'), - 'node_frontpage' => array('promote', 'status', 'sticky', 'created'), - 'node_status_type' => array('status', 'type', 'nid'), - 'node_title_type' => array('title', array('type', 4)), - 'node_type' => array(array('type', 4)), - 'vid' => array('vid'), - 'uid' => array('uid'), - ), - 'foreign keys' => array( - 'node_base' => array( - 'table' => 'node', - 'columns' => array('nid' => 'nid'), - ), - 'node_author' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ), - 'primary key' => array('nid', 'langcode'), - ); - - $schema['node_field_revision'] = array( - 'description' => 'Revision table for node base fields.', - 'fields' => array( - 'nid' => array( - 'description' => 'The {node} this version belongs to.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'vid' => array( - 'description' => 'The primary identifier for this version.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this version.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'default_langcode' => array( - 'description' => 'Boolean indicating whether the property values of this version are in the {language}.langcode of this node.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'title' => array( - 'description' => 'The title of this version, always treated as non-markup plain text.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'uid' => array( - 'description' => 'The {users}.uid that created this node.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'status' => array( - 'description' => 'Boolean indicating whether the node (at the time of this revision) is published (visible to non-administrators).', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'created' => array( - 'description' => 'The Unix timestamp when the node was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'changed' => array( - 'description' => 'The Unix timestamp when the version was most recently saved.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'promote' => array( - 'description' => 'Boolean indicating whether the node (at the time of this revision) should be displayed on the front page.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'sticky' => array( - 'description' => 'Boolean indicating whether the node (at the time of this revision) should be displayed at the top of lists in which it appears.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'indexes' => array( - 'uid' => array('uid'), - 'node_default_langcode' => array('default_langcode'), - 'node_langcode' => array('langcode'), - ), - 'foreign keys' => array( - 'versioned_node' => array( - 'table' => 'node', - 'columns' => array('nid' => 'nid'), - ), - 'node_author' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ), - 'primary key' => array('vid', 'langcode'), - ); - $schema['node_access'] = array( 'description' => 'Identifies which realm/grant pairs a user must possess in order to view, update, or delete specific nodes.', 'fields' => array( diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 2253f71..38e047b 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -807,7 +807,10 @@ function node_user_cancel($edit, $account, $method) { // Anonymize all of the nodes for this old account. module_load_include('inc', 'node', 'node.admin'); $vids = \Drupal::entityManager()->getStorage('node')->userRevisionIds($account); - node_mass_update($vids, array('uid' => 0), NULL, TRUE, TRUE); + node_mass_update($vids, array( + 'uid' => 0, + 'revision_uid' => 0, + ), NULL, TRUE, TRUE); break; } } diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 1d21504..7fb33e6 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -65,6 +65,23 @@ class Node extends ContentEntityBase implements NodeInterface { /** * {@inheritdoc} */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // If no owner has been set explicitly, make the current user the owner. + if (!$this->getOwner()) { + $this->setOwnerId(\Drupal::currentUser()->id()); + } + // If no revision author has been set explicitly, make the node owner the + // revision author. + if (!$this->getRevisionAuthor()) { + $this->setRevisionAuthorId($this->getOwnerId()); + } + } + + /** + * {@inheritdoc} + */ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { parent::preSaveRevision($storage, $record); @@ -340,6 +357,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Type')) ->setDescription(t('The node type.')) ->setSetting('target_type', 'node_type') + ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH) ->setReadOnly(TRUE); $fields['langcode'] = FieldDefinition::create('language') @@ -353,10 +371,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setRequired(TRUE) ->setTranslatable(TRUE) ->setRevisionable(TRUE) - ->setSettings(array( - 'default_value' => '', - 'max_length' => 255, - )) + ->setSetting('default_value', '') + ->setSetting('max_length', 255) ->setDisplayOptions('view', array( 'label' => 'hidden', 'type' => 'string', @@ -372,10 +388,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Author')) ->setDescription(t('The user that is the node author.')) ->setRevisionable(TRUE) - ->setSettings(array( - 'target_type' => 'user', - 'default_value' => 0, - )) + ->setSetting('target_type', 'user') ->setTranslatable(TRUE); $fields['status'] = FieldDefinition::create('boolean') @@ -408,7 +421,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setRevisionable(TRUE) ->setTranslatable(TRUE); - $fields['revision_timestamp'] = FieldDefinition::create('timestamp') + $fields['revision_timestamp'] = FieldDefinition::create('created') ->setLabel(t('Revision timestamp')) ->setDescription(t('The time that the current revision was created.')) ->setQueryable(FALSE) @@ -417,11 +430,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['revision_uid'] = FieldDefinition::create('entity_reference') ->setLabel(t('Revision user ID')) ->setDescription(t('The user ID of the author of the current revision.')) - ->setSettings(array('target_type' => 'user')) + ->setSetting('target_type', 'user') ->setQueryable(FALSE) ->setRevisionable(TRUE); - $fields['log'] = FieldDefinition::create('string') + $fields['log'] = FieldDefinition::create('string_long') ->setLabel(t('Log')) ->setDescription(t('The log entry explaining the changes in this revision.')) ->setRevisionable(TRUE) @@ -436,10 +449,30 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { $node_type = node_type_load($bundle); $fields = array(); + + // When deleting a node type the corresponding node displays are deleted as + // well. In order to be deleted, they need to be loaded first. Entity + // displays, however, fetch the field definitions of the respective entity + // type to fill in their defaults. Therefore this function ends up being + // called with a non-existing bundle. + // @todo Fix this in https://drupal.org/node/2248795 + if (!$node_type) { + return $fields; + } + if (isset($node_type->title_label)) { $fields['title'] = clone $base_field_definitions['title']; $fields['title']->setLabel($node_type->title_label); } + + $options = $node_type->getModuleSettings('node')['options']; + $fields['status'] = clone $base_field_definitions['status']; + $fields['status']->setSetting('default_value', !empty($options['status']) ? NODE_PUBLISHED : NODE_NOT_PUBLISHED); + $fields['promote'] = clone $base_field_definitions['promote']; + $fields['promote']->setSetting('default_value', !empty($options['promote']) ? NODE_PROMOTED : NODE_NOT_PROMOTED); + $fields['sticky'] = clone $base_field_definitions['sticky']; + $fields['sticky']->setSetting('default_value', !empty($options['sticky']) ? NODE_STICKY : NODE_NOT_STICKY); + return $fields; } diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php index 37bb7da..0c5bbc6 100644 --- a/core/modules/node/src/NodeForm.php +++ b/core/modules/node/src/NodeForm.php @@ -36,16 +36,7 @@ protected function prepareEntity() { $type = entity_load('node_type', $node->bundle()); $this->settings = $type->getModuleSettings('node'); - // If this is a new node, fill in the default values. - if ($node->isNew()) { - foreach (array('status', 'promote', 'sticky') as $key) { - // Multistep node forms might have filled in something already. - if ($node->$key->isEmpty()) { - $node->$key = (int) !empty($this->settings['options'][$key]); - } - } - } - else { + if (!$node->isNew()) { $node->date = format_date($node->getCreatedTime(), 'custom', 'Y-m-d H:i:s O'); // Remove the log message from the original node entity. $node->log = NULL; diff --git a/core/modules/node/src/NodeStorage.php b/core/modules/node/src/NodeStorage.php index 8bb17fd..8f87df5 100644 --- a/core/modules/node/src/NodeStorage.php +++ b/core/modules/node/src/NodeStorage.php @@ -2,12 +2,11 @@ /** * @file - * Definition of Drupal\node\NodeStorageController. + * Contains \Drupal\node\NodeStorage. */ namespace Drupal\node; -use Drupal\Core\Database\Database; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Language\Language; @@ -59,4 +58,51 @@ public function clearRevisionsLanguage($language) { ->condition('langcode', $language->id) ->execute(); } + + /** + * {@inheritdoc} + */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['node_field_data']['fields']['changed']['not null'] = TRUE; + $schema['node_field_data']['fields']['created']['not null'] = TRUE; + $schema['node_field_data']['fields']['default_langcode']['not null'] = TRUE; + $schema['node_field_data']['fields']['promote']['not null'] = TRUE; + $schema['node_field_data']['fields']['status']['not null'] = TRUE; + $schema['node_field_data']['fields']['sticky']['not null'] = TRUE; + $schema['node_field_data']['fields']['title']['not null'] = TRUE; + $schema['node_field_revision']['fields']['default_langcode']['not null'] = TRUE; + + // @todo Revisit index definitions in https://drupal.org/node/2015277. + $schema['node_revision']['indexes'] += array( + 'node__langcode' => array('langcode'), + ); + $schema['node_revision']['foreign keys'] += array( + 'node__revision_author' => array( + 'table' => 'users', + 'columns' => array('revision_uid' => 'uid'), + ), + ); + + $schema['node_field_data']['indexes'] += array( + 'node__changed' => array('changed'), + 'node__created' => array('created'), + 'node__default_langcode' => array('default_langcode'), + 'node__langcode' => array('langcode'), + 'node__frontpage' => array('promote', 'status', 'sticky', 'created'), + 'node__status_type' => array('status', 'type', 'nid'), + 'node__title_type' => array('title', array('type', 4)), + ); + + $schema['node_field_revision']['indexes'] += array( + 'node__default_langcode' => array('default_langcode'), + 'node__langcode' => array('langcode'), + ); + + return $schema; + } + } diff --git a/core/modules/node/src/Tests/Condition/NodeConditionTest.php b/core/modules/node/src/Tests/Condition/NodeConditionTest.php index 6ba87e6..e429de5 100644 --- a/core/modules/node/src/Tests/Condition/NodeConditionTest.php +++ b/core/modules/node/src/Tests/Condition/NodeConditionTest.php @@ -26,7 +26,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision')); + $this->installEntitySchema('node'); // Create the node bundles required for testing. $type = entity_create('node_type', array('type' => 'page', 'name' => 'page')); diff --git a/core/modules/node/src/Tests/Config/NodeImportCreateTest.php b/core/modules/node/src/Tests/Config/NodeImportCreateTest.php index 2f6a8db..fadc706 100644 --- a/core/modules/node/src/Tests/Config/NodeImportCreateTest.php +++ b/core/modules/node/src/Tests/Config/NodeImportCreateTest.php @@ -27,7 +27,7 @@ class NodeImportCreateTest extends DrupalUnitTestBase { */ public function setUp() { parent::setUp(); - $this->installSchema('user', array('users')); + $this->installEntitySchema('user'); // Set default storage backend. $this->installConfig(array('field')); diff --git a/core/modules/node/src/Tests/NodeFormButtonsTest.php b/core/modules/node/src/Tests/NodeFormButtonsTest.php index 1aa7dcd..9a6831d 100644 --- a/core/modules/node/src/Tests/NodeFormButtonsTest.php +++ b/core/modules/node/src/Tests/NodeFormButtonsTest.php @@ -108,7 +108,10 @@ function testNodeFormButtons() { // Set article content type default to unpublished. This will change the // the initial order of buttons and/or status of the node when creating // a node. - \Drupal::config('node.type.article')->set('settings.node.options.status', FALSE)->save(); + /** @var \Drupal\node\NodeTypeInterface $node_type */ + $node_type = $this->container->get('entity.manager')->getStorage('node_type')->load('article'); + $node_type->settings['node']['options']['status'] = FALSE; + $node_type->save(); // Verify the buttons on a node add form for an administrator. $this->drupalLogin($this->admin_user); diff --git a/core/modules/node/src/Tests/NodeLastChangedTest.php b/core/modules/node/src/Tests/NodeLastChangedTest.php index bf23498..aa1dbfa 100644 --- a/core/modules/node/src/Tests/NodeLastChangedTest.php +++ b/core/modules/node/src/Tests/NodeLastChangedTest.php @@ -31,11 +31,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('node', 'node'); - $this->installSchema('node', 'node_revision'); - $this->installSchema('node', 'node_field_data'); - $this->installSchema('node', 'node_field_revision'); - $this->installSchema('user', array('users')); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); } /** diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php index 5e9e37a..7f12113 100644 --- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php +++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php @@ -38,7 +38,7 @@ public static function getInfo() { */ public function setUp() { parent::setUp(); - $this->installSchema('node', array('node', 'node_field_revision', 'node_field_data', 'node_revision')); + $this->installEntitySchema('node'); $this->installConfig(array('filter')); $node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article')); diff --git a/core/modules/node/src/Tests/NodeValidationTest.php b/core/modules/node/src/Tests/NodeValidationTest.php index e71d75b..94c7ca3 100644 --- a/core/modules/node/src/Tests/NodeValidationTest.php +++ b/core/modules/node/src/Tests/NodeValidationTest.php @@ -34,7 +34,7 @@ public static function getInfo() { */ public function setUp() { parent::setUp(); - $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision')); + $this->installEntitySchema('node'); // Create a node type for testing. $type = entity_create('node_type', array('type' => 'page', 'name' => 'page')); diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php index db01fa9..f99ff78 100644 --- a/core/modules/path/src/Tests/PathAliasTest.php +++ b/core/modules/path/src/Tests/PathAliasTest.php @@ -58,12 +58,12 @@ function testPathCache() { \Drupal::cache('data')->deleteAll(); // Make sure the path is not converted to the alias. $this->drupalGet($edit['source'], array('alias' => TRUE)); - $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.'); + $this->assertTrue(\Drupal::cache('data')->get($edit['source']), 'Cache entry was created.'); // Visit the alias for the node and confirm a cache entry is created. \Drupal::cache('data')->deleteAll(); $this->drupalGet($edit['alias']); - $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.'); + $this->assertTrue(\Drupal::cache('data')->get($edit['source']), 'Cache entry was created.'); } /** diff --git a/core/modules/quickedit/src/Tests/QuickEditTestBase.php b/core/modules/quickedit/src/Tests/QuickEditTestBase.php index c72899b..9fb217c 100644 --- a/core/modules/quickedit/src/Tests/QuickEditTestBase.php +++ b/core/modules/quickedit/src/Tests/QuickEditTestBase.php @@ -27,7 +27,7 @@ protected function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test', 'entity_test_rev')); + $this->installEntitySchema('entity_test'); $this->installConfig(array('field', 'filter')); } diff --git a/core/modules/rdf/src/Tests/CommentAttributesTest.php b/core/modules/rdf/src/Tests/CommentAttributesTest.php index f152e0b..d3cacfd 100644 --- a/core/modules/rdf/src/Tests/CommentAttributesTest.php +++ b/core/modules/rdf/src/Tests/CommentAttributesTest.php @@ -163,7 +163,7 @@ public function testCommentRdfaMarkup() { $anonymous_user['name'] = $this->randomName(); $anonymous_user['mail'] = 'tester@simpletest.org'; $anonymous_user['homepage'] = 'http://example.org/'; - $comment2 = $this->saveComment($this->node->id(), NULL, $anonymous_user); + $comment2 = $this->saveComment($this->node->id(), 0, $anonymous_user); // Tests comment #2 as anonymous user. $parser = new \EasyRdf_Parser_Rdfa(); diff --git a/core/modules/rdf/src/Tests/Field/TaxonomyTermReferenceRdfaTest.php b/core/modules/rdf/src/Tests/Field/TaxonomyTermReferenceRdfaTest.php index 5086bf1..f733014 100644 --- a/core/modules/rdf/src/Tests/Field/TaxonomyTermReferenceRdfaTest.php +++ b/core/modules/rdf/src/Tests/Field/TaxonomyTermReferenceRdfaTest.php @@ -50,7 +50,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy')); + $this->installEntitySchema('taxonomy_term'); $vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => $this->randomName(), diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 9b3c792..718f5c8 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -133,6 +133,13 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity // Overwrite the received properties. foreach ($entity as $field_name => $field) { if (isset($entity->{$field_name})) { + // It is not possible to set the language to NULL as it is automatically + // re-initialized. As it must not be empty, skip it if it is. + // @todo: Use the langcode entity key when available. See + // https://drupal.org/node/2143729. + if ($field_name == 'langcode' && $field->isEmpty()) { + continue; + } if ($field->isEmpty() && !$original_entity->get($field_name)->access('delete')) { throw new AccessDeniedHttpException(t('Access denied on deleting field @field.', array('@field' => $field_name))); } diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php index 15ef7f3..7afedfa 100644 --- a/core/modules/rest/src/Tests/UpdateTest.php +++ b/core/modules/rest/src/Tests/UpdateTest.php @@ -58,7 +58,6 @@ public function testPatchUpdate() { $patch_entity = entity_create($entity_type, $patch_values); // We don't want to overwrite the UUID. unset($patch_entity->uuid); - $patch_entity->save(); $serialized = $serializer->serialize($patch_entity, $this->defaultFormat); // Update the entity over the REST API. diff --git a/core/modules/serialization/src/Tests/EntitySerializationTest.php b/core/modules/serialization/src/Tests/EntitySerializationTest.php index 865704d..1ad843a 100644 --- a/core/modules/serialization/src/Tests/EntitySerializationTest.php +++ b/core/modules/serialization/src/Tests/EntitySerializationTest.php @@ -80,18 +80,12 @@ public function testNormalize() { 'id' => array( array('value' => 1), ), - 'revision_id' => array( - array('value' => 1), - ), 'uuid' => array( array('value' => $this->entity->uuid()), ), 'langcode' => array( array('value' => Language::LANGCODE_NOT_SPECIFIED), ), - 'default_langcode' => array( - array('value' => NULL), - ), 'name' => array( array('value' => $this->values['name']), ), @@ -101,6 +95,9 @@ public function testNormalize() { 'user_id' => array( array('target_id' => $this->values['user_id']), ), + 'revision_id' => array( + array('value' => 1), + ), 'field_test_text' => array( array( 'value' => $this->values['field_test_text']['value'], @@ -141,13 +138,12 @@ public function testSerialize() { // order. $expected = array( 'id' => '' . $this->entity->id() . '', - 'revision_id' => '' . $this->entity->getRevisionId() . '', 'uuid' => '' . $this->entity->uuid() . '', 'langcode' => '' . Language::LANGCODE_NOT_SPECIFIED . '', - 'default_langcode' => '', 'name' => '' . $this->values['name'] . '', 'type' => 'entity_test_mulrev', 'user_id' => '' . $this->values['user_id'] . '', + 'revision_id' => '' . $this->entity->getRevisionId() . '', 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '', ); // Sort it in the same order as normalised. diff --git a/core/modules/serialization/src/Tests/NormalizerTestBase.php b/core/modules/serialization/src/Tests/NormalizerTestBase.php index 3b0cfd6..0c1dc03 100644 --- a/core/modules/serialization/src/Tests/NormalizerTestBase.php +++ b/core/modules/serialization/src/Tests/NormalizerTestBase.php @@ -21,8 +21,8 @@ protected function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_revision', 'entity_test_mulrev_property_revision', 'entity_test_mulrev_property_data')); - $this->installSchema('user', array('users', 'users_roles')); + $this->installEntitySchema('entity_test_mulrev'); + $this->installEntitySchema('user'); $this->installSchema('system', array('url_alias')); $this->installConfig(array('field')); diff --git a/core/modules/shortcut/shortcut.install b/core/modules/shortcut/shortcut.install index b7ed49c..be6f8b2 100644 --- a/core/modules/shortcut/shortcut.install +++ b/core/modules/shortcut/shortcut.install @@ -12,96 +12,6 @@ * Implements hook_schema(). */ function shortcut_schema() { - $schema['shortcut'] = array( - 'description' => 'Stores shortcut items.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique shortcut ID.', - ), - 'uuid' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - 'description' => 'Unique Key: Universally unique identifier for this shortcut.', - ), - 'shortcut_set' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The bundle of the shortcut.', - ), - 'langcode' => array( - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The {language}.langcode of the original variant of this shortcut.', - ), - 'weight' => array( - 'description' => 'Weight among shortcuts in the same shortcut set.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'route_name' => array( - 'description' => 'The machine name of a defined Symfony Route this menu item represents.', - 'type' => 'varchar', - 'length' => 255, - ), - 'route_parameters' => array( - 'description' => 'Serialized array of route parameters of this shortcut.', - 'type' => 'blob', - 'size' => 'big', - 'not null' => FALSE, - 'serialize' => TRUE, - ), - ), - 'primary key' => array('id'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), - ); - - $schema['shortcut_field_data'] = array( - 'description' => 'Stores shortcut properties.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The {shortcut}.id of the shortcut.', - ), - 'langcode' => array( - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The {language}.langcode of this variant of this shortcut.', - ), - 'default_langcode' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - 'description' => 'Boolean indicating whether the current variant is in the original entity language.', - ), - 'title' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => FALSE, - 'description' => 'The title of the shortcut.', - ), - ), - 'foreign keys' => array( - 'shortcut' => array( - 'table' => 'shortcut', - 'columns' => array('id' => 'id'), - ), - ), - 'primary key' => array('id', 'langcode'), - ); $schema['shortcut_set_users'] = array( 'description' => 'Maps users to shortcut sets.', diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php index 03be8c8..53106e7 100644 --- a/core/modules/shortcut/src/Entity/Shortcut.php +++ b/core/modules/shortcut/src/Entity/Shortcut.php @@ -167,6 +167,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Shortcut set')) ->setDescription(t('The bundle of the shortcut.')) ->setSetting('target_type', 'shortcut_set') + ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH) ->setRequired(TRUE); $fields['title'] = FieldDefinition::create('string') @@ -174,10 +175,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(t('The name of the shortcut.')) ->setRequired(TRUE) ->setTranslatable(TRUE) - ->setSettings(array( - 'default_value' => '', - 'max_length' => 255, - )) + ->setSetting('default_value', '') + ->setSetting('max_length', 255) ->setDisplayOptions('form', array( 'type' => 'string', 'weight' => -10, @@ -202,10 +201,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Language code')) ->setDescription(t('The language code of the shortcut.')); - $fields['default_langcode'] = FieldDefinition::create('boolean') - ->setLabel(t('Default language')) - ->setDescription(t('Flag to indicate whether this is the default language.')); - $fields['path'] = FieldDefinition::create('string') ->setLabel(t('Path')) ->setDescription(t('The computed shortcut path.')) diff --git a/core/modules/shortcut/src/Entity/ShortcutSet.php b/core/modules/shortcut/src/Entity/ShortcutSet.php index 32c1bb6..b1b0fd7 100644 --- a/core/modules/shortcut/src/Entity/ShortcutSet.php +++ b/core/modules/shortcut/src/Entity/ShortcutSet.php @@ -63,15 +63,17 @@ class ShortcutSet extends ConfigEntityBase implements ShortcutSetInterface { public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); - // Generate menu-compatible set name. - if (!$update && !$this->getOriginalId()) { + if (!$update && !$this->isSyncing()) { // Save a new shortcut set with links copied from the user's default set. $default_set = shortcut_default_set(); - foreach ($default_set->getShortcuts() as $shortcut) { - $shortcut = $shortcut->createDuplicate(); - $shortcut->enforceIsNew(); - $shortcut->shortcut_set->target_id = $this->id(); - $shortcut->save(); + // This is the default set, do not copy shortcuts. + if ($default_set->id() != $this->id()) { + foreach ($default_set->getShortcuts() as $shortcut) { + $shortcut = $shortcut->createDuplicate(); + $shortcut->enforceIsNew(); + $shortcut->shortcut_set->target_id = $this->id(); + $shortcut->save(); + } } } } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 1188822..2606747 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -7,11 +7,13 @@ namespace Drupal\simpletest; +use Drupal\Component\Utility\String; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DrupalKernel; use Drupal\Core\KeyValueStore\KeyValueMemoryFactory; use Drupal\Core\Language\Language; +use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Request; @@ -338,6 +340,39 @@ protected function installSchema($module, $tables) { ))); } + + + /** + * Installs the tables for a specific entity type. + * + * @param string $entity_type_id + * The ID of the entity type. + */ + protected function installEntitySchema($entity_type_id) { + /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */ + $entity_manager = $this->container->get('entity.manager'); + /** @var \Drupal\Core\Database\Schema $schema_handler */ + $schema_handler = $this->container->get('database')->schema(); + + $storage = $entity_manager->getStorage($entity_type_id); + if ($storage instanceof EntitySchemaProviderInterface) { + $schema = $storage->getSchema(); + foreach ($schema as $table_name => $table_schema) { + $schema_handler->createTable($table_name, $table_schema); + } + + $this->pass(String::format('Installed entity type tables for the %entity_type entity type: %tables', array( + '%entity_type' => $entity_type_id, + '%tables' => '{' . implode('}, {', array_keys($schema)) . '}', + ))); + } + else { + throw new \RuntimeException(String::format('Entity type %entity_type does not support automatic schema installation.', array( + '%entity-type' => $entity_type_id, + ))); + } + } + /** * Enables modules for this test. * diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index 0a97a63..1537393 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -75,7 +75,7 @@ function testEnableModulesLoad() { */ function testEnableModulesInstall() { $module = 'node'; - $table = 'node'; + $table = 'node_access'; // Verify that the module does not exist yet. $this->assertFalse(\Drupal::moduleHandler()->moduleExists($module), "$module module not found."); @@ -108,9 +108,9 @@ function testEnableModulesInstall() { */ function testEnableModulesInstallContainer() { // Install Node module. - $this->enableModules(array('field', 'node')); + $this->enableModules(array('user', 'field', 'node')); - $this->installSchema('node', array('node', 'node_field_data')); + $this->installEntitySchema('node', array('node', 'node_field_data')); // Perform an entity query against node. $query = \Drupal::entityQuery('node'); // Disable node access checks, since User module is not enabled. @@ -125,7 +125,7 @@ function testEnableModulesInstallContainer() { */ function testInstallSchema() { $module = 'entity_test'; - $table = 'entity_test'; + $table = 'entity_test_example'; // Verify that we can install a table from the module schema. $this->installSchema($module, $table); $this->assertTrue(db_table_exists($table), "'$table' database table found."); @@ -172,6 +172,18 @@ function testInstallSchema() { } /** + * Tests expected behavior of installEntitySchema(). + */ + function testInstallEntitySchema() { + $entity = 'entity_test'; + // The entity_test Entity has a field that depends on the User module. + $this->enableModules(array('user')); + // Verity that the entity schema is created properly. + $this->installEntitySchema($entity); + $this->assertTrue(db_table_exists($entity), "'$entity' database table found."); + } + + /** * Tests expected behavior of installConfig(). */ function testInstallConfig() { diff --git a/core/modules/system/src/Tests/Action/ActionUnitTest.php b/core/modules/system/src/Tests/Action/ActionUnitTest.php index 7e2f651..70fc663 100644 --- a/core/modules/system/src/Tests/Action/ActionUnitTest.php +++ b/core/modules/system/src/Tests/Action/ActionUnitTest.php @@ -45,7 +45,7 @@ protected function setUp() { parent::setUp(); $this->actionManager = $this->container->get('plugin.manager.action'); - $this->installSchema('user', array('users', 'users_roles')); + $this->installEntitySchema('user'); $this->installSchema('system', array('sequences')); } diff --git a/core/modules/system/src/Tests/Common/WriteRecordTest.php b/core/modules/system/src/Tests/Common/WriteRecordTest.php index ffe5fdb..fd6ac63 100644 --- a/core/modules/system/src/Tests/Common/WriteRecordTest.php +++ b/core/modules/system/src/Tests/Common/WriteRecordTest.php @@ -7,19 +7,19 @@ namespace Drupal\system\Tests\Common; -use Drupal\simpletest\WebTestBase; +use Drupal\simpletest\DrupalUnitTestBase; /** * Tests writing of data records with drupal_write_record(). */ -class WriteRecordTest extends WebTestBase { +class WriteRecordTest extends DrupalUnitTestBase { /** * Modules to enable. * * @var array */ - public static $modules = array('database_test', 'node'); + public static $modules = array('database_test'); public static function getInfo() { return array( @@ -33,6 +33,8 @@ public static function getInfo() { * Tests the drupal_write_record() API function. */ function testDrupalWriteRecord() { + $this->installSchema('database_test', array('test', 'test_null', 'test_serialized', 'test_composite_primary')); + // Insert a record with no columns populated. $record = array(); $insert_result = drupal_write_record('test', $record); @@ -135,15 +137,14 @@ function testDrupalWriteRecord() { $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a valid update is run without changing any values.'); // Insert an object record for a table with a multi-field primary key. - $node_access = new \stdClass(); - $node_access->nid = mt_rand(); - $node_access->gid = mt_rand(); - $node_access->realm = $this->randomName(); - $insert_result = drupal_write_record('node_access', $node_access); + $composite_primary = new \stdClass(); + $composite_primary->name = $this->randomName(); + $composite_primary->age = mt_rand(); + $insert_result = drupal_write_record('test_composite_primary', $composite_primary); $this->assertTrue($insert_result == SAVED_NEW, 'Correct value returned when a record is inserted with drupal_write_record() for a table with a multi-field primary key.'); // Update the record. - $update_result = drupal_write_record('node_access', $node_access, array('nid', 'gid', 'realm')); + $update_result = drupal_write_record('test_composite_primary', $composite_primary, array('name', 'job')); $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.'); } diff --git a/core/modules/system/src/Tests/Entity/EntityApiTest.php b/core/modules/system/src/Tests/Entity/EntityApiTest.php index 4f693c9..caa7bb8 100644 --- a/core/modules/system/src/Tests/Entity/EntityApiTest.php +++ b/core/modules/system/src/Tests/Entity/EntityApiTest.php @@ -25,16 +25,10 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('entity_test', array( - 'entity_test_mul', - 'entity_test_mul_property_data', - 'entity_test_rev', - 'entity_test_rev_revision', - 'entity_test_mulrev', - 'entity_test_mulrev_revision', - 'entity_test_mulrev_property_data', - 'entity_test_mulrev_property_revision' - )); + + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mulrev'); } /** diff --git a/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php b/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php index 1d49a02..fa701a7 100644 --- a/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php +++ b/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php @@ -44,9 +44,14 @@ public static function getInfo() { public function setUp() { parent::setUp(); + + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installSchema('user', array('users_data')); - $this->installSchema('node', array('node', 'node_revision', 'node_field_data', 'node_field_revision', 'node_access')); - $this->installSchema('comment', array('comment', 'comment_entity_statistics')); + $this->installSchema('file', array('file_usage')); + $this->installSchema('node', array('node_access')); + $this->installSchema('comment', array('comment_entity_statistics')); } /** @@ -219,7 +224,8 @@ public function testCommentHooks() { * Tests hook invocations for CRUD operations on files. */ public function testFileHooks() { - $this->installSchema('file', array('file_managed', 'file_usage')); + $this->installEntitySchema('file'); + $url = 'public://entity_crud_hook_test.file'; file_put_contents($url, 'Test test test'); $file = entity_create('file', array( @@ -346,7 +352,7 @@ public function testNodeHooks() { * Tests hook invocations for CRUD operations on taxonomy terms. */ public function testTaxonomyTermHooks() { - $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy')); + $this->installEntitySchema('taxonomy_term'); $vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => 'Test vocabulary', @@ -415,7 +421,7 @@ public function testTaxonomyTermHooks() { * Tests hook invocations for CRUD operations on taxonomy vocabularies. */ public function testTaxonomyVocabularyHooks() { - $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy')); + $this->installEntitySchema('taxonomy_term'); $vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => 'Test vocabulary', diff --git a/core/modules/system/src/Tests/Entity/EntityFieldTest.php b/core/modules/system/src/Tests/Entity/EntityFieldTest.php index a838d2f..f532b1f 100644 --- a/core/modules/system/src/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php @@ -41,18 +41,12 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('user', array('users_data')); - $this->installSchema('node', array('node', 'node_revision', 'node_field_data', 'node_field_revision', 'node_access')); - $this->installSchema('entity_test', array( - 'entity_test_mul', - 'entity_test_mul_property_data', - 'entity_test_rev', - 'entity_test_rev_revision', - 'entity_test_mulrev', - 'entity_test_mulrev_revision', - 'entity_test_mulrev_property_data', - 'entity_test_mulrev_property_revision' - )); + + $this->installEntitySchema('node'); + + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mulrev'); // Create the test field. entity_test_install(); diff --git a/core/modules/system/src/Tests/Entity/EntityLanguageTestBase.php b/core/modules/system/src/Tests/Entity/EntityLanguageTestBase.php index ada2fbd..c76fc97 100644 --- a/core/modules/system/src/Tests/Entity/EntityLanguageTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityLanguageTestBase.php @@ -50,16 +50,10 @@ function setUp() { $this->languageManager = $this->container->get('language_manager'); - $this->installSchema('entity_test', array( - 'entity_test_mul', - 'entity_test_mul_property_data', - 'entity_test_rev', - 'entity_test_rev_revision', - 'entity_test_mulrev', - 'entity_test_mulrev_revision', - 'entity_test_mulrev_property_data', - 'entity_test_mulrev_property_revision', - )); + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mulrev'); + $this->installConfig(array('language')); // Create the test field. diff --git a/core/modules/system/src/Tests/Entity/EntityQueryRelationshipTest.php b/core/modules/system/src/Tests/Entity/EntityQueryRelationshipTest.php index e5d8cf4..8d67666 100644 --- a/core/modules/system/src/Tests/Entity/EntityQueryRelationshipTest.php +++ b/core/modules/system/src/Tests/Entity/EntityQueryRelationshipTest.php @@ -70,7 +70,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy')); + $this->installEntitySchema('taxonomy_term'); // We want a taxonomy term reference field. It needs a vocabulary, terms, // a field and an instance. First, create the vocabulary. diff --git a/core/modules/system/src/Tests/Entity/EntityQueryTest.php b/core/modules/system/src/Tests/Entity/EntityQueryTest.php index 07ee0e9..b3cb9f7 100644 --- a/core/modules/system/src/Tests/Entity/EntityQueryTest.php +++ b/core/modules/system/src/Tests/Entity/EntityQueryTest.php @@ -57,7 +57,9 @@ public static function getInfo() { function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_revision', 'entity_test_mulrev_property_data', 'entity_test_mulrev_property_revision')); + + $this->installEntitySchema('entity_test_mulrev'); + $this->installConfig(array('language')); $figures = drupal_strtolower($this->randomName()); diff --git a/core/modules/system/src/Tests/Entity/EntityUUIDTest.php b/core/modules/system/src/Tests/Entity/EntityUUIDTest.php index ec7134c..a0ec726 100644 --- a/core/modules/system/src/Tests/Entity/EntityUUIDTest.php +++ b/core/modules/system/src/Tests/Entity/EntityUUIDTest.php @@ -23,16 +23,9 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('entity_test', array( - 'entity_test_mul', - 'entity_test_mul_property_data', - 'entity_test_rev', - 'entity_test_rev_revision', - 'entity_test_mulrev', - 'entity_test_mulrev_revision', - 'entity_test_mulrev_property_data', - 'entity_test_mulrev_property_revision', - )); + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mulrev'); } /** diff --git a/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php b/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php index 8ea9daa..24a4de9 100644 --- a/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php @@ -42,9 +42,11 @@ public function setUp() { $this->entityManager = $this->container->get('entity.manager'); $this->state = $this->container->get('state'); - $this->installSchema('user', array('users', 'users_roles')); $this->installSchema('system', 'sequences'); - $this->installSchema('entity_test', 'entity_test'); + + $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test'); + $this->installConfig(array('field')); } diff --git a/core/modules/system/src/Tests/Entity/EntityValidationTest.php b/core/modules/system/src/Tests/Entity/EntityValidationTest.php index 09e1e54..dceab23 100644 --- a/core/modules/system/src/Tests/Entity/EntityValidationTest.php +++ b/core/modules/system/src/Tests/Entity/EntityValidationTest.php @@ -32,17 +32,10 @@ public static function getInfo() { */ public function setUp() { parent::setUp(); - $this->installSchema('user', array('users_data')); - $this->installSchema('entity_test', array( - 'entity_test_mul', - 'entity_test_mul_property_data', - 'entity_test_rev', - 'entity_test_rev_revision', - 'entity_test_mulrev', - 'entity_test_mulrev_revision', - 'entity_test_mulrev_property_data', - 'entity_test_mulrev_property_revision' - )); + + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mulrev'); // Create the test field. entity_test_install(); diff --git a/core/modules/system/src/Tests/Entity/FieldAccessTest.php b/core/modules/system/src/Tests/Entity/FieldAccessTest.php index 7395dfa..d72b297 100644 --- a/core/modules/system/src/Tests/Entity/FieldAccessTest.php +++ b/core/modules/system/src/Tests/Entity/FieldAccessTest.php @@ -44,8 +44,9 @@ protected function setUp() { // Install field configuration. $this->installConfig(array('field')); // The users table is needed for creating dummy user accounts. - $this->installSchema('user', array('users')); + $this->installEntitySchema('user'); // Register entity_test text field. + module_load_install('entity_test'); entity_test_install(); } diff --git a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php index a030b70..ab07227 100644 --- a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php @@ -66,7 +66,8 @@ public static function getInfo() { function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision')); + + $this->installEntitySchema('entity_test_rev'); $entity_type = 'entity_test_rev'; $this->field_name = strtolower($this->randomName()); @@ -562,9 +563,9 @@ public function testTableNames() { 'deleted' => TRUE, )); $expected = 'field_deleted_data_' . substr(hash('sha256', $field->uuid), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field, TRUE), $expected); $expected = 'field_deleted_revision_' . substr(hash('sha256', $field->uuid), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field, TRUE), $expected); } } diff --git a/core/modules/system/src/Tests/Path/AliasTest.php b/core/modules/system/src/Tests/Path/AliasTest.php index d118b2e..0baae45 100644 --- a/core/modules/system/src/Tests/Path/AliasTest.php +++ b/core/modules/system/src/Tests/Path/AliasTest.php @@ -84,7 +84,7 @@ function testLookupPath() { $this->fixtures->createTables($connection); //Create AliasManager and Path object. - $aliasManager = $this->container->get('path.alias_manager'); + $aliasManager = $this->container->get('path.alias_manager.cached'); $aliasStorage = new AliasStorage($connection, $this->container->get('module_handler')); // Test the situation where the source is the same for multiple aliases. @@ -106,7 +106,7 @@ function testLookupPath() { ); $aliasStorage->save($path['source'], $path['alias'], $path['langcode']); // Hook that clears cache is not executed with unit tests. - \Drupal::service('path.alias_manager')->cacheClear(); + \Drupal::service('path.alias_manager.cached')->cacheClear(); $this->assertEqual($aliasManager->getAliasByPath($path['source']), $path['alias'], 'English alias overrides language-neutral alias.'); $this->assertEqual($aliasManager->getPathByAlias($path['alias']), $path['source'], 'English source overrides language-neutral source.'); @@ -170,7 +170,7 @@ function testWhitelist() { // Create AliasManager and Path object. $aliasStorage = new AliasStorage($connection, $this->container->get('module_handler')); $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage); - $aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend); + $aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager')); // No alias for user and admin yet, so should be NULL. $this->assertNull($whitelist->get('user')); diff --git a/core/modules/system/src/Tests/Routing/MockAliasManager.php b/core/modules/system/src/Tests/Routing/MockAliasManager.php index 6c3839d..5337099 100644 --- a/core/modules/system/src/Tests/Routing/MockAliasManager.php +++ b/core/modules/system/src/Tests/Routing/MockAliasManager.php @@ -82,6 +82,20 @@ public function getAliasByPath($path, $langcode = NULL) { /** * {@inheritdoc} */ + public function getPathLookups() { + return array_keys($this->lookedUp); + } + + /** + * {@inheritdoc} + */ + public function preloadPathLookups(array $path_list) { + // Not needed. + } + + /** + * {@inheritdoc} + */ public function cacheClear($source = NULL) { // Not needed. } diff --git a/core/modules/system/src/Tests/TypedData/TypedDataTest.php b/core/modules/system/src/Tests/TypedData/TypedDataTest.php index 5893f7b..37c4af9 100644 --- a/core/modules/system/src/Tests/TypedData/TypedDataTest.php +++ b/core/modules/system/src/Tests/TypedData/TypedDataTest.php @@ -44,7 +44,7 @@ public static function getInfo() { public function setUp() { parent::setup(); - $this->installSchema('file', array('file_managed', "file_usage")); + $this->installEntitySchema('file'); $this->typedDataManager = $this->container->get('typed_data_manager'); } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 377c30e..8feb47b 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -651,16 +651,14 @@ function system_element_info() { * Implements hook_theme_suggestions_HOOK(). */ function system_theme_suggestions_html(array $variables) { - $path_args = explode('/', current_path()); - return theme_get_suggestions($path_args, 'html'); + return theme_get_suggestions(arg(), 'html'); } /** * Implements hook_theme_suggestions_HOOK(). */ function system_theme_suggestions_page(array $variables) { - $path_args = explode('/', current_path()); - return theme_get_suggestions($path_args, 'page'); + return theme_get_suggestions(arg(), 'page'); } /** @@ -1719,19 +1717,19 @@ function theme_system_config_form($variables) { * Implements hook_path_update(). */ function system_path_update() { - \Drupal::service('path.alias_manager')->cacheClear(); + \Drupal::service('path.alias_manager.cached')->cacheClear(); } /** * Implements hook_path_insert(). */ function system_path_insert() { - \Drupal::service('path.alias_manager')->cacheClear(); + \Drupal::service('path.alias_manager.cached')->cacheClear(); } /** * Implements hook_path_delete(). */ function system_path_delete($path) { - \Drupal::service('path.alias_manager')->cacheClear(); + \Drupal::service('path.alias_manager.cached')->cacheClear(); } diff --git a/core/modules/system/tests/modules/database_test/database_test.install b/core/modules/system/tests/modules/database_test/database_test.install index 866d453..7c74c1c 100644 --- a/core/modules/system/tests/modules/database_test/database_test.install +++ b/core/modules/system/tests/modules/database_test/database_test.install @@ -245,5 +245,34 @@ function database_test_schema() { ), ); + $schema['test_composite_primary'] = array( + 'description' => 'Basic test table with a composite primary key', + 'fields' => array( + 'name' => array( + 'description' => "A person's name", + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + ), + 'age' => array( + 'description' => "The person's age", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'job' => array( + 'description' => "The person's job", + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => 'Undefined', + ), + ), + 'primary key' => array('name', 'age'), + ); + return $schema; } diff --git a/core/modules/system/tests/modules/entity_test/entity_test.install b/core/modules/system/tests/modules/entity_test/entity_test.install index 9e48029..015b067 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.install +++ b/core/modules/system/tests/modules/entity_test/entity_test.install @@ -42,7 +42,7 @@ function entity_test_install() { */ function entity_test_schema() { // Schema for simple entity. - $schema['entity_test'] = array( + $schema['entity_test_example'] = array( 'description' => 'Stores entity_test items.', 'fields' => array( 'id' => array( @@ -50,392 +50,8 @@ function entity_test_schema() { 'not null' => TRUE, 'description' => 'Primary Key: Unique entity-test item ID.', ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'type' => array( - 'description' => 'The bundle of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of the original variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'name' => array( - 'description' => 'The name of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'user_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - 'description' => 'The {users}.uid of the associated user.', - ), ), 'primary key' => array('id'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), ); - - // Schema for entity with revisions. - $schema['entity_test_rev'] = array( - 'description' => 'Stores entity_test_rev items.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique entity-test item ID.', - ), - 'revision_id' => array( - 'description' => 'The current {entity_test_rev_property_revision}.revision_id version identifier.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'type' => array( - 'description' => 'The bundle of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'name' => array( - 'description' => 'The name of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'user_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - 'description' => 'The {users}.uid of the associated user.', - ), - ), - 'primary key' => array('id'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), - ); - $schema['entity_test_rev_revision'] = array( - 'description' => 'Stores entity_test_rev item property revisions.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The {entity_test_rev}.id of the test entity.', - ), - 'revision_id' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The primary identifier for this version.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'name' => array( - 'description' => 'The name of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'user_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - 'description' => 'The {users}.uid of the associated user.', - ), - ), - 'indexes' => array( - 'user_id' => array('user_id'), - ), - 'foreign keys' => array( - 'user_id' => array('users' => 'uid'), - 'id' => array('entity_test_rev' => 'id'), - ), - 'primary key' => array('revision_id'), - ); - - // Schema for entity with data table. - $schema['entity_test_mul'] = array( - 'description' => 'Stores entity_test_mul items.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique entity-test item ID.', - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'type' => array( - 'description' => 'The bundle of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of the original variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'primary key' => array('id'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), - ); - $schema['entity_test_mul_property_data'] = array( - 'description' => 'Stores entity_test_mul item properties.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The {entity_test_mul}.id of the test entity.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'default_langcode' => array( - 'description' => 'Boolean indicating whether the current variant is in the original entity language.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'name' => array( - 'description' => 'The name of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => FALSE, - ), - 'user_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - 'description' => 'The {users}.uid of the associated user.', - ), - ), - 'indexes' => array( - 'user_id' => array('user_id'), - ), - 'foreign keys' => array( - 'user_id' => array('users' => 'uid'), - 'id' => array('entity_test_mul' => 'id'), - ), - 'primary key' => array('id', 'langcode'), - ); - - // Schema for entity with data table and revisions. - $schema['entity_test_mulrev'] = array( - 'description' => 'Stores entity_test_mulrev items.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique entity-test item ID.', - ), - 'revision_id' => array( - 'description' => 'The current {entity_test_mulrev_property_revision}.revision_id version identifier.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'type' => array( - 'description' => 'The bundle of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'primary key' => array('id'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), - ); - $schema['entity_test_mulrev_revision'] = array( - 'description' => 'Stores entity_test_rev item property revisions.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The {entity_test_rev}.id of the test entity.', - ), - 'revision_id' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The primary identifier for this version.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - ), - 'foreign keys' => array( - 'id' => array('entity_test_rev' => 'id'), - ), - 'primary key' => array('revision_id'), - ); - $schema['entity_test_mulrev_property_data'] = array( - 'description' => 'Stores entity_test_mulrev item properties.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The {entity_test_mulrev}.id of the test entity.', - ), - 'revision_id' => array( - 'description' => 'The current {entity_test_mulrev_property_revision}.revision_id version identifier.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'default_langcode' => array( - 'description' => 'Boolean indicating whether the current variant is in the original entity language.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'name' => array( - 'description' => 'The name of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'user_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - 'description' => 'The {users}.uid of the associated user.', - ), - ), - 'indexes' => array( - 'user_id' => array('user_id'), - ), - 'foreign keys' => array( - 'user_id' => array('users' => 'uid'), - 'id' => array('entity_test_mulrev' => 'id'), - ), - 'primary key' => array('id', 'langcode'), - ); - $schema['entity_test_mulrev_property_revision'] = array( - 'description' => 'Stores entity_test_mulrev item property revisions.', - 'fields' => array( - 'id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The {entity_test_mulrev}.id of the test entity.', - ), - 'revision_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The primary identifier for this version.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this variant of this test entity.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'default_langcode' => array( - 'description' => 'Boolean indicating whether the current variant is in the original entity language.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 1, - ), - 'name' => array( - 'description' => 'The name of the test entity.', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'user_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => NULL, - 'description' => 'The {users}.uid of the associated user.', - ), - ), - 'indexes' => array( - 'user_id' => array('user_id'), - ), - 'foreign keys' => array( - 'user_id' => array('users' => 'uid'), - 'id' => array('entity_test_mulrev' => 'id'), - ), - 'primary key' => array('revision_id', 'langcode'), - ); - return $schema; } diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php index 6a8badd..0c0c753 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php @@ -28,7 +28,6 @@ * entity_keys = { * "id" = "id", * "uuid" = "uuid", - * "revision" = "revision_id", * "bundle" = "type" * }, * links = { diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php index 481711f..e881b9a 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php @@ -15,7 +15,6 @@ * label = @Translation("Entity test label callback"), * field_cache = FALSE, * base_table = "entity_test", - * revision_table = "entity_test_revision", * label_callback = "entity_test_label_callback", * fieldable = TRUE, * entity_keys = { diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php index 9930262..a8abda5 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php @@ -46,17 +46,4 @@ */ class EntityTestMul extends EntityTest { - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields = parent::baseFieldDefinitions($entity_type); - - $fields['default_langcode'] = FieldDefinition::create('boolean') - ->setLabel(t('Default language')) - ->setDescription(t('Flag to indicate whether this is the default language.')); - - return $fields; - } - } diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php index 21f6c54..b30cbb1 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php @@ -46,23 +46,4 @@ */ class EntityTestMulRev extends EntityTestRev { - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields = parent::baseFieldDefinitions($entity_type); - - $fields['revision_id'] = FieldDefinition::create('integer') - ->setLabel(t('Revision ID')) - ->setDescription(t('The version id of the test entity.')) - ->setReadOnly(TRUE) - ->setSetting('unsigned', TRUE); - - $fields['default_langcode'] = FieldDefinition::create('boolean') - ->setLabel(t('Default language')) - ->setDescription(t('Flag to indicate whether this is the default language.')); - - return $fields; - } - } diff --git a/core/modules/system/tests/modules/menu_test/src/EventSubscriber/MaintenanceModeSubscriber.php b/core/modules/system/tests/modules/menu_test/src/EventSubscriber/MaintenanceModeSubscriber.php index 0b6ae61..2071a37 100644 --- a/core/modules/system/tests/modules/menu_test/src/EventSubscriber/MaintenanceModeSubscriber.php +++ b/core/modules/system/tests/modules/menu_test/src/EventSubscriber/MaintenanceModeSubscriber.php @@ -10,8 +10,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Drupal\Core\EventSubscriber\MaintenanceModeSubscriber as CoreMaintenanceModeSubscriber; - /** * Maintenance mode subscriber to set site online on a test. @@ -27,8 +25,8 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface { public function onKernelRequestMaintenance(GetResponseEvent $event) { $request = $event->getRequest(); // Allow access to menu_login_callback even if in maintenance mode. - if ($request->attributes->get('_maintenance') == CoreMaintenanceModeSubscriber::SITE_OFFLINE && $request->attributes->get('_system_path') == 'menu_login_callback') { - $request->attributes->set('_maintenance', CoreMaintenanceModeSubscriber::SITE_ONLINE); + if ($request->attributes->get('_maintenance') == MENU_SITE_OFFLINE && $request->attributes->get('_system_path') == 'menu_login_callback') { + $request->attributes->set('_maintenance', MENU_SITE_ONLINE); } } diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 510524a..8b6996a 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\FieldDefinition; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\Language; use Drupal\Core\TypedData\DataDefinition; use Drupal\taxonomy\TermInterface; @@ -164,6 +165,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['parent'] = FieldDefinition::create('integer') ->setLabel(t('Term Parents')) ->setDescription(t('The parents of this term.')) + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) // Save new terms with no parents by default. ->setSetting('default_value', 0) ->setSetting('unsigned', TRUE) diff --git a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php index 6dfd7bd..cf25c15 100644 --- a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php +++ b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php @@ -7,7 +7,6 @@ namespace Drupal\taxonomy\Plugin\views\argument_default; -use Drupal\taxonomy\TermInterface; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; @@ -124,8 +123,8 @@ public function submitOptionsForm(&$form, &$form_state, &$options = array()) { public function getArgument() { // Load default argument from taxonomy page. if (!empty($this->options['term_page'])) { - if (($taxonomy_term = $this->request->attributes->get('taxonomy_term')) && $taxonomy_term instanceof TermInterface) { - return $taxonomy_term->id(); + if (arg(0) == 'taxonomy' && arg(1) == 'term' && is_numeric(arg(2))) { + return arg(2); } } // Load default argument from node. diff --git a/core/modules/taxonomy/src/TermStorage.php b/core/modules/taxonomy/src/TermStorage.php index b7b9201..381b0ab 100644 --- a/core/modules/taxonomy/src/TermStorage.php +++ b/core/modules/taxonomy/src/TermStorage.php @@ -8,6 +8,7 @@ namespace Drupal\taxonomy; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\ContentEntityDatabaseStorage; @@ -151,4 +152,103 @@ public function resetWeights($vid) { ->execute(); } + /** + * {@inheritdoc} + */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['taxonomy_term_data']['fields']['weight']['not null'] = TRUE; + $schema['taxonomy_term_data']['fields']['name']['not null'] = TRUE; + + unset($schema['taxonomy_term_data']['indexes']['field__vid']); + unset($schema['taxonomy_term_data']['indexes']['field__description__format']); + $schema['taxonomy_term_data']['indexes'] += array( + 'taxonomy_term__tree' => array('vid', 'weight', 'name'), + 'taxonomy_term__vid_name' => array('vid', 'name'), + 'taxonomy_term__name' => array('name'), + ); + + $schema['taxonomy_term_hierarchy'] = array( + 'description' => 'Stores the hierarchical relationship between terms.', + 'fields' => array( + 'tid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.', + ), + 'parent' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.", + ), + ), + 'indexes' => array( + 'parent' => array('parent'), + ), + 'foreign keys' => array( + 'taxonomy_term_data' => array( + 'table' => 'taxonomy_term_data', + 'columns' => array('tid' => 'tid'), + ), + ), + 'primary key' => array('tid', 'parent'), + ); + + $schema['taxonomy_index'] = array( + 'description' => 'Maintains denormalized information about node/term relationships.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record tracks.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'tid' => array( + 'description' => 'The term ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sticky' => array( + 'description' => 'Boolean indicating whether the node is sticky.', + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'size' => 'tiny', + ), + 'created' => array( + 'description' => 'The Unix timestamp when the node was created.', + 'type' => 'int', + 'not null' => TRUE, + 'default'=> 0, + ), + ), + 'primary key' => array('nid', 'tid'), + 'indexes' => array( + 'term_node' => array('tid', 'sticky', 'created'), + ), + 'foreign keys' => array( + 'tracked_node' => array( + 'table' => 'node', + 'columns' => array('nid' => 'nid'), + ), + 'term' => array( + 'table' => 'taxonomy_term_data', + 'columns' => array('tid' => 'tid'), + ), + ), + ); + + return $schema; + } + } diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTermReferenceItemTest.php b/core/modules/taxonomy/src/Tests/TaxonomyTermReferenceItemTest.php index 0a67140..78c877f 100644 --- a/core/modules/taxonomy/src/Tests/TaxonomyTermReferenceItemTest.php +++ b/core/modules/taxonomy/src/Tests/TaxonomyTermReferenceItemTest.php @@ -42,8 +42,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('taxonomy', 'taxonomy_term_data'); - $this->installSchema('taxonomy', 'taxonomy_term_hierarchy'); + $this->installEntitySchema('taxonomy_term'); $vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => $this->randomName(), diff --git a/core/modules/taxonomy/src/Tests/TermTranslationUITest.php b/core/modules/taxonomy/src/Tests/TermTranslationUITest.php index a027ade..c51428c 100644 --- a/core/modules/taxonomy/src/Tests/TermTranslationUITest.php +++ b/core/modules/taxonomy/src/Tests/TermTranslationUITest.php @@ -121,7 +121,10 @@ function testTranslateLinkVocabularyAdminPage() { $this->admin_user = $this->drupalCreateUser(array_merge(parent::getTranslatorPermissions(), array('access administration pages', 'administer taxonomy'))); $this->drupalLogin($this->admin_user); - $translatable_tid = $this->createEntity(array(), $this->langcodes[0], $this->vocabulary->id()); + $values = array( + 'name' => $this->randomName(), + ); + $translatable_tid = $this->createEntity($values, $this->langcodes[0], $this->vocabulary->id()); // Create an untranslatable vocabulary. $untranslatable_vocabulary = entity_create('taxonomy_vocabulary', array( @@ -133,7 +136,10 @@ function testTranslateLinkVocabularyAdminPage() { )); $untranslatable_vocabulary->save(); - $untranslatable_tid = $this->createEntity(array(), $this->langcodes[0], $untranslatable_vocabulary->id()); + $values = array( + 'name' => $this->randomName(), + ); + $untranslatable_tid = $this->createEntity($values, $this->langcodes[0], $untranslatable_vocabulary->id()); // Verify translation links. $this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/overview'); diff --git a/core/modules/taxonomy/src/Tests/TermValidationTest.php b/core/modules/taxonomy/src/Tests/TermValidationTest.php index 737f662..f545403 100644 --- a/core/modules/taxonomy/src/Tests/TermValidationTest.php +++ b/core/modules/taxonomy/src/Tests/TermValidationTest.php @@ -34,7 +34,7 @@ public static function getInfo() { */ public function setUp() { parent::setUp(); - $this->installSchema('taxonomy', array('taxonomy_term_data')); + $this->installEntitySchema('taxonomy_term'); } /** diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install deleted file mode 100644 index ae5dc7f..0000000 --- a/core/modules/taxonomy/taxonomy.install +++ /dev/null @@ -1,164 +0,0 @@ - 'Stores term information.', - 'fields' => array( - 'tid' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary Key: Unique term ID.', - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'vid' => array( - 'type' => 'varchar', - 'length' => EntityTypeInterface::BUNDLE_MAX_LENGTH, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The ID of the vocabulary to which the term is assigned.', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this term.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'name' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The term name.', - ), - 'description__value' => array( - 'type' => 'text', - 'not null' => FALSE, - 'size' => 'big', - 'description' => 'A description of the term.', - ), - 'description__format' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - 'description' => 'The filter format ID of the description.', - ), - 'weight' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The weight of this term in relation to other terms.', - ), - 'changed' => array( - 'description' => 'The Unix timestamp when the term was most recently saved.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'primary key' => array('tid'), - 'unique keys' => array( - 'uuid' => array('uuid'), - ), - 'indexes' => array( - 'taxonomy_tree' => array('vid', 'weight', 'name'), - 'vid_name' => array('vid', 'name'), - 'name' => array('name'), - ), - ); - - $schema['taxonomy_term_hierarchy'] = array( - 'description' => 'Stores the hierarchical relationship between terms.', - 'fields' => array( - 'tid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.', - ), - 'parent' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.", - ), - ), - 'indexes' => array( - 'parent' => array('parent'), - ), - 'foreign keys' => array( - 'taxonomy_term_data' => array( - 'table' => 'taxonomy_term_data', - 'columns' => array('tid' => 'tid'), - ), - ), - 'primary key' => array('tid', 'parent'), - ); - - $schema['taxonomy_index'] = array( - 'description' => 'Maintains denormalized information about node/term relationships.', - 'fields' => array( - 'nid' => array( - 'description' => 'The {node}.nid this record tracks.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'tid' => array( - 'description' => 'The term ID.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'sticky' => array( - 'description' => 'Boolean indicating whether the node is sticky.', - 'type' => 'int', - 'not null' => FALSE, - 'default' => 0, - 'size' => 'tiny', - ), - 'created' => array( - 'description' => 'The Unix timestamp when the node was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default'=> 0, - ), - ), - 'primary key' => array('nid', 'tid'), - 'indexes' => array( - 'term_node' => array('tid', 'sticky', 'created'), - ), - 'foreign keys' => array( - 'tracked_node' => array( - 'table' => 'node', - 'columns' => array('nid' => 'nid'), - ), - 'term' => array( - 'table' => 'taxonomy_term_data', - 'columns' => array('tid' => 'tid'), - ), - ), - ); - - return $schema; -} diff --git a/core/modules/text/src/Tests/Formatter/TextPlainUnitTest.php b/core/modules/text/src/Tests/Formatter/TextPlainUnitTest.php index 1c35b24..3357d1d 100644 --- a/core/modules/text/src/Tests/Formatter/TextPlainUnitTest.php +++ b/core/modules/text/src/Tests/Formatter/TextPlainUnitTest.php @@ -48,7 +48,7 @@ function setUp() { // Configure the theme system. $this->installConfig(array('system', 'field')); - $this->installSchema('entity_test', 'entity_test'); + $this->installEntitySchema('entity_test'); // @todo Add helper methods for all of the following. diff --git a/core/modules/text/src/Tests/TextWithSummaryItemTest.php b/core/modules/text/src/Tests/TextWithSummaryItemTest.php index 9b93095..30af0bf 100644 --- a/core/modules/text/src/Tests/TextWithSummaryItemTest.php +++ b/core/modules/text/src/Tests/TextWithSummaryItemTest.php @@ -50,7 +50,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision')); + $this->installEntitySchema('entity_test_rev'); // Create the necessary formats. $this->installConfig(array('filter')); diff --git a/core/modules/update/src/Tests/UpdateCoreTest.php b/core/modules/update/src/Tests/UpdateCoreTest.php index 00782b8..7928455 100644 --- a/core/modules/update/src/Tests/UpdateCoreTest.php +++ b/core/modules/update/src/Tests/UpdateCoreTest.php @@ -29,7 +29,7 @@ public static function getInfo() { function setUp() { parent::setUp(); - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer modules', 'administer themes')); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer modules')); $this->drupalLogin($admin_user); } diff --git a/core/modules/update/update.module b/core/modules/update/update.module index ee2f107..6341857 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -12,7 +12,6 @@ */ use Drupal\Core\Site\Settings; -use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; // These are internally used constants for this code, do not modify. @@ -98,11 +97,8 @@ function update_help($route_name, Request $request) { * Implements hook_page_build(). */ function update_page_build() { - /** @var \Drupal\Core\Routing\AdminContext $admin_context */ - $admin_context = \Drupal::service('router.admin_context'); - if ($admin_context->isAdminRoute(\Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) && \Drupal::currentUser()->hasPermission('administer site configuration')) { - $current_path = current_path(); - switch ($current_path) { + if (arg(0) == 'admin' && user_access('administer site configuration')) { + switch (current_path()) { // These pages don't need additional nagging. case 'admin/appearance/update': case 'admin/appearance/install': diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 42a2292..5ac1c00 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -378,9 +378,12 @@ public function validate(array $form, array &$form_state) { // Move text value for user signature into 'signature'. $form_state['values']['signature'] = $form_state['values']['signature']['value']; - $user_schema = drupal_get_schema('users'); - if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) { - $this->setFormError('signature', $form_state, $this->t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length']))); + // @todo Make the user signature field use a widget to benefit from + // automatic typed data validation in https://drupal.org/node/2227381. + $field_definitions = $this->entityManager->getFieldDefinitions('user', $this->getEntity()->bundle()); + $max_length = $field_definitions['signature']->getSetting('max_length'); + if (drupal_strlen($form_state['values']['signature']) > $max_length) { + $this->setFormError('signature', $form_state, $this->t('The signature is too long: it must be %max characters or less.', array('%max' => $max_length))); } } } diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php index 25eb641..6c0fb85 100644 --- a/core/modules/user/src/Entity/User.php +++ b/core/modules/user/src/Entity/User.php @@ -18,6 +18,9 @@ /** * Defines the user entity class. * + * The base table name here is plural, despite Drupal table naming standards, + * because "user" is a reserved word in many databases. + * * @ContentEntityType( * id = "user", * label = @Translation("User"), @@ -500,8 +503,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['status'] = FieldDefinition::create('boolean') ->setLabel(t('User status')) - ->setDescription(t('Whether the user is active (1) or blocked (0).')) - ->setSetting('default_value', 1); + ->setDescription(t('Whether the user is active or blocked.')) + // @todo As the status has access implications users should be created as + // blocked by default and activated explicitly if needed. See + // https://drupal.org/node/2248969. + ->setSetting('default_value', TRUE); $fields['created'] = FieldDefinition::create('created') ->setLabel(t('Created')) @@ -525,6 +531,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // @todo Convert this to entity_reference_field, see // https://drupal.org/node/2044859. $fields['roles'] = FieldDefinition::create('string') + ->setCustomStorage(TRUE) ->setLabel(t('Roles')) ->setCardinality(FieldDefinitionInterface::CARDINALITY_UNLIMITED) ->setDescription(t('The roles the user has.')); diff --git a/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php b/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php index 1e8d114..3c478f7 100644 --- a/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php +++ b/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php @@ -11,7 +11,6 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Drupal\Core\EventSubscriber\MaintenanceModeSubscriber as CoreMaintenanceModeSubscriber; /** * Maintenance mode subscriber to logout users. @@ -29,7 +28,7 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) { $request = $event->getRequest(); $site_status = $request->attributes->get('_maintenance'); $path = $request->attributes->get('_system_path'); - if ($site_status == CoreMaintenanceModeSubscriber::SITE_OFFLINE) { + if ($site_status == MENU_SITE_OFFLINE) { // If the site is offline, log out unprivileged users. if ($user->isAuthenticated() && !$user->hasPermission('access site in maintenance mode')) { user_logout(); @@ -47,12 +46,12 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) { case 'user/login': case 'user/password': // Disable offline mode. - $request->attributes->set('_maintenance', CoreMaintenanceModeSubscriber::SITE_ONLINE); + $request->attributes->set('_maintenance', MENU_SITE_ONLINE); break; default: if (strpos($path, 'user/reset/') === 0) { // Disable offline mode. - $request->attributes->set('_maintenance', CoreMaintenanceModeSubscriber::SITE_ONLINE); + $request->attributes->set('_maintenance', MENU_SITE_ONLINE); } break; } diff --git a/core/modules/user/src/Plugin/Block/UserLoginBlock.php b/core/modules/user/src/Plugin/Block/UserLoginBlock.php index 3466c21..b2cff4c 100644 --- a/core/modules/user/src/Plugin/Block/UserLoginBlock.php +++ b/core/modules/user/src/Plugin/Block/UserLoginBlock.php @@ -9,7 +9,6 @@ use Drupal\Core\Session\AccountInterface; use Drupal\block\BlockBase; -use Symfony\Cmf\Component\Routing\RouteObjectInterface; /** * Provides a 'User login' block. @@ -26,8 +25,7 @@ class UserLoginBlock extends BlockBase { * {@inheritdoc} */ public function access(AccountInterface $account) { - $route_name = \Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_NAME); - return ($account->isAnonymous() && !in_array($route_name, array('user.register', 'user.login', 'user.logout'))); + return (!$account->id() && !(arg(0) == 'user' && !is_numeric(arg(1)))); } /** diff --git a/core/modules/user/src/Tests/UserAdminTest.php b/core/modules/user/src/Tests/UserAdminTest.php index d8d384d..f71d1e6 100644 --- a/core/modules/user/src/Tests/UserAdminTest.php +++ b/core/modules/user/src/Tests/UserAdminTest.php @@ -34,13 +34,22 @@ public static function getInfo() { */ function testUserAdmin() { $user_a = $this->drupalCreateUser(); + $user_a->name = 'User A'; $user_a->mail = $this->randomName() . '@example.com'; $user_a->save(); $user_b = $this->drupalCreateUser(array('administer taxonomy')); + $user_b->name = 'User B'; + $user_b->save(); $user_c = $this->drupalCreateUser(array('administer taxonomy')); + $user_c->name = 'User C'; + $user_c->save(); // Create admin user to delete registered user. $admin_user = $this->drupalCreateUser(array('administer users')); + // Use a predictable name so that we can reliably order the user admin page + // by name. + $admin_user->name = 'Admin user'; + $admin_user->save(); $this->drupalLogin($admin_user); $this->drupalGet('admin/people'); $this->assertText($user_a->getUsername(), 'Found user A on admin users page'); @@ -86,8 +95,12 @@ function testUserAdmin() { $this->assertTrue($account->isActive(), 'User C not blocked'); $edit = array(); $edit['action'] = 'user_block_user_action'; - $edit['user_bulk_form[1]'] = TRUE; - $this->drupalPostForm('admin/people', $edit, t('Apply')); + $edit['user_bulk_form[4]'] = TRUE; + $this->drupalPostForm('admin/people', $edit, t('Apply'), array( + // Sort the table by username so that we know reliably which user will be + // targeted with the blocking action. + 'query' => array('order' => 'name', 'sort' => 'asc') + )); $account = user_load($user_c->id(), TRUE); $this->assertTrue($account->isBlocked(), 'User C blocked'); @@ -100,8 +113,12 @@ function testUserAdmin() { // Test unblocking of a user from /admin/people page and sending of activation mail $editunblock = array(); $editunblock['action'] = 'user_unblock_user_action'; - $editunblock['user_bulk_form[1]'] = TRUE; - $this->drupalPostForm('admin/people', $editunblock, t('Apply')); + $editunblock['user_bulk_form[4]'] = TRUE; + $this->drupalPostForm('admin/people', $editunblock, t('Apply'), array( + // Sort the table by username so that we know reliably which user will be + // targeted with the blocking action. + 'query' => array('order' => 'name', 'sort' => 'asc') + )); $account = user_load($user_c->id(), TRUE); $this->assertTrue($account->isActive(), 'User C unblocked'); $this->assertMail("to", $account->getEmail(), "Activation mail sent to user C"); diff --git a/core/modules/user/src/Tests/UserInstallTest.php b/core/modules/user/src/Tests/UserInstallTest.php index 7014a03..9fdb8db 100644 --- a/core/modules/user/src/Tests/UserInstallTest.php +++ b/core/modules/user/src/Tests/UserInstallTest.php @@ -37,7 +37,9 @@ public static function getInfo() { */ protected function setUp() { parent::setUp(); - $this->installSchema('user', array('users')); + $this->container->get('module_handler')->loadInclude('user', 'install'); + $this->installEntitySchema('user'); + user_install(); } @@ -45,7 +47,6 @@ protected function setUp() { * Test that the initial users have correct values. */ public function testUserInstall() { - user_install(); $anon = db_query('SELECT * FROM {users} WHERE uid = 0')->fetchObject(); $admin = db_query('SELECT * FROM {users} WHERE uid = 1')->fetchObject(); $this->assertFalse(empty($anon->uuid), 'Anon user has a UUID'); diff --git a/core/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php index 499877e..06d42b9 100644 --- a/core/modules/user/src/Tests/UserValidationTest.php +++ b/core/modules/user/src/Tests/UserValidationTest.php @@ -36,7 +36,7 @@ public static function getInfo() { */ public function setUp() { parent::setUp(); - $this->installSchema('user', array('users')); + $this->installEntitySchema('user'); $this->installSchema('system', array('sequences')); } diff --git a/core/modules/user/src/Tests/Views/UserUnitTestBase.php b/core/modules/user/src/Tests/Views/UserUnitTestBase.php index eafc620..16ba123 100644 --- a/core/modules/user/src/Tests/Views/UserUnitTestBase.php +++ b/core/modules/user/src/Tests/Views/UserUnitTestBase.php @@ -48,7 +48,7 @@ protected function setUp() { ViewTestData::createTestViews(get_class($this), array('user_test_views')); - $this->installSchema('user', array('users', 'users_roles')); + $this->installEntitySchema('user'); $this->installSchema('system', 'sequences'); $entity_manager = $this->container->get('entity.manager'); diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php index d5d91ca..44d323f 100644 --- a/core/modules/user/src/UserStorage.php +++ b/core/modules/user/src/UserStorage.php @@ -47,15 +47,13 @@ class UserStorage extends ContentEntityDatabaseStorage implements UserStorageInt * The database connection to be used. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. - * @param \Drupal\Component\Uuid\UuidInterface $uuid_service - * The UUID Service. * @param \Drupal\Core\Password\PasswordInterface $password * The password hashing service. * @param \Drupal\user\UserDataInterface $user_data * The user data service. */ - public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, UuidInterface $uuid_service, PasswordInterface $password, UserDataInterface $user_data) { - parent::__construct($entity_type, $database, $entity_manager, $uuid_service); + public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, PasswordInterface $password, UserDataInterface $user_data) { + parent::__construct($entity_type, $database, $entity_manager); $this->password = $password; $this->userData = $user_data; @@ -69,7 +67,6 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $entity_type, $container->get('database'), $container->get('entity.manager'), - $container->get('uuid'), $container->get('password'), $container->get('user.data') ); @@ -154,4 +151,59 @@ public function updateLastLoginTimestamp(UserInterface $account) { ->execute(); } + /** + * {@inheritdoc} + */ + public function getSchema() { + $schema = parent::getSchema(); + + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['users']['fields']['access']['not null'] = TRUE; + $schema['users']['fields']['created']['not null'] = TRUE; + $schema['users']['fields']['name']['not null'] = TRUE; + + // The "users" table does not use serial identifiers. + $schema['users']['fields']['uid']['type'] = 'int'; + $schema['users']['indexes'] += array( + 'user__access' => array('access'), + 'user__created' => array('created'), + 'user__mail' => array('mail'), + ); + $schema['users']['unique keys'] += array( + 'user__name' => array('name'), + ); + + $schema['users_roles'] = array( + 'description' => 'Maps users to roles.', + 'fields' => array( + 'uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Primary Key: {users}.uid for user.', + ), + 'rid' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'description' => 'Primary Key: ID for the role.', + ), + ), + 'primary key' => array('uid', 'rid'), + 'indexes' => array( + 'rid' => array('rid'), + ), + 'foreign keys' => array( + 'user' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + ), + ); + + return $schema; + } + } diff --git a/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml index b16cb3e..8b62fc7 100644 --- a/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml +++ b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml @@ -115,7 +115,20 @@ display: group_items: { } reduce_duplicates: '1' plugin_id: user_permissions - sorts: { } + sorts: + uid: + id: uid + table: users + field: uid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + plugin_id: standard + provider: views human_name: test_filter_permission module: views id: test_filter_permission diff --git a/core/modules/user/user.info.yml b/core/modules/user/user.info.yml index 45a421a..dbdb520 100644 --- a/core/modules/user/user.info.yml +++ b/core/modules/user/user.info.yml @@ -6,3 +6,5 @@ version: VERSION core: 8.x required: true configure: user.admin_index +dependencies: + - entity diff --git a/core/modules/user/user.install b/core/modules/user/user.install index 086a5a0..e0658c5 100644 --- a/core/modules/user/user.install +++ b/core/modules/user/user.install @@ -9,130 +9,6 @@ * Implements hook_schema(). */ function user_schema() { - // The table name here is plural, despite Drupal table naming standards, - // because "user" is a reserved word in many databases. - $schema['users'] = array( - 'description' => 'Stores user data.', - 'fields' => array( - 'uid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary Key: Unique user ID.', - 'default' => 0, - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'name' => array( - 'type' => 'varchar', - 'length' => 60, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Unique user name.', - ), - 'langcode' => array( - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - 'description' => "The {language}.langcode of the user's profile.", - ), - 'pass' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => "User's password (hashed).", - ), - 'mail' => array( - 'type' => 'varchar', - 'length' => 254, - 'not null' => FALSE, - 'default' => '', - 'description' => "User's e-mail address.", - ), - 'signature' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "User's signature.", - ), - 'signature_format' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - 'description' => 'The filter format ID of the signature.', - ), - 'created' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Timestamp for when user was created.', - ), - 'access' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Timestamp for previous time user accessed the site.', - ), - 'login' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => "Timestamp for user's last login.", - ), - 'status' => array( - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'tiny', - 'description' => 'Whether the user is active(1) or blocked(0).', - ), - 'timezone' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => FALSE, - 'description' => "User's time zone.", - ), - 'preferred_langcode' => array( - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The {language}.langcode that the user prefers for receiving emails and viewing the site.', - ), - 'preferred_admin_langcode' => array( - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The {language}.langcode that the user prefers for viewing administration pages.', - ), - 'init' => array( - 'type' => 'varchar', - 'length' => 254, - 'not null' => FALSE, - 'default' => '', - 'description' => 'E-mail address used for initial account creation.', - ), - ), - 'indexes' => array( - 'access' => array('access'), - 'created' => array('created'), - 'mail' => array('mail'), - ), - 'unique keys' => array( - 'uuid' => array('uuid'), - 'name' => array('name'), - ), - 'primary key' => array('uid'), - ); - $schema['users_data'] = array( 'description' => 'Stores module data as key/value pairs per user.', 'fields' => array( @@ -181,35 +57,6 @@ function user_schema() { ), ); - $schema['users_roles'] = array( - 'description' => 'Maps users to roles.', - 'fields' => array( - 'uid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: {users}.uid for user.', - ), - 'rid' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => TRUE, - 'description' => 'Primary Key: ID for the role.', - ), - ), - 'primary key' => array('uid', 'rid'), - 'indexes' => array( - 'rid' => array('rid'), - ), - 'foreign keys' => array( - 'user' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ), - ); - return $schema; } diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index c3e19aa..87a5674 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -275,7 +275,10 @@ public function calculateDependencies() { // Ensure that the view is dependent on the module that provides the schema // for the base table. $schema = $this->drupalGetSchema($this->base_table); - if ($this->module != $schema['module']) { + // @todo Entity base tables are no longer registered in hook_schema(). Once + // we automate the views data for entity types add the entity type + // type provider as a dependency. See https://drupal.org/node/1740492. + if ($schema && $this->module != $schema['module']) { $this->addDependency('module', $schema['module']); } @@ -283,7 +286,6 @@ public function calculateDependencies() { foreach (Views::getHandlerTypes() as $type) { $handler_types[] = $type['plural']; } - foreach ($this->get('display') as $display) { // Collect all dependencies of all handlers. foreach ($handler_types as $handler_type) { @@ -296,10 +298,10 @@ public function calculateDependencies() { // Add the additional dependencies from the handler configuration. if (!empty($handler['dependencies'])) { $this->addDependencies($handler['dependencies']); - } } } } + } // Collect all dependencies of plugins. foreach (Views::getPluginTypes('plugin') as $plugin_type) { diff --git a/core/modules/views/src/Plugin/views/argument_default/Raw.php b/core/modules/views/src/Plugin/views/argument_default/Raw.php index 5403fb3..e7007fa 100644 --- a/core/modules/views/src/Plugin/views/argument_default/Raw.php +++ b/core/modules/views/src/Plugin/views/argument_default/Raw.php @@ -56,7 +56,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('path.alias_manager') + $container->get('path.alias_manager.cached') ); } @@ -70,13 +70,12 @@ protected function defineOptions() { public function buildOptionsForm(&$form, &$form_state) { parent::buildOptionsForm($form, $form_state); + // Using range(1, 10) will create an array keyed 0-9, which allows arg() to + // properly function since it is also zero-based. $form['index'] = array( '#type' => 'select', '#title' => t('Path component'), '#default_value' => $this->options['index'], - // range(1, 10) returns an array with: - // - keys that count from 0 to match PHP array keys from explode(). - // - values that count from 1 for display to humans. '#options' => range(1, 10), '#description' => t('The numbering starts from 1, e.g. on the page admin/structure/types, the 3rd path component is "types".'), ); diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index b960d5b..c5634e7 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -16,7 +16,6 @@ use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\PluginBase; use Drupal\views\Views; -use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException as DependencyInjectionRuntimeException; /** @@ -2399,9 +2398,8 @@ public function getSpecialBlocks() { * The rendered exposed form as string or NULL otherwise. */ public function viewExposedFormBlocks() { - // Avoid interfering with the admin forms. - $route_name = \Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_NAME); - if (strpos($route_name, 'views_ui.') === 0) { + // avoid interfering with the admin forms. + if (arg(0) == 'admin' && arg(1) == 'structure' && arg(2) == 'views') { return; } $this->view->initHandlers(); diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 175e7f4..5b51afd 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -1425,7 +1425,6 @@ function execute(ViewExecutable $view) { $result = $query->execute(); $result->setFetchMode(\PDO::FETCH_CLASS, 'Drupal\views\ResultRow'); - $view->result = iterator_to_array($result); $view->pager->postExecute($view->result); diff --git a/core/modules/views/src/Tests/Entity/RowEntityRenderersTest.php b/core/modules/views/src/Tests/Entity/RowEntityRenderersTest.php index 869ad88..35c8c61 100644 --- a/core/modules/views/src/Tests/Entity/RowEntityRenderersTest.php +++ b/core/modules/views/src/Tests/Entity/RowEntityRenderersTest.php @@ -56,8 +56,9 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->installSchema('node', array('node', 'node_revision', 'node_field_data', 'node_field_revision', 'node_access')); - $this->installSchema('user', array('users')); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('node', array('node_access')); $this->installConfig(array('node', 'language')); // The node.view route must exist when nodes are rendered. diff --git a/core/modules/views/src/Tests/Handler/AreaTextTest.php b/core/modules/views/src/Tests/Handler/AreaTextTest.php index 2ac78f8..3ab29d7 100644 --- a/core/modules/views/src/Tests/Handler/AreaTextTest.php +++ b/core/modules/views/src/Tests/Handler/AreaTextTest.php @@ -38,7 +38,7 @@ protected function setUp() { parent::setUp(); $this->installConfig(array('system', 'filter')); - $this->installSchema('user', array('users')); + $this->installEntitySchema('user'); } public function testAreaText() { diff --git a/core/modules/views/src/Tests/Handler/HandlerAliasTest.php b/core/modules/views/src/Tests/Handler/HandlerAliasTest.php index caec4c7..ac9b920 100644 --- a/core/modules/views/src/Tests/Handler/HandlerAliasTest.php +++ b/core/modules/views/src/Tests/Handler/HandlerAliasTest.php @@ -35,7 +35,7 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->installSchema('user', 'users'); + $this->installEntitySchema('user'); } /** diff --git a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php index fae3515..5542619 100644 --- a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php +++ b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php @@ -26,7 +26,7 @@ * Overrides \Drupal\views\Tests\ViewUnitTestBase::setUpFixtures(). */ protected function setUpFixtures() { - $this->installSchema('user', array('users', 'users_roles')); + $this->installEntitySchema('user'); $this->installConfig(array('user')); parent::setUpFixtures(); diff --git a/core/modules/views/src/Tests/Plugin/RowEntityTest.php b/core/modules/views/src/Tests/Plugin/RowEntityTest.php index 0b406f5..8cecbd1 100644 --- a/core/modules/views/src/Tests/Plugin/RowEntityTest.php +++ b/core/modules/views/src/Tests/Plugin/RowEntityTest.php @@ -53,7 +53,7 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy')); + $this->installEntitySchema('taxonomy_term'); $this->installConfig(array('taxonomy')); \Drupal::service('router.builder')->rebuild(); } diff --git a/core/modules/views/src/Tests/QueryGroupByTest.php b/core/modules/views/src/Tests/QueryGroupByTest.php index 7b7d0b7..ff0c225 100644 --- a/core/modules/views/src/Tests/QueryGroupByTest.php +++ b/core/modules/views/src/Tests/QueryGroupByTest.php @@ -49,7 +49,7 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->installSchema('entity_test', array('entity_test')); + $this->installEntitySchema('entity_test'); $this->storage = $this->container->get('entity.manager')->getStorage('entity_test'); } diff --git a/core/modules/views/src/Tests/ViewExecutableTest.php b/core/modules/views/src/Tests/ViewExecutableTest.php index 74787f1..183e1af 100644 --- a/core/modules/views/src/Tests/ViewExecutableTest.php +++ b/core/modules/views/src/Tests/ViewExecutableTest.php @@ -84,10 +84,12 @@ public static function getInfo() { } protected function setUpFixtures() { - $this->installSchema('user', array('users')); - $this->installSchema('node', array('node', 'node_field_data')); - $this->installSchema('comment', array('comment', 'comment_entity_statistics')); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installSchema('comment', array('comment_entity_statistics')); $this->installConfig(array('field')); + entity_create('node_type', array( 'type' => 'page', 'name' => 'Page', diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module index ba5e342..a39fa71 100644 --- a/core/modules/views_ui/views_ui.module +++ b/core/modules/views_ui/views_ui.module @@ -320,7 +320,7 @@ function views_ui_views_analyze(ViewExecutable $view) { continue; } if ($display->hasPath() && $path = $display->getOption('path')) { - $normal_path = \Drupal::service('path.alias_manager')->getPathByAlias($path); + $normal_path = \Drupal::service('path.alias_manager.cached')->getPathByAlias($path); if ($path != $normal_path) { $ret[] = Analyzer::formatMessage(t('You have configured display %display with a path which is an path alias as well. This might lead to unwanted effects so better use an internal path.', array('%display' => $display->display['display_title'])), 'warning'); } diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php index 6e60480..fcc85df 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php @@ -23,6 +23,34 @@ class ContentEntityDatabaseStorageTest extends UnitTestCase { /** + * The content entity database storage used in this test. + * + * @var \Drupal\Core\Entity\ContentEntityDatabaseStorage|\PHPUnit_Framework_MockObject_MockObject + */ + protected $entityStorage; + + /** + * The mocked entity type used in this test. + * + * @var \Drupal\Core\Entity\ContentEntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $entityType; + + /** + * An array of field definitions used for this test, keyed by field name. + * + * @var \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[] + */ + protected $fieldDefinitions = array(); + + /** + * The mocked entity manager used in this test. + * + * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $entityManager; + + /** * {@inheritdoc} */ public static function getInfo() { @@ -34,27 +62,875 @@ public static function getInfo() { } /** + * {@inheritdoc} + */ + public function setUp() { + $this->entityType = $this->getMock('Drupal\Core\Entity\ContentEntityTypeInterface'); + $this->entityType->expects($this->any()) + ->method('id') + ->will($this->returnValue('entity_test')); + + $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + } + + /** + * Tests ContentEntityDatabaseStorage::getBaseTable(). + * + * @param string $base_table + * The base table to be returned by the mocked entity type. + * @param string $expected + * The expected return value of + * ContentEntityDatabaseStorage::getBaseTable(). + * + * @covers ::__construct() + * @covers ::getBaseTable() + * + * @dataProvider providerTestGetBaseTable + */ + public function testGetBaseTable($base_table, $expected) { + $this->entityType->expects($this->once()) + ->method('getBaseTable') + ->will($this->returnValue('entity_test')); + + $this->setUpEntityStorage(); + + $this->assertSame($expected, $this->entityStorage->getBaseTable()); + } + + /** + * Provides test data for testGetBaseTable(). + * + * @return array[] + * An nested array where each inner array has the base table to be returned + * by the mocked entity type as the first value and the expected return + * value of ContentEntityDatabaseStorage::getBaseTable() as the second + * value. + */ + public function providerTestGetBaseTable() { + return array( + // Test that the entity type's base table is used, if provided. + array('entity_test', 'entity_test'), + // Test that the storage falls back to the entity type ID. + array(NULL, 'entity_test'), + ); + } + + /** + * Tests ContentEntityDatabaseStorage::getRevisionTable(). + * + * @param string $revision_table + * The revision table to be returned by the mocked entity type. + * @param string $expected + * The expected return value of + * ContentEntityDatabaseStorage::getRevisionTable(). + * + * @cover ::__construct() + * @covers ::getRevisionTable() + * + * @dataProvider providerTestGetRevisionTable + */ + public function testGetRevisionTable($revision_table, $expected) { + $this->entityType->expects($this->once()) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->once()) + ->method('getRevisionTable') + ->will($this->returnValue($revision_table)); + + $this->setUpEntityStorage(); + + $this->assertSame($expected, $this->entityStorage->getRevisionTable()); + } + + /** + * Provides test data for testGetRevisionTable(). + * + * @return array[] + * An nested array where each inner array has the revision table to be + * returned by the mocked entity type as the first value and the expected + * return value of ContentEntityDatabaseStorage::getRevisionTable() as the + * second value. + */ + public function providerTestGetRevisionTable() { + return array( + // Test that the entity type's revision table is used, if provided. + array('entity_test_revision', 'entity_test_revision'), + // Test that the storage falls back to the entity type ID with a + // '_revision' suffix. + array(NULL, 'entity_test_revision'), + ); + } + + /** + * Tests ContentEntityDatabaseStorage::getDataTable(). + * + * @cover ::__construct() + * @covers ::getDataTable() + */ + public function testGetDataTable() { + $this->entityType->expects($this->once()) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(2)) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + + $this->setUpEntityStorage(); + + $this->assertSame('entity_test_field_data', $this->entityStorage->getDataTable()); + } + + /** + * Tests ContentEntityDatabaseStorage::getRevisionDataTable(). + * + * @param string $revision_data_table + * The revision data table to be returned by the mocked entity type. + * @param string $expected + * The expected return value of + * ContentEntityDatabaseStorage::getRevisionDataTable(). + * + * @cover ::__construct() + * @covers ::getRevisionDataTable() + * + * @dataProvider providerTestGetRevisionDataTable + */ + public function testGetRevisionDataTable($revision_data_table, $expected) { + $this->entityType->expects($this->once()) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->once()) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(2)) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->entityType->expects($this->once()) + ->method('getRevisionDataTable') + ->will($this->returnValue($revision_data_table)); + + $this->setUpEntityStorage(); + + $actual = $this->entityStorage->getRevisionDataTable(); + $this->assertSame($expected, $actual); + } + + /** + * Provides test data for testGetRevisionDataTable(). + * + * @return array[] + * An nested array where each inner array has the revision data table to be + * returned by the mocked entity type as the first value and the expected + * return value of ContentEntityDatabaseStorage::getRevisionDataTable() as + * the second value. + */ + public function providerTestGetRevisionDataTable() { + return array( + // Test that the entity type's revision data table is used, if provided. + array('entity_test_field_revision', 'entity_test_field_revision'), + // Test that the storage falls back to the entity type ID with a + // '_field_revision' suffix. + array(NULL, 'entity_test_field_revision'), + ); + } + + /** + * Tests ContentEntityDatabaseStorage::getSchema(). + * + * @covers ::__construct() + * @covers ::getSchema() + * @covers ::schemaHandler() + * @covers ::getTableMapping() + */ + public function testGetSchema() { + $columns = array( + 'value' => array( + 'type' => 'int', + ), + ); + + $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + $this->fieldDefinitions['id']->expects($this->once()) + ->method('getColumns') + ->will($this->returnValue($columns)); + $this->fieldDefinitions['id']->expects($this->once()) + ->method('getSchema') + ->will($this->returnValue(array('columns' => $columns))); + + $this->entityType->expects($this->once()) + ->method('getKeys') + ->will($this->returnValue(array('id' => 'id'))); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + // EntityStorageBase::__construct() + array('id', 'id'), + // ContentEntityStorageBase::__construct() + array('uuid', NULL), + array('bundle', NULL), + // ContentEntitySchemaHandler::initializeBaseTable() + array('id' => 'id'), + // ContentEntitySchemaHandler::processBaseTable() + array('id' => 'id'), + ))); + + $this->entityManager->expects($this->once()) + ->method('getFieldStorageDefinitions') + ->with($this->entityType->id()) + ->will($this->returnValue($this->fieldDefinitions)); + + $this->setUpEntityStorage(); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'description' => NULL, + 'not null' => TRUE, + ), + ), + 'primary key' => array('id'), + 'indexes' => array(), + 'foreign keys' => array(), + ), + ); + $this->assertEquals($expected, $this->entityStorage->getSchema()); + + // Test that repeated calls do not result in repeatedly instantiating + // ContentEntitySchemaHandler as getFieldStorageDefinitions() is only + // expected to be called once. + $this->assertEquals($expected, $this->entityStorage->getSchema()); + } + + /** + * Tests getTableMapping() with an empty entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + */ + public function testGetTableMappingEmpty() { + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + $this->assertSame(array('entity_test'), $mapping->getTableNames()); + $this->assertSame(array(), $mapping->getFieldNames('entity_test')); + $this->assertSame(array(), $mapping->getExtraColumns('entity_test')); + } + + /** + * Tests getTableMapping() with a simple entity type. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingSimple(array $entity_keys) { + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $this->assertEquals(array('entity_test'), $mapping->getTableNames()); + + $expected = array_values(array_filter($entity_keys)); + $this->assertEquals($expected, $mapping->getFieldNames('entity_test')); + + $this->assertEquals(array(), $mapping->getExtraColumns('entity_test')); + } + + /** + * Tests getTableMapping() with a simple entity type with some base fields. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingSimpleWithFields(array $entity_keys) { + $base_field_names = array('title', 'description', 'owner'); + $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); + + $definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + $this->fieldDefinitions = array_fill_keys($field_names, $definition); + + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + $this->assertEquals(array('entity_test'), $mapping->getTableNames()); + $this->assertEquals($field_names, $mapping->getFieldNames('entity_test')); + $this->assertEquals(array(), $mapping->getExtraColumns('entity_test')); + } + + /** + * Provides test data for testGetTableMappingSimple(). + * + * @return array[] + * A nested array, where each inner array has a single value being a map of + * entity keys to use for the mocked entity type. + */ + public function providerTestGetTableMappingSimple() { + return array( + array(array( + 'id' => 'test_id', + 'bundle' => NULL, + 'uuid' => NULL, + )), + array(array( + 'id' => 'test_id', + 'bundle' => 'test_bundle', + 'uuid' => NULL, + )), + array(array( + 'id' => 'test_id', + 'bundle' => NULL, + 'uuid' => 'test_uuid', + )), + array(array( + 'id' => 'test_id', + 'bundle' => 'test_bundle', + 'uuid' => 'test_uuid', + )), + ); + } + + /** + * Tests getTableMapping() with a revisionable, non-translatable entity type. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingRevisionable(array $entity_keys) { + // This allows to re-use the data provider. + $entity_keys = array( + 'id' => $entity_keys['id'], + 'revision' => 'test_revision', + 'bundle' => $entity_keys['bundle'], + 'uuid' => $entity_keys['uuid'], + ); + + $this->entityType->expects($this->exactly(2)) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + array('revision', $entity_keys['revision']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $expected = array('entity_test', 'entity_test_revision'); + $this->assertEquals($expected, $mapping->getTableNames()); + + $expected = array_values(array_filter($entity_keys)); + $this->assertEquals($expected, $mapping->getFieldNames('entity_test')); + $expected = array($entity_keys['id'], $entity_keys['revision']); + $this->assertEquals($expected, $mapping->getFieldNames('entity_test_revision')); + + $this->assertEquals(array(), $mapping->getExtraColumns('entity_test')); + $this->assertEquals(array(), $mapping->getExtraColumns('entity_test_revision')); + } + + /** + * Tests getTableMapping() with a revisionable entity type with fields. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingRevisionableWithFields(array $entity_keys) { + // This allows to re-use the data provider. + $entity_keys = array( + 'id' => $entity_keys['id'], + 'revision' => 'test_revision', + 'bundle' => $entity_keys['bundle'], + 'uuid' => $entity_keys['uuid'], + ); + + // PHPUnit does not allow for multiple data providers. + $test_cases = array( + array(), + array('revision_timestamp'), + array('revision_uid'), + array('log'), + array('revision_timestamp', 'revision_uid'), + array('revision_timestamp', 'log'), + array('revision_uid', 'log'), + array('revision_timestamp', 'revision_uid', 'log'), + ); + foreach ($test_cases as $revision_metadata_field_names) { + $this->setUp(); + + $base_field_names = array('title'); + $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); + + $definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + $this->fieldDefinitions = array_fill_keys($field_names, $definition); + + $revisionable_field_names = array('description', 'owner'); + $definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + // isRevisionable() is only called once, but we re-use the same definition + // for all revisionable fields. + $definition->expects($this->any()) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $field_names = array_merge( + $field_names, + $revisionable_field_names + ); + $this->fieldDefinitions += array_fill_keys( + array_merge($revisionable_field_names, $revision_metadata_field_names), + $definition + ); + + $this->entityType->expects($this->exactly(2)) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + array('revision', $entity_keys['revision']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $expected = array('entity_test', 'entity_test_revision'); + $this->assertEquals($expected, $mapping->getTableNames()); + + $this->assertEquals($field_names, $mapping->getFieldNames('entity_test')); + $expected = array_merge( + array($entity_keys['id'], $entity_keys['revision']), + $revisionable_field_names, + $revision_metadata_field_names + ); + $this->assertEquals($expected, $mapping->getFieldNames('entity_test_revision')); + + $this->assertEquals(array(), $mapping->getExtraColumns('entity_test')); + $this->assertEquals(array(), $mapping->getExtraColumns('entity_test_revision')); + } + } + + /** + * Tests getTableMapping() with a non-revisionable, translatable entity type. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingTranslatable(array $entity_keys) { + // This allows to re-use the data provider. + $entity_keys['langcode'] = 'langcode'; + + $this->entityType->expects($this->exactly(2)) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(3)) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $expected = array('entity_test', 'entity_test_field_data'); + $this->assertEquals($expected, $mapping->getTableNames()); + + $expected = array_values(array_filter($entity_keys)); + $actual = $mapping->getFieldNames('entity_test'); + $this->assertEquals($expected, $actual); + // The UUID is not stored on the data table. + $expected = array_values(array_filter(array( + $entity_keys['id'], + $entity_keys['bundle'], + $entity_keys['langcode'], + ))); + $actual = $mapping->getFieldNames('entity_test_field_data'); + $this->assertEquals($expected, $actual); + + $expected = array(); + $actual = $mapping->getExtraColumns('entity_test'); + $this->assertEquals($expected, $actual); + $expected = array('default_langcode'); + $actual = $mapping->getExtraColumns('entity_test_field_data'); + $this->assertEquals($expected, $actual); + } + + /** + * Tests getTableMapping() with a translatable entity type with fields. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingTranslatableWithFields(array $entity_keys) { + // This allows to re-use the data provider. + $entity_keys['langcode'] = 'langcode'; + + $base_field_names = array('title', 'description', 'owner'); + $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); + + $definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + $this->fieldDefinitions = array_fill_keys($field_names, $definition); + + $this->entityType->expects($this->exactly(2)) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(3)) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $expected = array('entity_test', 'entity_test_field_data'); + $this->assertEquals($expected, $mapping->getTableNames()); + + $expected = array_values(array_filter($entity_keys)); + $actual = $mapping->getFieldNames('entity_test'); + $this->assertEquals($expected, $actual); + // The UUID is not stored on the data table. + $expected = array_merge(array_filter(array( + $entity_keys['id'], + $entity_keys['bundle'], + $entity_keys['langcode'], + )), $base_field_names); + $actual = $mapping->getFieldNames('entity_test_field_data'); + $this->assertEquals($expected, $actual); + + $expected = array(); + $actual = $mapping->getExtraColumns('entity_test'); + $this->assertEquals($expected, $actual); + $expected = array('default_langcode'); + $actual = $mapping->getExtraColumns('entity_test_field_data'); + $this->assertEquals($expected, $actual); + } + + /** + * Tests getTableMapping() with a revisionable, translatable entity type. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingRevisionableTranslatable(array $entity_keys) { + // This allows to re-use the data provider. + $entity_keys = array( + 'id' => $entity_keys['id'], + 'revision' => 'test_revision', + 'bundle' => $entity_keys['bundle'], + 'uuid' => $entity_keys['uuid'], + 'langcode' => 'langcode', + ); + + $this->entityType->expects($this->exactly(2)) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(2)) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(3)) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + array('revision', $entity_keys['revision']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $expected = array( + 'entity_test', + 'entity_test_field_data', + 'entity_test_revision', + 'entity_test_field_revision', + ); + $this->assertEquals($expected, $mapping->getTableNames()); + + // The language code is not stored on the base table, but on the revision + // table. + $expected = array_values(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['bundle'], + $entity_keys['uuid'], + ))); + $actual = $mapping->getFieldNames('entity_test'); + $this->assertEquals($expected, $actual); + // The revision table on the other hand does not store the bundle and the + // UUID. + $expected = array_values(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['langcode'], + ))); + $actual = $mapping->getFieldNames('entity_test_revision'); + $this->assertEquals($expected, $actual); + // The UUID is not stored on the data table. + $expected = array_values(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['bundle'], + $entity_keys['langcode'], + ))); + $actual = $mapping->getFieldNames('entity_test_field_data'); + $this->assertEquals($expected, $actual); + // The data revision also does not store the bundle. + $expected = array_values(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['langcode'], + ))); + $actual = $mapping->getFieldNames('entity_test_field_revision'); + $this->assertEquals($expected, $actual); + + $expected = array(); + $actual = $mapping->getExtraColumns('entity_test'); + $this->assertEquals($expected, $actual); + $actual = $mapping->getExtraColumns('entity_test_revision'); + $this->assertEquals($expected, $actual); + $expected = array('default_langcode'); + $actual = $mapping->getExtraColumns('entity_test_field_data'); + $this->assertEquals($expected, $actual); + $actual = $mapping->getExtraColumns('entity_test_field_revision'); + $this->assertEquals($expected, $actual); + } + + /** + * Tests getTableMapping() with a complex entity type with fields. + * + * @param string[] $entity_keys + * A map of entity keys to use for the mocked entity type. + * + * @covers ::__construct() + * @covers ::getTableMapping() + * + * @dataProvider providerTestGetTableMappingSimple() + */ + public function testGetTableMappingRevisionableTranslatableWithFields(array $entity_keys) { + // This allows to re-use the data provider. + $entity_keys = array( + 'id' => $entity_keys['id'], + 'revision' => 'test_revision', + 'bundle' => $entity_keys['bundle'], + 'uuid' => $entity_keys['uuid'], + 'langcode' => 'langcode', + ); + + // PHPUnit does not allow for multiple data providers. + $test_cases = array( + array(), + array('revision_timestamp'), + array('revision_uid'), + array('log'), + array('revision_timestamp', 'revision_uid'), + array('revision_timestamp', 'log'), + array('revision_uid', 'log'), + array('revision_timestamp', 'revision_uid', 'log'), + ); + foreach ($test_cases as $revision_metadata_field_names) { + $this->setUp(); + + $base_field_names = array('title'); + $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); + + $definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + $this->fieldDefinitions = array_fill_keys($field_names, $definition); + + $revisionable_field_names = array('description', 'owner'); + $definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface'); + // isRevisionable() is only called once, but we re-use the same definition + // for all revisionable fields. + $definition->expects($this->any()) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->fieldDefinitions += array_fill_keys( + array_merge($revisionable_field_names, $revision_metadata_field_names), + $definition + ); + + $this->entityType->expects($this->exactly(2)) + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(2)) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->exactly(3)) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', $entity_keys['id']), + array('uuid', $entity_keys['uuid']), + array('bundle', $entity_keys['bundle']), + array('revision', $entity_keys['revision']), + ))); + + $this->setUpEntityStorage(); + + $mapping = $this->entityStorage->getTableMapping(); + + $expected = array( + 'entity_test', + 'entity_test_field_data', + 'entity_test_revision', + 'entity_test_field_revision', + ); + $this->assertEquals($expected, $mapping->getTableNames()); + + $expected = array( + 'entity_test', + 'entity_test_field_data', + 'entity_test_revision', + 'entity_test_field_revision', + ); + $this->assertEquals($expected, $mapping->getTableNames()); + + // The language code is not stored on the base table, but on the revision + // table. + $expected = array_values(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['bundle'], + $entity_keys['uuid'], + ))); + $actual = $mapping->getFieldNames('entity_test'); + $this->assertEquals($expected, $actual); + // The revision table on the other hand does not store the bundle and the + // UUID. + $expected = array_merge(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['langcode'], + )), $revision_metadata_field_names); + $actual = $mapping->getFieldNames('entity_test_revision'); + $this->assertEquals($expected, $actual); + // The UUID is not stored on the data table. + $expected = array_merge(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['bundle'], + $entity_keys['langcode'], + )), $base_field_names, $revisionable_field_names); + $actual = $mapping->getFieldNames('entity_test_field_data'); + $this->assertEquals($expected, $actual); + // The data revision also does not store the bundle. + $expected = array_merge(array_filter(array( + $entity_keys['id'], + $entity_keys['revision'], + $entity_keys['langcode'], + )), $revisionable_field_names); + $actual = $mapping->getFieldNames('entity_test_field_revision'); + $this->assertEquals($expected, $actual); + + $expected = array(); + $actual = $mapping->getExtraColumns('entity_test'); + $this->assertEquals($expected, $actual); + $actual = $mapping->getExtraColumns('entity_test_revision'); + $this->assertEquals($expected, $actual); + $expected = array('default_langcode'); + $actual = $mapping->getExtraColumns('entity_test_field_data'); + $this->assertEquals($expected, $actual); + $actual = $mapping->getExtraColumns('entity_test_field_revision'); + $this->assertEquals($expected, $actual); + } + } + + /** * Tests field SQL schema generation for an entity with a string identifier. * * @covers ::_fieldSqlSchema() */ public function testFieldSqlSchemaForEntityWithStringIdentifier() { $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); - $entity_manager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); $container = new ContainerBuilder(); $container->set('plugin.manager.field.field_type', $field_type_manager); - $container->set('entity.manager', $entity_manager); + $container->set('entity.manager', $this->entityManager); \Drupal::setContainer($container); - $definition = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); - $definition->expects($this->any()) + $this->entityType->expects($this->any()) ->method('getKey') ->will($this->returnValueMap(array( array('id', 'id'), array('revision', 'revision'), ))); - $definition->expects($this->once()) + $this->entityType->expects($this->once()) ->method('hasKey') ->with('revision') ->will($this->returnValue(TRUE)); @@ -66,29 +942,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getDefaultInstanceSettings') ->will($this->returnValue(array())); - $fields['id'] = FieldDefinition::create('string') + $this->fieldDefinitions['id'] = FieldDefinition::create('string') ->setName('id'); - $fields['revision'] = FieldDefinition::create('string') + $this->fieldDefinitions['revision'] = FieldDefinition::create('string') ->setName('revision'); - $entity_manager->expects($this->any()) + $this->entityManager->expects($this->any()) ->method('getDefinition') ->with('test_entity') - ->will($this->returnValue($definition)); - $entity_manager->expects($this->any()) + ->will($this->returnValue($this->entityType)); + $this->entityManager->expects($this->once()) ->method('getBaseFieldDefinitions') - ->with('test_entity') - ->will($this->returnValue($fields)); + ->will($this->returnValue($this->fieldDefinitions)); - // Define a field definition for a test_field field. - $field = $this->getMock('\Drupal\field\FieldConfigInterface'); + // Define a field definition for a test_field fuuidield. + $field = $this->getMock('\Drupal\Core\Field\FieldStorageDefinitionInterface'); $field->deleted = FALSE; - $field->entity_type = 'test_entity'; - $field->name = 'test_field'; $field->expects($this->any()) ->method('getName') - ->will($this->returnValue('test')); + ->will($this->returnValue('test_field')); + + $field->expects($this->any()) + ->method('getTargetEntityTypeId') + ->will($this->returnValue('test_entity')); $field_schema = array( 'columns' => array( @@ -118,43 +995,42 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { public function testCreate() { $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); - // @todo Add field definitions to test default values of fields. - $entity_manager->expects($this->atLeastOnce()) - ->method('getFieldDefinitions') - ->will($this->returnValue(array())); $container = new ContainerBuilder(); $container->set('language_manager', $language_manager); - $container->set('entity.manager', $entity_manager); + $container->set('entity.manager', $this->entityManager); $container->set('module_handler', $module_handler); \Drupal::setContainer($container); - $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\ContentEntityBase', array(), '', FALSE, TRUE, TRUE, array('id')); - $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); - $entity_type->expects($this->atLeastOnce()) - ->method('id') - ->will($this->returnValue('test_entity_type')); - $entity_type->expects($this->atLeastOnce()) + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->setMethods(array('id')) + ->getMockForAbstractClass(); + + $this->entityType->expects($this->atLeastOnce()) ->method('getClass') ->will($this->returnValue(get_class($entity))); - $entity_type->expects($this->atLeastOnce()) + $this->entityType->expects($this->atLeastOnce()) ->method('getKeys') ->will($this->returnValue(array('id' => 'id'))); - $entity_type->expects($this->atLeastOnce()) + + // ContentEntityStorageBase iterates over the entity which calls this method + // internally in ContentEntityBase::getProperties(). + $this->entityManager->expects($this->once()) + ->method('getFieldDefinitions') + ->will($this->returnValue(array())); + + $this->entityType->expects($this->atLeastOnce()) ->method('isRevisionable') ->will($this->returnValue(FALSE)); - $entity_manager->expects($this->atLeastOnce()) + $this->entityManager->expects($this->atLeastOnce()) ->method('getDefinition') - ->with('test_entity_type') - ->will($this->returnValue($entity_type)); + ->with($this->entityType->id()) + ->will($this->returnValue($this->entityType)); - $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') - ->disableOriginalConstructor() - ->getMock(); - $entity_storage = new ContentEntityDatabaseStorage($entity_type, $connection, $entity_manager); + $this->setUpEntityStorage(); - $entity = $entity_storage->create(); + $entity = $this->entityStorage->create(); $entity->expects($this->atLeastOnce()) ->method('id') ->will($this->returnValue('foo')); @@ -164,4 +1040,19 @@ public function testCreate() { $this->assertTrue($entity->isNew()); } + /** + * Sets up the content entity database storage. + */ + protected function setUpEntityStorage() { + $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $this->entityManager->expects($this->once()) + ->method('getBaseFieldDefinitions') + ->will($this->returnValue($this->fieldDefinitions)); + + $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $connection, $this->entityManager); + } + } diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php new file mode 100644 index 0000000..b39fbcb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php @@ -0,0 +1,770 @@ + 'Content entity schema handler', + 'description' => 'Tests the schema generation for content entities.', + 'group' => 'Entity', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') + ->disableOriginalConstructor() + ->getMock(); + + $this->storage->expects($this->any()) + ->method('getBaseTable') + ->will($this->returnValue('entity_test')); + + // Add an ID field. This also acts as a test for a simple, single-column + // field. + $this->setUpStorageDefinition('id', array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + ), + ), + )); + } + + /** + * Tests the schema for non-revisionable, non-translatable entities. + * + * @param bool $uuid_key + * Whether or not the tested entity type should have a UUID key. + * + * @covers ::__construct() + * @covers ::getSchema() + * @covers ::getTables() + * @covers ::initializeBaseTable() + * @covers ::getEntityIndexName() + * @covers ::addFieldSchema() + * @covers ::getFieldIndexes() + * @covers ::getFieldUniqueKeys() + * @covers ::getFieldForeignKeys() + * @covers ::getFieldSchemaData() + * @covers ::addDefaultLangcodeSchema() + * @covers ::processBaseTable() + * @covers ::processIdentifierSchema() + * + * @dataProvider providerTestGetSchemaLayoutBase + */ + public function testGetSchemaBase($uuid_key) { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + 'uuid' => $uuid_key ? 'uuid' : NULL, + ), + )); + + // Add a field with a 'length' constraint. + $this->setUpStorageDefinition('name', array( + 'columns' => array( + 'value' => array( + 'type' => 'varchar', + 'length' => 255, + ), + ), + )); + if ($uuid_key) { + $this->setUpStorageDefinition('uuid', array( + 'columns' => array( + 'value' => array( + 'type' => 'varchar', + 'length' => 128, + ), + ), + )); + } + // Add a multi-column field. + $this->setUpStorageDefinition('description', array( + 'columns' => array( + 'value' => array( + 'type' => 'text', + 'description' => 'The text value', + ), + 'format' => array( + 'type' => 'varchar', + 'description' => 'The text description', + ), + ), + )); + // Add a field with an index. + $this->setUpStorageDefinition('owner', array( + 'columns' => array( + 'target_id' => array( + 'description' => 'The ID of the target entity.', + 'type' => 'int', + ), + ), + 'indexes' => array( + 'target_id' => array('target_id'), + ), + )); + // Add a field with an index, specified as column name and length. + $this->setUpStorageDefinition('translator', array( + 'columns' => array( + 'target_id' => array( + 'description' => 'The ID of the target entity.', + 'type' => 'int', + ), + ), + 'indexes' => array( + 'target_id' => array(array('target_id', 10)), + ), + )); + // Add a field with a multi-column index. + $this->setUpStorageDefinition('location', array( + 'columns' => array( + 'country' => array( + 'type' => 'varchar', + ), + 'state' => array( + 'type' => 'varchar', + ), + 'city' => array( + 'type' => 'varchar', + ) + ), + 'indexes' => array( + 'country_state_city' => array('country', 'state', array('city', 10)), + ), + )); + // Add a field with a foreign key. + $this->setUpStorageDefinition('editor', array( + 'columns' => array( + 'target_id' => array( + 'type' => 'int', + ), + ), + 'foreign keys' => array( + 'user_id' => array( + 'table' => 'users', + 'columns' => array('target_id' => 'uid'), + ), + ), + )); + // Add a multi-column field with a foreign key. + $this->setUpStorageDefinition('editor_revision', array( + 'columns' => array( + 'target_id' => array( + 'type' => 'int', + ), + 'target_revision_id' => array( + 'type' => 'int', + ), + ), + 'foreign keys' => array( + 'user_id' => array( + 'table' => 'users', + 'columns' => array('target_id' => 'uid'), + ), + ), + )); + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setExtraColumns('entity_test', array('default_langcode')); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The name field.', + 'type' => 'varchar', + 'length' => 255, + ), + 'description__value' => array( + 'description' => 'The description field.', + 'type' => 'text', + ), + 'description__format' => array( + 'description' => 'The description field.', + 'type' => 'varchar', + ), + 'owner' => array( + 'description' => 'The owner field.', + 'type' => 'int', + ), + 'translator' => array( + 'description' => 'The translator field.', + 'type' => 'int', + ), + 'location__country' => array( + 'description' => 'The location field.', + 'type' => 'varchar', + ), + 'location__state' => array( + 'description' => 'The location field.', + 'type' => 'varchar', + ), + 'location__city' => array( + 'description' => 'The location field.', + 'type' => 'varchar', + ), + 'editor' => array( + 'description' => 'The editor field.', + 'type' => 'int', + ), + 'editor_revision__target_id' => array( + 'description' => 'The editor_revision field.', + 'type' => 'int', + ), + 'editor_revision__target_revision_id' => array( + 'description' => 'The editor_revision field.', + 'type' => 'int', + ), + 'default_langcode' => array( + 'description' => 'Boolean indicating whether field values are in the default entity language.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'entity_test_field__owner__target_id' => array('owner'), + 'entity_test_field__translator__target_id' => array( + array('translator', 10), + ), + 'entity_test_field__location__country_state_city' => array( + 'location__country', + 'location__state', + array('location__city', 10), + ), + ), + 'foreign keys' => array( + 'entity_test_field__editor__user_id' => array( + 'table' => 'users', + 'columns' => array('editor' => 'uid'), + ), + 'entity_test_field__editor_revision__user_id' => array( + 'table' => 'users', + 'columns' => array('editor_revision__target_id' => 'uid'), + ), + ), + ), + ); + if ($uuid_key) { + $expected['entity_test']['fields']['uuid'] = array( + 'type' => 'varchar', + 'length' => 128, + 'description' => 'The uuid field.', + 'not null' => TRUE, + ); + $expected['entity_test']['unique keys']['entity_test__uuid'] = array('uuid'); + } + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); + } + + /** + * Provides data for testGetSchemaLayoutBase(). + * + * @return array + * Returns a nested array where each inner array returns a boolean, + * indicating whether or not the tested entity type should include a UUID + * key. + */ + public function providerTestGetSchemaLayoutBase() { + return array( + array(FALSE), + array(TRUE), + ); + } + + /** + * Tests the schema for revisionable, non-translatable entities. + * + * @covers ::__construct() + * @covers ::getSchema() + * @covers ::getTables() + * @covers ::initializeBaseTable() + * @covers ::initializeRevisionTable() + * @covers ::getEntityIndexName() + * @covers ::processRevisionTable() + * @covers ::processIdentifierSchema() + */ + public function testGetSchemaRevisionable() { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + 'revision' => 'revision_id', + ), + )); + + $this->storage->expects($this->exactly(2)) + ->method('getRevisionTable') + ->will($this->returnValue('entity_test_revision')); + + $this->setUpStorageDefinition('revision_id', array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + ), + ), + )); + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + ) + ), + 'primary key' => array('id'), + 'indexes' => array(), + 'foreign keys' => array( + 'entity_test__revision' => array( + 'table' => 'entity_test_revision', + 'columns' => array('revision_id' => 'revision_id'), + ) + ), + 'unique keys' => array( + 'entity_test__revision_id' => array('revision_id'), + ), + ), + 'entity_test_revision' => array( + 'description' => 'The revision table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'serial', + ), + ), + 'primary key' => array('revision_id'), + 'indexes' => array( + 'entity_test__id' => array('id'), + ), + 'foreign keys' => array( + 'entity_test__revisioned' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + ), + ), + ); + + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); + } + + /** + * Tests the schema for non-revisionable, translatable entities. + * + * @covers ::__construct() + * @covers ::getSchema() + * @covers ::getTables() + * @covers ::initializeDataTable() + * @covers ::getEntityIndexName() + * @covers ::processDataTable() + */ + public function testGetSchemaTranslatable() { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + ), + )); + + $this->storage->expects($this->once()) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + + $this->setUpStorageDefinition('langcode', array( + 'columns' => array( + 'value' => array( + 'type' => 'varchar', + ), + ), + )); + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ) + ), + 'primary key' => array('id'), + 'indexes' => array(), + 'foreign keys' => array(), + ), + 'entity_test_field_data' => array( + 'description' => 'The data table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('id', 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + 'entity_test' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + ), + ), + ); + + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); + } + + /** + * Tests the schema for revisionable, translatable entities. + * + * @covers ::__construct() + * @covers ::getSchema() + * @covers ::getTables() + * @covers ::initializeDataTable() + * @covers ::getEntityIndexName() + * @covers ::initializeRevisionDataTable() + * @covers ::processRevisionDataTable() + */ + public function testGetSchemaRevisionableTranslatable() { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + 'revision' => 'revision_id', + ), + )); + + $this->storage->expects($this->exactly(3)) + ->method('getRevisionTable') + ->will($this->returnValue('entity_test_revision')); + $this->storage->expects($this->once()) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->storage->expects($this->once()) + ->method('getRevisionDataTable') + ->will($this->returnValue('entity_test_revision_field_data')); + + $this->setUpStorageDefinition('revision_id', array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + ), + ), + )); + $this->setUpStorageDefinition('langcode', array( + 'columns' => array( + 'value' => array( + 'type' => 'varchar', + ), + ), + )); + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ) + ), + 'primary key' => array('id'), + 'indexes' => array(), + 'unique keys' => array( + 'entity_test__revision_id' => array('revision_id'), + ), + 'foreign keys' => array( + 'entity_test__revision' => array( + 'table' => 'entity_test_revision', + 'columns' => array('revision_id' => 'revision_id'), + ), + ), + ), + 'entity_test_revision' => array( + 'description' => 'The revision table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'serial', + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('revision_id'), + 'indexes' => array( + 'entity_test__id' => array('id'), + ), + 'foreign keys' => array( + 'entity_test__revisioned' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + ), + ), + 'entity_test_field_data' => array( + 'description' => 'The data table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('id', 'langcode'), + 'indexes' => array( + 'entity_test__revision_id' => array('revision_id'), + ), + 'foreign keys' => array( + 'entity_test' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + ), + ), + 'entity_test_revision_field_data' => array( + 'description' => 'The revision data table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('revision_id', 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + 'entity_test' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + 'entity_test__revision' => array( + 'table' => 'entity_test_revision', + 'columns' => array('revision_id' => 'revision_id'), + ), + ), + ), + ); + + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); + } + + /** + * Sets up the schema handler. + * + * This uses the field definitions set in $this->fieldDefinitions. + */ + protected function setUpSchemaHandler() { + $this->entityManager->expects($this->once()) + ->method('getFieldStorageDefinitions') + ->with($this->entityType->id()) + ->will($this->returnValue($this->storageDefinitions)); + $this->schemaHandler = new ContentEntitySchemaHandler( + $this->entityManager, + $this->entityType, + $this->storage + ); + } + + /** + * Sets up a field definition. + * + * @param string $field_name + * The field name. + * @param array $schema + * The schema array of the field definition, as returned from + * FieldDefinitionInterface::schema(). + */ + public function setUpStorageDefinition($field_name, array $schema) { + $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + // getDescription() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) + ->method('getDescription') + ->will($this->returnValue("The $field_name field.")); + // getSchema() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) + ->method('getSchema') + ->will($this->returnValue($schema)); + $this->storageDefinitions[$field_name]->expects($this->once()) + ->method('getColumns') + ->will($this->returnValue($schema['columns'])); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php new file mode 100644 index 0000000..196ff77 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php @@ -0,0 +1,268 @@ + 'Default table mapping', + 'description' => 'Check that the default table mapping works.', + 'group' => 'Entity', + ]; + } + + /** + * Tests DefaultTableMapping::getTableNames(). + * + * @covers ::getTableNames() + */ + public function testGetTableNames() { + // The storage definitions are only used in getColumnNames() so we do not + // need to provide any here. + $table_mapping = new DefaultTableMapping([]); + $this->assertSame([], $table_mapping->getTableNames()); + + $table_mapping->setFieldNames('foo', []); + $this->assertSame(['foo'], $table_mapping->getTableNames()); + + $table_mapping->setFieldNames('bar', []); + $this->assertSame(['foo', 'bar'], $table_mapping->getTableNames()); + + $table_mapping->setExtraColumns('baz', []); + $this->assertSame(['foo', 'bar', 'baz'], $table_mapping->getTableNames()); + + // Test that table names are not duplicated. + $table_mapping->setExtraColumns('foo', []); + $this->assertSame(['foo', 'bar', 'baz'], $table_mapping->getTableNames()); + } + + /** + * Tests DefaultTableMapping::getAllColumns(). + * + * @covers ::__construct() + * @covers ::getAllColumns() + * @covers ::getFieldNames() + * @covers ::getColumnNames() + * @covers ::setFieldNames() + * @covers ::getExtraColumns() + * @covers ::setExtraColumns() + */ + public function testGetAllColumns() { + // Set up single-column and multi-column definitions. + $definitions['id'] = $this->setUpDefinition(['value']); + $definitions['name'] = $this->setUpDefinition(['value']); + $definitions['type'] = $this->setUpDefinition(['value']); + $definitions['description'] = $this->setUpDefinition(['value', 'format']); + $definitions['owner'] = $this->setUpDefinition([ + 'target_id', + 'target_revision_id', + ]); + + $table_mapping = new DefaultTableMapping($definitions); + $expected = []; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + // Test adding field columns. + $table_mapping->setFieldNames('test', ['id']); + $expected = ['id']; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + $table_mapping->setFieldNames('test', ['id', 'name']); + $expected = ['id', 'name']; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + $table_mapping->setFieldNames('test', ['id', 'name', 'type']); + $expected = ['id', 'name', 'type']; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + $table_mapping->setFieldNames('test', [ + 'id', + 'name', + 'type', + 'description', + ]); + $expected = [ + 'id', + 'name', + 'type', + 'description__value', + 'description__format', + ]; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + $table_mapping->setFieldNames('test', [ + 'id', + 'name', + 'type', + 'description', + 'owner', + ]); + $expected = [ + 'id', + 'name', + 'type', + 'description__value', + 'description__format', + 'owner__target_id', + 'owner__target_revision_id', + ]; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + // Test adding extra columns. + $table_mapping->setFieldNames('test', []); + $table_mapping->setExtraColumns('test', ['default_langcode']); + $expected = ['default_langcode']; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + $table_mapping->setExtraColumns('test', [ + 'default_langcode', + 'default_revision', + ]); + $expected = ['default_langcode', 'default_revision']; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + + // Test adding both field and extra columns. + $table_mapping->setFieldNames('test', [ + 'id', + 'name', + 'type', + 'description', + 'owner', + ]); + $table_mapping->setExtraColumns('test', [ + 'default_langcode', + 'default_revision', + ]); + $expected = [ + 'id', + 'name', + 'type', + 'description__value', + 'description__format', + 'owner__target_id', + 'owner__target_revision_id', + 'default_langcode', + 'default_revision', + ]; + $this->assertSame($expected, $table_mapping->getAllColumns('test')); + } + + /** + * Tests DefaultTableMapping::getFieldNames(). + * + * @covers ::getFieldNames() + * @covers ::setFieldNames() + */ + public function testGetFieldNames() { + // The storage definitions are only used in getColumnNames() so we do not + // need to provide any here. + $table_mapping = new DefaultTableMapping([]); + + // Test that requesting the list of field names for a table for which no + // fields have been added does not fail. + $this->assertSame([], $table_mapping->getFieldNames('foo')); + + $return = $table_mapping->setFieldNames('foo', ['id', 'name', 'type']); + $this->assertSame($table_mapping, $return); + $expected = ['id', 'name', 'type']; + $this->assertSame($expected, $table_mapping->getFieldNames('foo')); + $this->assertSame([], $table_mapping->getFieldNames('bar')); + + $return = $table_mapping->setFieldNames('bar', ['description', 'owner']); + $this->assertSame($table_mapping, $return); + $expected = ['description', 'owner']; + $this->assertSame($expected, $table_mapping->getFieldNames('bar')); + // Test that the previously added field names are unaffected. + $expected = ['id', 'name', 'type']; + $this->assertSame($expected, $table_mapping->getFieldNames('foo')); + } + + /** + * Tests DefaultTableMapping::getColumnNames(). + * + * @covers ::__construct() + * @covers ::getColumnNames() + */ + public function testGetColumnNames() { + $definitions['test'] = $this->setUpDefinition([]); + $table_mapping = new DefaultTableMapping($definitions); + $expected = []; + $this->assertSame($expected, $table_mapping->getColumnNames('test')); + + $definitions['test'] = $this->setUpDefinition(['value']); + $table_mapping = new DefaultTableMapping($definitions); + $expected = ['value' => 'test']; + $this->assertSame($expected, $table_mapping->getColumnNames('test')); + + $definitions['test'] = $this->setUpDefinition(['value', 'format']); + $table_mapping = new DefaultTableMapping($definitions); + $expected = ['value' => 'test__value', 'format' => 'test__format']; + $this->assertSame($expected, $table_mapping->getColumnNames('test')); + } + + /** + * Tests DefaultTableMapping::getExtraColumns(). + * + * @covers ::getExtraColumns() + * @covers ::setExtraColumns() + */ + public function testGetExtraColumns() { + // The storage definitions are only used in getColumnNames() so we do not + // need to provide any here. + $table_mapping = new DefaultTableMapping([]); + + // Test that requesting the list of field names for a table for which no + // fields have been added does not fail. + $this->assertSame([], $table_mapping->getExtraColumns('foo')); + + $return = $table_mapping->setExtraColumns('foo', ['id', 'name', 'type']); + $this->assertSame($table_mapping, $return); + $expected = ['id', 'name', 'type']; + $this->assertSame($expected, $table_mapping->getExtraColumns('foo')); + $this->assertSame([], $table_mapping->getExtraColumns('bar')); + + $return = $table_mapping->setExtraColumns('bar', ['description', 'owner']); + $this->assertSame($table_mapping, $return); + $expected = ['description', 'owner']; + $this->assertSame($expected, $table_mapping->getExtraColumns('bar')); + // Test that the previously added field names are unaffected. + $expected = ['id', 'name', 'type']; + $this->assertSame($expected, $table_mapping->getExtraColumns('foo')); + } + + /** + * Sets up a field storage definition for the test. + * + * @param array $column_names + * An array of column names for the storage definition. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function setUpDefinition(array $column_names) { + $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $definition->expects($this->any()) + ->method('getColumns') + ->will($this->returnValue(array_fill_keys($column_names, []))); + return $definition; + } + +}