Skip to content

Instantly share code, notes, and snippets.

@Eyal-Shalev
Created May 19, 2016 11:24
Show Gist options
  • Save Eyal-Shalev/a3c27b942bb8e1361a5b81dfac2dcb1c to your computer and use it in GitHub Desktop.
Save Eyal-Shalev/a3c27b942bb8e1361a5b81dfac2dcb1c to your computer and use it in GitHub Desktop.
This is a Drupal 8 utilities service to help update entity fields.
<?php
/**
* @File
* Contains \Drupal\foo\EntityUpdater.
*/
namespace Drupal\foo;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class EntityUpdater
* @package Drupal\foo
*/
class EntityUpdater implements ContainerInjectionInterface {
const THROW_ERROR = 0;
const KEEP_DATA = 1;
const DELETE_DATA = 2;
const DELETE_AND_RETURN_DATA = 3;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
*/
protected $fieldStorageDefinitionListener;
/**
* @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
*/
protected $entityLastInstalledSchemaRepository;
/**
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* EntityUpdater constructor.
* @param \Drupal\Core\Database\Connection $database
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* @param \Drupal\Core\Field\FieldStorageDefinitionListenerInterface $field_storage_definition_listener
* @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository
*/
public function __construct(Connection $database, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldStorageDefinitionListenerInterface $field_storage_definition_listener, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository) {
$this->database = $database;
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->fieldStorageDefinitionListener = $field_storage_definition_listener;
$this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('field_storage_definition.listener'),
$container->get('entity.last_installed_schema.repository')
);
}
public function applyUpdates(EntityTypeInterface $entity_type) {
throw new \Exception('This feature was not implemented yet.');
}
public function createFields(EntityTypeInterface $entity_type, $field_names) {
throw new \Exception('This feature was not implemented yet.');
}
/**
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* @param string[] $field_names
* @param int $with_data
* @param callable|null $data_alter
* This function will alter the data array according to the provided context.
* Signature: $data_migration(array &data, array $context): mixed
* @return array|null
*/
public function updateFields(EntityTypeInterface $entity_type, $field_names, $with_data = self::KEEP_DATA, callable $data_alter = NULL) {
$this->entityTypeManager->clearCachedDefinitions();
// Get the field storage definitions that are currently used.
$original_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
$original_definitions = array_intersect_key($original_definitions, array_flip($field_names));
$definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type->id());
$definitions = array_intersect_key($definitions, array_flip($field_names));
if ($with_data === self::KEEP_DATA || $with_data === self::DELETE_AND_RETURN_DATA) {
$data = $this->getFieldData($entity_type, $field_names, $data_alter, $original_definitions);
}
if ($with_data === self::DELETE_DATA || $with_data === self::KEEP_DATA || $with_data === self::DELETE_AND_RETURN_DATA) {
// Deletes the existing data (if there is such), to avoid update issues.
$this->deleteFieldData($entity_type, $field_names, $original_definitions);
}
foreach ($field_names as $field_name) {
if (isset($original_definitions[$field_name])) {
$this->fieldStorageDefinitionListener->onFieldStorageDefinitionUpdate($definitions[$field_name], $original_definitions[$field_name]);
}
else {
throw new \LogicException($entity_type->getLabel() . ' does not contain the ' . $field_name . ' field.');
}
}
switch ($with_data) {
case self::KEEP_DATA:
if (isset($data)) {
$this->setFieldsData($entity_type, $field_names, $data, $definitions);
}
else {
throw new \LogicException('The data array is missing.');
}
return NULL;
case self::DELETE_AND_RETURN_DATA:
return isset($data) ? $data : NULL;
default:
return NULL;
}
}
/**
* Deletes base fields from the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* @param string[] $field_names
* @param int $with_data Could be set to the following options:
* self::DELETE_DATA
* Existing data will be deleted.
* self::DELETE_AND_RETURN_DATA
* Existing data will be deleted and returned.
* self::THROW_ERROR
* If data exists for one of the fields, an exception will be thrown.
* @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
* @return array|null
* if $with_data equals self::DELETE_AND_RETURN_DATA, then the data will be returned.
* otherwise NULL will be returned.
*/
public function deleteFields(EntityTypeInterface $entity_type, $field_names, $with_data = self::DELETE_DATA) {
$this->entityTypeManager->clearCachedDefinitions();
// Get the field storage definitions that are currently used.
$field_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
$field_storage_definitions = array_intersect_key($field_storage_definitions, array_flip($field_names));
switch ($with_data) {
case self::DELETE_AND_RETURN_DATA:
$data = $this->getFieldData($entity_type, $field_names, $field_storage_definitions);
$this->deleteFieldData($entity_type, $field_names, $field_storage_definitions);
break;
case self::DELETE_DATA:
$this->deleteFieldData($entity_type, $field_names, $field_storage_definitions);
break;
}
foreach ($field_names as $field_name) {
if (isset($field_storage_definitions[$field_name])) {
$this->fieldStorageDefinitionListener->onFieldStorageDefinitionDelete($field_storage_definitions[$field_name]);
}
else {
throw new \LogicException($entity_type->getLabel() . ' does not contain the ' . $field_name . ' field.');
}
}
if ($with_data === self::DELETE_AND_RETURN_DATA && isset($data)) {
return $data;
}
else {
return NULL;
}
}
/**
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* @param array $field_names
* @param callable|NULL $data_alter
* This function will alter the data array according to the provided context.
* Signature: $data_migration(array &data, array $context): mixed
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[]|NULL $storage_definitions
* @throws \Exception
* @return array;
* | ID | FIELD | DELTA | PROPERTY | VALUE | PROPERTY PATH |
* +====+===========+=======+==========+=======+=============================================+
* | 1 | deal_type | 0 | value | fixed | $data[1]['deal_type'][0]['value'] = 'fixed' |
* | | | | | | |
*/
public function getFieldData(EntityTypeInterface $entity_type, array $field_names, callable $data_alter = NULL, array $storage_definitions = NULL) {
$data = [];
$context = ['entity_type' => $entity_type];
if (empty($storage_definitions)) {
// Get the field storage definitions that are currently used.
$storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
$storage_definitions = array_intersect_key($storage_definitions, array_flip($field_names));
}
// Delete any data that may be in the fields.
$storage = \Drupal::entityTypeManager()->getStorage($entity_type->id());
if ($storage instanceof SqlContentEntityStorage) {
$table_mapping = $storage->getTableMapping($storage_definitions);
foreach ($storage_definitions as $definition) {
// Skip over definitions that do not require a dedicated table.
if ($table_mapping->requiresDedicatedTableStorage($definition)) {
$table_name = $table_mapping->getDedicatedDataTableName($definition);
$columns = $table_mapping->getColumnNames($definition->getName());
$properties = array_flip($columns);
$extra_columns = $table_mapping->getExtraColumns($table_name);
$query = $this->database
->select($table_name)
->fields($table_name, array_values($columns))
->fields($table_name, $extra_columns);
foreach ($query->execute()->fetchAll() as $result) {
// Add the keys to the data matrix.
$data += [
$result->entity_id => []
];
$data[$result->entity_id] += [
$definition->getName() => []
];
$data[$result->entity_id][$definition->getName()] += [
$result->delta => []
];
$context['entity_id'] = $result->entity_id;
$context['field_name'] = $definition->getName();
$context['delta'] = $result->delta;
foreach ($columns as $column_name) {
$property_name = $properties[$column_name];
$property_value = $result->{$column_name};
$context['property_name'] = $property_name;
$context['property_value'] = $property_value;
$data[$result->entity_id][$definition->getName()][$result->delta][$property_name] = $property_value;
if (isset($data_alter)) {
call_user_func_array($data_alter, [&$data, $context]);
}
}
foreach ($extra_columns as $column_name) {
$data[$result->entity_id][$definition->getName()][$result->delta][$column_name] = $result->{$column_name};
}
}
}
}
$base_field_names = array_filter($field_names, function ($field_name) use ($table_mapping, $entity_type) {
return $table_mapping->getFieldTableName($field_name) === $entity_type->getBaseTable();
});
$base_field_columns = array_reduce(array_map([$table_mapping, 'getColumnNames'], $base_field_names), 'array_merge', []);
$query = $this->database->select($entity_type->getBaseTable())
->fields($entity_type->getBaseTable(), [$entity_type->getKey('id')])
->fields($entity_type->getBaseTable(), array_values($base_field_columns));
foreach ($query->execute()->fetchAll() as $result) {
$entity_id = $result->{$entity_type->getKey('id')};
$data += [
$entity_id => []
];
$context['entity_id'] = $entity_id;
$context['delta'] = 0;
foreach ($base_field_columns as $property_name => $field_name) {
$property_value = $result->{$field_name};
$data[$entity_id] += [
$field_name => []
];
$data[$entity_id][$field_name][0] = [
$property_name => $property_value
];
$context['field_name'] = $field_name;
$context['property_name'] = $property_name;
$context['property_value'] = $property_value;
if (isset($data_alter)) {
call_user_func_array($data_alter, [&$data, $context]);
}
}
}
return $data;
}
else {
throw new \BadMethodCallException('The received entity type\'s storage does not extends ' . SqlContentEntityStorage::class);
}
}
/**
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* @param string[] $field_names
* @param mixed[][][][][] $data
* | ID | FIELD | DELTA | PROPERTY | VALUE | PROPERTY PATH |
* +====+===========+=======+==========+=======+=============================================+
* | 1 | deal_type | 0 | value | fixed | $data[1]['deal_type'][0]['value'] = 'fixed' |
* | | | | | | |
* @param FieldStorageDefinitionInterface[]|NULL $storage_definitions
* @throws \Exception
*/
public function setFieldsData(EntityTypeInterface $entity_type, array $field_names, array $data, array $storage_definitions = NULL) {
if (empty($storage_definitions)) {
// Get the field storage definitions that are currently used.
$storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
$storage_definitions = array_intersect_key($storage_definitions, array_flip($field_names));
}
// Delete any data that may be in the fields.
$storage = \Drupal::entityTypeManager()->getStorage($entity_type->id());
if ($storage instanceof SqlContentEntityStorage) {
$table_mapping = $storage->getTableMapping($storage_definitions);
foreach ($storage_definitions as $definition) {
// Skip over definitions that do not require a dedicated table.
if ($table_mapping->requiresDedicatedTableStorage($definition)) {
$table_name = $table_mapping->getDedicatedDataTableName($definition);
$condition_base = new Condition('AND');
foreach ($data as $id => $entity_data) {
$entity_condition = clone $condition_base;
$entity_condition = $entity_condition->condition('entity_id', $id);
foreach ($entity_data as $field_name => $field_data) {
foreach ($field_data as $delta => $field_item_data) {
$item_condition = clone $entity_condition;
$item_condition = $item_condition->condition('delta', $delta);
$column_data = [];
foreach ($field_item_data as $property_name => $property_value) {
$column_name = $table_mapping->getFieldColumnName($definition, $property_name);
$column_data[$column_name] = $property_value;
}
$select_query = $this->database->select($table_name);
$select_query->condition($item_condition);
if ($select_query->execute()->rowCount() === 1) {
$this->database->update($table_name)->condition($item_condition)->fields($column_data);
}
else {
$column_data['entity_id'] = $id;
$column_data['delta'] = $delta;
if ($entity_type->hasKey('bundle')) {
// @TODO add support for bundles.
// $column_data['bundle'] = $bundle;
}
else {
$column_data['bundle'] = $entity_type->id();
}
$this->database->insert($table_name)->fields($column_data)->execute();
}
}
}
}
}
}
$base_field_names = array_filter($field_names, function ($field_name) use ($table_mapping, $entity_type) {
return $table_mapping->getFieldTableName($field_name) === $entity_type->getBaseTable();
});
foreach ($data as $id => $entity_data) {
$columns_data = array_reduce(array_map(function($base_field_name) use ($entity_data, $table_mapping, $storage_definitions) {
$property_data = $entity_data[$base_field_name][0];
reset($property_data);
$column_name = $table_mapping->getFieldColumnName($storage_definitions[$base_field_name], key($property_data));
return [$column_name => current($property_data)];
}, $base_field_names), 'array_merge', []);
$this->database->update($entity_type->getBaseTable())
->condition($entity_type->getKey('id'), $id)
->fields($columns_data)->execute();
}
}
}
/**
* Deletes specific fields data across all the entities of a specifc type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* @param string[] $field_names
* @param \Drupal\Core\Field\FieldStorageDefinitionListenerInterface[]|NULL $storage_definitions
*/
public function deleteFieldData(EntityTypeInterface $entity_type, array $field_names, array $storage_definitions = NULL) {
if (empty($storage_definitions)) {
// Get the field storage definitions that are currently used.
$storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
$storage_definitions = array_intersect_key($storage_definitions, array_flip($field_names));
}
// Delete any data that may be in the fields.
$storage = \Drupal::entityTypeManager()->getStorage($entity_type->id());
if ($storage instanceof SqlContentEntityStorage) {
$table_mapping = $storage->getTableMapping($storage_definitions);
foreach ($table_mapping->getDedicatedTableNames() as $table_name) {
$this->database->delete($table_name)->execute();
}
$base_field_names = array_filter($field_names, function ($field_name) use ($table_mapping, $entity_type) {
return $table_mapping->getFieldTableName($field_name) === $entity_type->getBaseTable();
});
$null_fields = array_map(function () {
return NULL;
}, array_flip($base_field_names));
$this->database->update($entity_type->getBaseTable())
->fields($null_fields)
->execute();
if ($entity_type->isRevisionable()) {
$this->database->update($entity_type->getRevisionTable())
->fields($null_fields)
->execute();
}
}
}
}
@Eyal-Shalev
Copy link
Author

Eyal-Shalev commented May 19, 2016

Usage examples:

Deleting fields

function foo_update_8001() {
  /** @var \Drupal\foo\EntityUpdater $entity_updater */
  $entity_updater = \Drupal::service('foo.entity_updater');
  $bar_entity_type = \Drupal::entityTypeManager()->getDefinition('bar');

  $entity_updater->deleteFields($deal_entity_type, ['bad_field_1', 'bad_field_2']);
}

Updating fields

function foo_update_8002() {
  /** @var \Drupal\foo\EntityUpdater $entity_updater */
  $entity_updater = \Drupal::service('foo.entity_updater');
  $bar_entity_type = \Drupal::entityTypeManager()->getDefinition('bar');

  $alter = function(array &$data, array $context) {
    $data[$context['entity_id']][$context['field_name']][$context['delta']][$context['property_name']] .= ' Updated';
  };

  $entity_updater->updateFields($deal_entity_type, ['bad_field_1', 'bad_field_2'], $entity_updater::KEEP_DATA, $alter);
}

Updating fields (error thrown if data exists)

function foo_update_8002() {
  /** @var \Drupal\foo\EntityUpdater $entity_updater */
  $entity_updater = \Drupal::service('foo.entity_updater');
  $bar_entity_type = \Drupal::entityTypeManager()->getDefinition('bar');

  $entity_updater->updateFields($deal_entity_type, ['bad_field_1', 'bad_field_2'], $entity_updater::THROW_ERROR);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment