Issue #1893772 by chx, fubhy, slashrsm: Move entity-type specific storage logic into entity classes.

8.0.x
Alex Pott 2013-06-16 11:40:11 +02:00
parent d9f7b3a35b
commit 1648a479c4
69 changed files with 2269 additions and 1579 deletions

View File

@ -278,6 +278,7 @@ class ConfigStorageController extends EntityStorageControllerBase {
*/
public function create(array $values) {
$class = $this->entityInfo['class'];
$class::preCreate($this, $values);
// Set default language to site default if not provided.
$values += array('langcode' => language_default()->langcode);
@ -292,6 +293,7 @@ class ConfigStorageController extends EntityStorageControllerBase {
$uuid = new Uuid();
$entity->{$this->uuidKey} = $uuid->generate();
}
$entity->postCreate($this);
// Modules might need to add or change the data initially held by the new
// entity object, for instance to fill-in default values.
@ -314,7 +316,8 @@ class ConfigStorageController extends EntityStorageControllerBase {
return;
}
$this->preDelete($entities);
$entity_class = $this->entityInfo['class'];
$entity_class::preDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('predelete', $entity);
}
@ -324,7 +327,7 @@ class ConfigStorageController extends EntityStorageControllerBase {
$config->delete();
}
$this->postDelete($entities);
$entity_class::postDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('delete', $entity);
}
@ -367,7 +370,7 @@ class ConfigStorageController extends EntityStorageControllerBase {
$this->configFactory->rename($prefix . $id, $prefix . $entity->id());
}
$this->preSave($entity);
$entity->preSave($this);
$this->invokeHook('presave', $entity);
// Retrieve the desired properties and set them in config.
@ -378,7 +381,7 @@ class ConfigStorageController extends EntityStorageControllerBase {
if (!$is_new) {
$return = SAVED_UPDATED;
$config->save();
$this->postSave($entity, TRUE);
$entity->postSave($this, TRUE);
$this->invokeHook('update', $entity);
// Immediately update the original ID.
@ -388,7 +391,7 @@ class ConfigStorageController extends EntityStorageControllerBase {
$return = SAVED_NEW;
$config->save();
$entity->enforceIsNew(FALSE);
$this->postSave($entity, FALSE);
$entity->postSave($this, FALSE);
$this->invokeHook('insert', $entity);
}
@ -397,45 +400,6 @@ class ConfigStorageController extends EntityStorageControllerBase {
return $return;
}
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
*/
protected function preSave(EntityInterface $entity) {
}
/**
* Acts on a saved entity before the insert or update hook is invoked.
*
* Used after the entity is saved, but before invoking the insert or update
* hook.
*
* @param EntityInterface $entity
* The entity to act on.
* @param $update
* (bool) TRUE if the entity has been updated, or FALSE if it has been
* inserted.
*/
protected function postSave(EntityInterface $entity, $update) {
}
/**
* Acts on entities before they are deleted.
*
* Used before the entities are deleted and before invoking the delete hook.
*/
protected function preDelete($entities) {
}
/**
* Acts on deleted entities before the delete hook is invoked.
*
* Used after the entities are deleted but before invoking the delete hook.
*/
protected function postDelete($entities) {
}
/**
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::getFieldDefinitions().
*/

View File

@ -370,15 +370,17 @@ class DatabaseStorageController extends EntityStorageControllerBase {
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::create().
*/
public function create(array $values) {
$class = $this->entityInfo['class'];
$entity_class = $this->entityInfo['class'];
$entity_class::preCreate($this, $values);
$entity = new $class($values, $this->entityType);
$entity = new $entity_class($values, $this->entityType);
// Assign a new UUID if there is none yet.
if ($this->uuidKey && !isset($entity->{$this->uuidKey})) {
$uuid = new Uuid();
$entity->{$this->uuidKey} = $uuid->generate();
}
$entity->postCreate($this);
// Modules might need to add or change the data initially held by the new
// entity object, for instance to fill-in default values.
@ -398,7 +400,8 @@ class DatabaseStorageController extends EntityStorageControllerBase {
$transaction = $this->database->startTransaction();
try {
$this->preDelete($entities);
$entity_class = $this->entityInfo['class'];
$entity_class::preDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('predelete', $entity);
}
@ -417,7 +420,7 @@ class DatabaseStorageController extends EntityStorageControllerBase {
// Reset the cache as soon as the changes have been applied.
$this->resetCache($ids);
$this->postDelete($entities);
$entity_class::postDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('delete', $entity);
}
@ -442,7 +445,7 @@ class DatabaseStorageController extends EntityStorageControllerBase {
$entity->original = entity_load_unchanged($this->entityType, $entity->id());
}
$this->preSave($entity);
$entity->preSave($this);
$this->invokeHook('presave', $entity);
if (!$entity->isNew()) {
@ -458,7 +461,7 @@ class DatabaseStorageController extends EntityStorageControllerBase {
$this->saveRevision($entity);
}
$this->resetCache(array($entity->id()));
$this->postSave($entity, TRUE);
$entity->postSave($this, TRUE);
$this->invokeHook('update', $entity);
}
else {
@ -470,7 +473,7 @@ class DatabaseStorageController extends EntityStorageControllerBase {
$this->resetCache(array());
$entity->enforceIsNew(FALSE);
$this->postSave($entity, FALSE);
$entity->postSave($this, FALSE);
$this->invokeHook('insert', $entity);
}
@ -507,7 +510,7 @@ class DatabaseStorageController extends EntityStorageControllerBase {
// Cast to object as preSaveRevision() expects one to be compatible with the
// upcoming NG storage controller.
$record = (object) $record;
$this->preSaveRevision($record, $entity);
$entity->preSaveRevision($this, $record);
$record = (array) $record;
if ($entity->isNewRevision()) {
@ -527,49 +530,6 @@ class DatabaseStorageController extends EntityStorageControllerBase {
$entity->{$this->revisionKey} = $record[$this->revisionKey];
}
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
*/
protected function preSave(EntityInterface $entity) { }
/**
* Acts on a saved entity before the insert or update hook is invoked.
*
* Used after the entity is saved, but before invoking the insert or update
* hook.
*
* @param $update
* (bool) TRUE if the entity has been updated, or FALSE if it has been
* inserted.
*/
protected function postSave(EntityInterface $entity, $update) { }
/**
* Acts on entities before they are deleted.
*
* Used before the entities are deleted and before invoking the delete hook.
*/
protected function preDelete($entities) { }
/**
* Acts on deleted entities before the delete hook is invoked.
*
* Used after the entities are deleted but before invoking the delete hook.
*/
protected function postDelete($entities) { }
/**
* Act on a revision before being saved.
*
* @param \stdClass $record
* The revision object.
* @param Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*/
protected function preSaveRevision(\stdClass $record, EntityInterface $entity) { }
/**
* Invokes a hook on behalf of the entity.
*

View File

@ -98,6 +98,9 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
* A new entity object.
*/
public function create(array $values) {
$entity_class = $this->entityClass;
$entity_class::preCreate($this, $values);
// We have to determine the bundle first.
$bundle = FALSE;
if ($this->bundleKey) {
@ -118,6 +121,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$uuid = new Uuid();
$entity->{$this->uuidKey} = $uuid->generate();
}
$entity->postCreate($this);
// Modules might need to add or change the data initially held by the new
// entity object, for instance to fill-in default values.
@ -354,7 +358,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$entity->original = entity_load_unchanged($this->entityType, $entity->id());
}
$this->preSave($entity);
$entity->preSave($this);
$this->invokeHook('presave', $entity);
// Create the storage record to be saved.
@ -376,7 +380,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$this->savePropertyData($entity);
}
$this->resetCache(array($entity->id()));
$this->postSave($entity, TRUE);
$entity->postSave($this, TRUE);
$this->invokeHook('update', $entity);
}
else {
@ -394,7 +398,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$this->resetCache(array());
$entity->enforceIsNew(FALSE);
$this->postSave($entity, FALSE);
$entity->postSave($this, FALSE);
$this->invokeHook('insert', $entity);
}
@ -445,7 +449,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$record->{$this->revisionKey} = NULL;
}
$this->preSaveRevision($record, $entity);
$entity->preSaveRevision($this, $record);
if ($entity->isNewRevision()) {
drupal_write_record($this->revisionTable, $record);
@ -596,12 +600,14 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$transaction = $this->database->startTransaction();
try {
$entity_class = $this->entityClass;
$entity_class::preDelete($this, $entities);
// Ensure we are dealing with the actual entities.
foreach ($entities as $id => $entity) {
$entities[$id] = $entity->getNGEntity();
}
$this->preDelete($entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('predelete', $entity);
}
@ -626,7 +632,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
// Reset the cache as soon as the changes have been applied.
$this->resetCache($ids);
$this->postDelete($entities);
$entity_class::postDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('delete', $entity);
}

View File

@ -531,4 +531,52 @@ class Entity implements IteratorAggregate, EntityInterface {
return !empty($bundles[$this->bundle()]['translatable']);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageControllerInterface $storage_controller) {
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
}
}

View File

@ -533,4 +533,53 @@ class EntityBCDecorator implements IteratorAggregate, EntityInterface {
public function isTranslatable() {
return $this->decorated->isTranslatable();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$this->decorated->preSave($storage_controller);
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
$this->decorated->preSave($storage_controller, $record);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
$this->decorated->postSave($storage_controller, $update);
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
}
public function postCreate(EntityStorageControllerInterface $storage_controller) {
$this->decorated->postCreate($storage_controller);
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
}
}

View File

@ -162,6 +162,94 @@ interface EntityInterface extends ComplexDataInterface, AccessibleInterface, Tra
*/
public function delete();
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
*
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
*/
public function preSave(EntityStorageControllerInterface $storage_controller);
/**
* Acts on a revision before it gets saved.
*
* @param EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param \stdClass $record
* The revision object.
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record);
/**
* Acts on a saved entity before the insert or update hook is invoked.
*
* Used after the entity is saved, but before invoking the insert or update
* hook.
*
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param bool $update
* TRUE if the entity has been updated, or FALSE if it has been inserted.
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE);
/**
* Changes the values of an entity before it is created.
*
* Load defaults for example.
*
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param array $values
* An array of values to set, keyed by property name. If the entity type has
* bundles the bundle key has to be specified.
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values);
/**
* Acts on an entity after it is created but before hooks are invoked.
*
* @param EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
*/
public function postCreate(EntityStorageControllerInterface $storage_controller);
/**
* Acts on entities before they are deleted and before hooks are invoked.
*
* Used before the entities are deleted and before invoking the delete hook.
*
* @param EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param array $entities
* An array of entities.
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities);
/**
* Acts on deleted entities before the delete hook is invoked.
*
* Used after the entities are deleted but before invoking the delete hook.
*
* @param EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param array $entities
* An array of entities.
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities);
/**
* Acts on loaded entities before the load hook is invoked.
*
* @param EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param array $entities
* An array of entities.
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities);
/**
* Creates a duplicate of the entity.
*

View File

@ -33,7 +33,7 @@ interface EntityStorageControllerInterface {
* Loads one or more entities.
*
* @param $ids
* An array of entity IDs, or FALSE to load all entities.
* An array of entity IDs, or NULL to load all entities.
*
* @return
* An array of entity objects indexed by their ids.

View File

@ -8,6 +8,7 @@
namespace Drupal\aggregator;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\aggregator\Plugin\Core\Entity\Feed;
use Drupal\Core\Entity\EntityInterface;
/**
@ -16,101 +17,14 @@ use Drupal\Core\Entity\EntityInterface;
* This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for feed entities.
*/
class FeedStorageController extends DatabaseStorageControllerNG {
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::create().
*/
public function create(array $values) {
$values += array(
'link' => '',
'description' => '',
'image' => '',
);
return parent::create($values);
}
class FeedStorageController extends DatabaseStorageControllerNG implements FeedStorageControllerInterface {
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::attachLoad().
*/
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
parent::attachLoad($queried_entities, $load_revision);
foreach ($queried_entities as $item) {
$item->categories = db_query('SELECT c.cid, c.title FROM {aggregator_category} c JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => $item->id()))->fetchAllKeyed();
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::preDelete().
*/
protected function preDelete($entities) {
parent::preDelete($entities);
// Invalidate the block cache to update aggregator feed-based derivatives.
if (module_exists('block')) {
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
foreach ($entities as $entity) {
// Notify processors to remove stored items.
$manager = \Drupal::service('plugin.manager.aggregator.processor');
foreach ($manager->getDefinitions() as $id => $definition) {
$manager->createInstance($id)->remove($entity);
}
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::postDelete().
*/
protected function postDelete($entities) {
parent::postDelete($entities);
foreach ($entities as $entity) {
// Make sure there is no active block for this feed.
$block_configs = config_get_storage_names_with_prefix('plugin.core.block');
foreach ($block_configs as $config_id) {
$config = config($config_id);
if ($config->get('id') == 'aggregator_feed_block:' . $entity->id()) {
$config->delete();
}
}
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::preSave().
*/
protected function preSave(EntityInterface $entity) {
parent::preSave($entity);
// Invalidate the block cache to update aggregator feed-based derivatives.
if (module_exists('block')) {
drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
}
// An existing feed is being modified, delete the category listings.
db_delete('aggregator_category_feed')
->condition('fid', $entity->id())
->execute();
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
if (!empty($entity->categories)) {
foreach ($entity->categories as $cid => $value) {
if ($value) {
db_insert('aggregator_category_feed')
->fields(array(
'fid' => $entity->id(),
'cid' => $cid,
))
->execute();
}
}
}
$this->loadCategories($queried_entities);
}
/**
@ -191,4 +105,39 @@ class FeedStorageController extends DatabaseStorageControllerNG {
return $fields;
}
/**
* {@inheritdoc}
*/
public function loadCategories(array $feeds) {
foreach ($feeds as $feed) {
$feed->categories = $this->database->query('SELECT c.cid, c.title FROM {aggregator_category} c JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => $feed->id()))->fetchAllKeyed();
}
}
/**
* {@inheritdoc}
*/
public function saveCategories(Feed $feed, array $categories) {
foreach ($categories as $cid => $value) {
if ($value) {
$this->database->insert('aggregator_category_feed')
->fields(array(
'fid' => $feed->id(),
'cid' => $cid,
))
->execute();
}
}
}
/**
* {@inheritdoc}
*/
public function deleteCategories(array $feeds) {
// An existing feed is being modified, delete the category listings.
$this->database->delete('aggregator_category_feed')
->condition('fid', array_keys($feeds))
->execute();
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\aggregator\FeedStorageControllerInterface.
*/
namespace Drupal\aggregator;
use Drupal\aggregator\Plugin\Core\Entity\Feed;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for aggregator feed entity controller classes.
*/
interface FeedStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Loads the categories of a feed.
*
* @param array $entities
* A list of feed entities keyed by feed id. Each entity will get a
* categories property added.
*/
public function loadCategories(array $feeds);
/**
* Saves the categories of a feed.
*
* @param Feed $feed
* The feed entity.
* @param array $categories
* The array of categories.
*/
public function saveCategories(Feed $feed, array $categories);
/**
* Deletes the categories of a feed.
*
* @param array $feeds
* A list of feed entities keyed by feed id.
*/
public function deleteCategories(array $feeds);
}

View File

@ -8,6 +8,7 @@
namespace Drupal\aggregator;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\aggregator\Plugin\Core\Entity\Item;
use Drupal\Core\Entity\EntityInterface;
/**
@ -16,55 +17,14 @@ use Drupal\Core\Entity\EntityInterface;
* This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for feed item entities.
*/
class ItemStorageController extends DatabaseStorageControllerNG {
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::create().
*/
public function create(array $values) {
$entity = parent::create($values);
// Set an initial timestamp, this will be overwritten if known.
$entity->timestamp->value = REQUEST_TIME;
return $entity;
}
class ItemStorageController extends DatabaseStorageControllerNG implements ItemStorageControllerInterface {
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::attachLoad().
*/
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
parent::attachLoad($queried_entities, $load_revision);
foreach ($queried_entities as $item) {
$item->categories = db_query('SELECT c.title, c.cid FROM {aggregator_category_item} ci LEFT JOIN {aggregator_category} c ON ci.cid = c.cid WHERE ci.iid = :iid ORDER BY c.title', array(':iid' => $item->id()))->fetchAll();
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::preDelete().
*/
protected function preDelete($entities) {
parent::preDelete($entities);
db_delete('aggregator_category_item')
->condition('iid', array_keys($entities), 'IN')
->execute();
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
$result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $entity->fid->value));
foreach ($result as $category) {
db_merge('aggregator_category_item')
->key(array(
'iid' => $entity->id(),
'cid' => $category->cid,
))
->execute();
}
$this->loadCategories($queried_entities);
}
/**
@ -120,4 +80,36 @@ class ItemStorageController extends DatabaseStorageControllerNG {
return $fields;
}
/**
* {@inheritdoc}
*/
public function loadCategories(array $entities) {
foreach ($entities as $item) {
$item->categories = db_query('SELECT c.title, c.cid FROM {aggregator_category_item} ci LEFT JOIN {aggregator_category} c ON ci.cid = c.cid WHERE ci.iid = :iid ORDER BY c.title', array(':iid' => $item->id()))->fetchAll();
}
}
/**
* {@inheritdoc}
*/
public function deleteCategories(array $entities) {
$this->database->delete('aggregator_category_item')
->condition('iid', array_keys($entities))
->execute();
}
/**
* {@inheritdoc}
*/
public function saveCategories(Item $item) {
$result = $this->database->query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $item->fid->value));
foreach ($result as $category) {
$this->database->merge('aggregator_category_item')
->key(array(
'iid' => $item->id(),
'cid' => $category->cid,
))
->execute();
}
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains Drupal\aggregator\ItemStorageControllerInterface.
*/
namespace Drupal\aggregator;
use Drupal\aggregator\Plugin\Core\Entity\Item;
use Drupal\core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for aggregator item entity controller classes.
*/
interface ItemStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Load the categories for aggregator items.
*
* @param array $entities
* An array of aggregator item objects, keyed by the item id. Each object
* will get a categories property added.
*/
public function loadCategories(array $entities);
/**
* Delete the categories for aggregator items.
*
* @param array $entities
* An array of aggregator item objects, keyed by the item id being
* deleted. The storage backend should delete the category data of the
* items.
*/
public function deleteCategories(array $entities);
/**
* Store the categories for aggregator items.
*
* @param Item $item
* The storage backend should save the categories of this item.
*/
public function saveCategories(Item $item);
}

View File

@ -8,6 +8,8 @@
namespace Drupal\aggregator\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityNG;
use Symfony\Component\DependencyInjection\Container;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\aggregator\FeedInterface;
@ -157,7 +159,6 @@ class Feed extends EntityNG implements FeedInterface {
unset($this->etag);
unset($this->modified);
unset($this->block);
}
/**
@ -173,4 +174,75 @@ class Feed extends EntityNG implements FeedInterface {
public function label($langcode = NULL) {
return $this->get('title')->value;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
$values += array(
'link' => '',
'description' => '',
'image' => '',
);
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
// Invalidate the block cache to update aggregator feed-based derivatives.
if (module_exists('block')) {
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
$storage_controller->deleteCategories($entities);
foreach ($entities as $entity) {
// Notify processors to remove stored items.
$manager = \Drupal::service('plugin.manager.aggregator.processor');
foreach ($manager->getDefinitions() as $id => $definition) {
$manager->createInstance($id)->remove($entity);
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $entity) {
// Make sure there is no active block for this feed.
$block_configs = config_get_storage_names_with_prefix('plugin.core.block');
foreach ($block_configs as $config_id) {
$config = config($config_id);
if ($config->get('id') == 'aggregator_feed_block:' . $entity->id()) {
$config->delete();
}
}
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$this->clearBlockCacheDefinitions();
$storage_controller->deleteCategories(array($this->id() => $this));
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = FALSE) {
if (!empty($this->categories)) {
$storage_controller->saveCategories($this, $this->categories);
}
}
/**
* Invalidate the block cache to update aggregator feed-based derivatives.
*/
protected function clearBlockCacheDefinitions() {
if ($block_manager = \Drupal::getContainer()->get('plugin.manager.block', Container::NULL_ON_INVALID_REFERENCE)) {
$block_manager->clearCachedDefinitions();
}
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\aggregator\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\aggregator\ItemInterface;
@ -130,4 +131,25 @@ class Item extends EntityNG implements ItemInterface {
public function label($langcode = NULL) {
return $this->get('title')->value;
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageControllerInterface $storage_controller) {
$this->timestamp->value = REQUEST_TIME;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
$storage_controller->saveCategories($this);
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
$storage_controller->deleteCategories($entities);
}
}

View File

@ -8,41 +8,15 @@
namespace Drupal\custom_block;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Controller class for custom blocks.
*
* This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for custom block entities.
* This extends the Drupal\Core\Entity\DatabaseStorageControllerNG class,
* adding required special handling for custom block entities.
*/
class CustomBlockStorageController extends DatabaseStorageControllerNG {
/**
* Overrides \Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
*/
protected function preSaveRevision(\stdClass $record, EntityInterface $entity) {
if ($entity->isNewRevision()) {
// When inserting either a new custom block or a new custom_block
// revision, $entity->log must be set because {block_custom_revision}.log
// is a text column and therefore cannot have a default value. However,
// it might not be set at this point (for example, if the user submitting
// the form does not have permission to create revisions), so we ensure
// that it is at least an empty string in that case.
// @todo: Make the {block_custom_revision}.log column nullable so that we
// can remove this check.
if (!isset($record->log)) {
$record->log = '';
}
}
elseif (isset($entity->original) && (!isset($record->log) || $record->log === '')) {
// If we are updating an existing custom_block without adding a new
// revision, we need to make sure $entity->log is reset whenever it is
// empty. Therefore, this code allows us to avoid clobbering an existing
// log entry with an empty one.
$record->log = $entity->original->log->value;
}
}
class CustomBlockStorageController extends DatabaseStorageControllerNG implements EntityStorageControllerInterface {
/**
* Overrides \Drupal\Core\Entity\DatabaseStorageController::attachLoad().
@ -62,14 +36,6 @@ class CustomBlockStorageController extends DatabaseStorageControllerNG {
parent::attachLoad($blocks, $load_revision);
}
/**
* Overrides \Drupal\Core\Entity\DatabaseStorageController::postSave().
*/
protected function postSave(EntityInterface $block, $update) {
// Invalidate the block cache to update custom block-based derivatives.
drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
}
/**
* Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::basePropertyDefinitions().
*/

View File

@ -1,44 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\custom_block\CustomBlockTypeStorageController.
*/
namespace Drupal\custom_block;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Controller class for custom block types.
*/
class CustomBlockTypeStorageController extends ConfigStorageController {
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
if (!$update) {
entity_invoke_bundle_hook('create', 'custom_block', $entity->id());
custom_block_add_body_field($entity->id());
}
elseif ($entity->original->id() != $entity->id()) {
entity_invoke_bundle_hook('rename', 'custom_block', $entity->original->id(), $entity->id());
}
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
*/
protected function postDelete($entities) {
parent::postDelete($entities);
foreach ($entities as $entity) {
entity_invoke_bundle_hook('delete', 'custom_block', $entity->id());
}
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\custom_block\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\custom_block\CustomBlockInterface;
@ -181,6 +182,14 @@ class CustomBlock extends EntityNG implements CustomBlockInterface {
);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Invalidate the block cache to update custom block-based derivatives.
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
/**
* {@inheritdoc}
*/
@ -188,6 +197,30 @@ class CustomBlock extends EntityNG implements CustomBlockInterface {
return entity_load_multiple_by_properties('block', array('plugin' => 'custom_block:' . $this->uuid->value));
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
if ($this->isNewRevision()) {
// When inserting either a new custom block or a new custom_block
// revision, $entity->log must be set because {block_custom_revision}.log
// is a text column and therefore cannot have a default value. However,
// it might not be set at this point (for example, if the user submitting
// the form does not have permission to create revisions), so we ensure
// that it is at least an empty string in that case.
// @todo: Make the {block_custom_revision}.log column nullable so that we
// can remove this check.
if (!isset($record->log)) {
$record->log = '';
}
}
elseif (isset($this->original) && (!isset($record->log) || $record->log === '')) {
// If we are updating an existing custom_block without adding a new
// revision and the user did not supply a log, keep the existing one.
$record->log = $this->original->log->value;
}
}
/**
* {@inheritdoc}
*/

View File

@ -10,6 +10,7 @@ namespace Drupal\custom_block\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\custom_block\CustomBlockTypeInterface;
/**
@ -20,7 +21,7 @@ use Drupal\custom_block\CustomBlockTypeInterface;
* label = @Translation("Custom block type"),
* module = "custom_block",
* controllers = {
* "storage" = "Drupal\custom_block\CustomBlockTypeStorageController",
* "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
* "form" = {
* "default" = "Drupal\custom_block\CustomBlockTypeFormController"
* },
@ -83,4 +84,26 @@ class CustomBlockType extends ConfigEntityBase implements CustomBlockTypeInterfa
)
);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
if (!$update) {
entity_invoke_bundle_hook('create', 'custom_block', $this->id());
custom_block_add_body_field($this->id);
}
elseif ($this->originalID != $this->id) {
entity_invoke_bundle_hook('rename', 'custom_block', $this->originalID, $this->id);
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $entity) {
entity_invoke_bundle_hook('delete', 'custom_block', $entity->id());
}
}
}

View File

@ -26,13 +26,4 @@ class BlockStorageController extends ConfigStorageController {
});
}
/**
* {@inheritdoc}
*/
protected function preSave(EntityInterface $entity) {
parent::preSave($entity);
$entity->set('settings', $entity->getPlugin()->getConfig());
}
}

View File

@ -12,6 +12,7 @@ use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\block\BlockPluginBag;
use Drupal\block\BlockInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a Block configuration entity class.
@ -163,4 +164,11 @@ class Block extends ConfigEntityBase implements BlockInterface {
return $properties;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$this->set('settings', $this->getPlugin()->getConfig());
}
}

View File

@ -18,14 +18,15 @@ use LogicException;
* This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for comment entities.
*/
class CommentStorageController extends DatabaseStorageControllerNG {
class CommentStorageController extends DatabaseStorageControllerNG implements CommentStorageControllerInterface {
/**
* The thread for which a lock was acquired.
*/
protected $threadLock = '';
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::buildQuery().
* {@inheritdoc}
*/
protected function buildQuery($ids, $revision_id = FALSE) {
$query = parent::buildQuery($ids, $revision_id);
@ -39,7 +40,7 @@ class CommentStorageController extends DatabaseStorageControllerNG {
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::attachLoad().
* {@inheritdoc}
*/
protected function attachLoad(&$records, $load_revision = FALSE) {
// Prepare standard comment fields.
@ -52,156 +53,9 @@ class CommentStorageController extends DatabaseStorageControllerNG {
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageControllerNG::create().
* {@inheritdoc}
*/
public function create(array $values) {
if (empty($values['node_type']) && !empty($values['nid'])) {
$node = node_load(is_object($values['nid']) ? $values['nid']->value : $values['nid']);
$values['node_type'] = 'comment_node_' . $node->type;
}
return parent::create($values);
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
*
* @see comment_int_to_alphadecimal()
* @see comment_alphadecimal_to_int()
*/
protected function preSave(EntityInterface $comment) {
global $user;
if (!isset($comment->status->value)) {
$comment->status->value = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
}
// Make sure we have a proper bundle name.
if (!isset($comment->node_type->value)) {
$comment->node_type->value = 'comment_node_' . $comment->nid->entity->type;
}
if ($comment->isNew()) {
// Add the comment to database. This next section builds the thread field.
// Also see the documentation for comment_view().
if (!empty($comment->thread->value)) {
// Allow calling code to set thread itself.
$thread = $comment->thread->value;
}
else {
if ($this->threadLock) {
// As preSave() is protected, this can only happen when this class
// is extended in a faulty manner.
throw new LogicException('preSave is called again without calling postSave() or releaseThreadLock()');
}
if ($comment->pid->target_id == 0) {
// This is a comment with no parent comment (depth 0): we start
// by retrieving the maximum thread level.
$max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid->target_id))->fetchField();
// Strip the "/" from the end of the thread.
$max = rtrim($max, '/');
// We need to get the value at the correct depth.
$parts = explode('.', $max);
$n = comment_alphadecimal_to_int($parts[0]);
$prefix = '';
}
else {
// This is a comment with a parent comment, so increase the part of
// the thread value at the proper depth.
// Get the parent comment:
$parent = $comment->pid->entity;
// Strip the "/" from the end of the parent thread.
$parent->thread->value = (string) rtrim((string) $parent->thread->value, '/');
$prefix = $parent->thread->value . '.';
// Get the max value in *this* thread.
$max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
':thread' => $parent->thread->value . '.%',
':nid' => $comment->nid->target_id,
))->fetchField();
if ($max == '') {
// First child of this parent. As the other two cases do an
// increment of the thread number before creating the thread
// string set this to -1 so it requires an increment too.
$n = -1;
}
else {
// Strip the "/" at the end of the thread.
$max = rtrim($max, '/');
// Get the value at the correct depth.
$parts = explode('.', $max);
$parent_depth = count(explode('.', $parent->thread->value));
$n = comment_alphadecimal_to_int($parts[$parent_depth]);
}
}
// Finally, build the thread field for this new comment. To avoid
// race conditions, get a lock on the thread. If aother process already
// has the lock, just move to the next integer.
do {
$thread = $prefix . comment_int_to_alphadecimal(++$n) . '/';
} while (!lock()->acquire("comment:{$comment->nid->target_id}:$thread"));
$this->threadLock = $thread;
}
if (empty($comment->created->value)) {
$comment->created->value = REQUEST_TIME;
}
if (empty($comment->changed->value)) {
$comment->changed->value = $comment->created->value;
}
// We test the value with '===' because we need to modify anonymous
// users as well.
if ($comment->uid->target_id === $user->uid && $user->uid) {
$comment->name->value = $user->name;
}
// Add the values which aren't passed into the function.
$comment->thread->value = $thread;
$comment->hostname->value = \Drupal::request()->getClientIP();
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
*/
protected function postSave(EntityInterface $comment, $update) {
$this->releaseThreadLock();
// Update the {node_comment_statistics} table prior to executing the hook.
$this->updateNodeStatistics($comment->nid->target_id);
if ($comment->status->value == COMMENT_PUBLISHED) {
module_invoke_all('comment_publish', $comment);
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
*/
protected function postDelete($comments) {
// Delete the comments' replies.
$query = db_select('comment', 'c')
->fields('c', array('cid'))
->condition('pid', array(array_keys($comments)), 'IN');
$child_cids = $query->execute()->fetchCol();
entity_delete_multiple('comment', $child_cids);
foreach ($comments as $comment) {
$this->updateNodeStatistics($comment->nid->target_id);
}
}
/**
* Updates the comment statistics for a given node.
*
* The {node_comment_statistics} table has the following fields:
* - last_comment_timestamp: The timestamp of the last comment for this node,
* or the node created timestamp if no comments exist for the node.
* - last_comment_name: The name of the anonymous poster for the last comment.
* - last_comment_uid: The user ID of the poster for the last comment for
* this node, or the node author's user ID if no comments exist for the
* node.
* - comment_count: The total number of approved/published comments on this
* node.
*
* @param $nid
* The node ID.
*/
protected function updateNodeStatistics($nid) {
public function updateNodeStatistics($nid) {
// Allow bulk updates and inserts to temporarily disable the
// maintenance of the {node_comment_statistics} table.
if (!variable_get('comment_maintain_node_statistics', TRUE)) {
@ -247,17 +101,7 @@ class CommentStorageController extends DatabaseStorageControllerNG {
}
/**
* Release the lock acquired for the thread in preSave().
*/
protected function releaseThreadLock() {
if ($this->threadLock) {
lock()->release($this->threadLock);
$this->threadLock = '';
}
}
/**
* Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::basePropertyDefinitions().
* {@inheritdoc}
*/
public function baseFieldDefinitions() {
$properties['cid'] = array(
@ -356,4 +200,32 @@ class CommentStorageController extends DatabaseStorageControllerNG {
);
return $properties;
}
/**
* {@inheritdoc}
*/
public function getMaxThread(EntityInterface $comment) {
return db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid->target_id))->fetchField();
}
/**
* {@inheritdoc}
*/
public function getMaxThreadPerThread(EntityInterface $comment) {
return $this->database->query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
':thread' => rtrim($comment->pid->entity->thread->value, '/') . '.%',
':nid' => $comment->nid->target_id,
))->fetchField();
}
/**
* {@inheritdoc}
*/
public function getChildCids(array $comments) {
return $this->database->select('comment', 'c')
->fields('c', array('cid'))
->condition('pid', array_keys($comments))
->execute()
->fetchCol();
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentStorageControllerInterface.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for comment entity controller classes.
*/
interface CommentStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Get the maximum encoded thread value for the top level comments.
*
* @param EntityInterface $comment
* A comment entity.
*
* @return string
* The maximum encoded thread value among the top level comments of the
* node $comment belongs to.
*/
public function getMaxThread(EntityInterface $comment);
/**
* Get the maximum encoded thread value for the children of this comment.
*
* @param EntityInterface $comment
* A comment entity.
*
* @return string
* The maximum encoded thread value among all replies of $comment.
*/
public function getMaxThreadPerThread(EntityInterface $comment);
/**
* Gets the comment ids of the passed comment entities' children.
*
* @param array $comments
* An array of comment entities keyed by their ids.
* @return array
* The entity ids of the passed comment entities' children as an array.
*/
public function getChildCids(array $comments);
/**
* Updates the comment statistics for a given node.
*
* The {node_comment_statistics} table has the following fields:
* - last_comment_timestamp: The timestamp of the last comment for this node,
* or the node created timestamp if no comments exist for the node.
* - last_comment_name: The name of the anonymous poster for the last comment.
* - last_comment_uid: The user ID of the poster for the last comment for
* this node, or the node author's user ID if no comments exist for the
* node.
* - comment_count: The total number of approved/published comments on this
* node.
*
* @param $nid
* The node ID.
*/
public function updateNodeStatistics($nid);
}

View File

@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\comment\CommentInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Language\Language;
/**
@ -224,6 +225,139 @@ class Comment extends EntityNG implements CommentInterface {
return $this->get('cid')->value;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
if (empty($values['node_type']) && !empty($values['nid'])) {
$node = node_load(is_object($values['nid']) ? $values['nid']->value : $values['nid']);
$values['node_type'] = 'comment_node_' . $node->type;
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
global $user;
if (!isset($this->status->value)) {
$this->status->value = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
}
// Make sure we have a proper bundle name.
if (!isset($this->node_type->value)) {
$this->node_type->value = 'comment_node_' . $this->nid->entity->type;
}
if ($this->isNew()) {
// Add the comment to database. This next section builds the thread field.
// Also see the documentation for comment_view().
if (!empty($this->thread->value)) {
// Allow calling code to set thread itself.
$thread = $this->thread->value;
}
else {
if ($this->threadLock) {
// As preSave() is protected, this can only happen when this class
// is extended in a faulty manner.
throw new \LogicException('preSave is called again without calling postSave() or releaseThreadLock()');
}
if ($this->pid->target_id == 0) {
// This is a comment with no parent comment (depth 0): we start
// by retrieving the maximum thread level.
$max = $storage_controller->getMaxThread($this);
// Strip the "/" from the end of the thread.
$max = rtrim($max, '/');
// We need to get the value at the correct depth.
$parts = explode('.', $max);
$n = comment_alphadecimal_to_int($parts[0]);
$prefix = '';
}
else {
// This is a comment with a parent comment, so increase the part of
// the thread value at the proper depth.
// Get the parent comment:
$parent = $this->pid->entity;
// Strip the "/" from the end of the parent thread.
$parent->thread->value = (string) rtrim((string) $parent->thread->value, '/');
$prefix = $parent->thread->value . '.';
// Get the max value in *this* thread.
$max = $storage_controller->getMaxThreadPerThread($this);
if ($max == '') {
// First child of this parent. As the other two cases do an
// increment of the thread number before creating the thread
// string set this to -1 so it requires an increment too.
$n = -1;
}
else {
// Strip the "/" at the end of the thread.
$max = rtrim($max, '/');
// Get the value at the correct depth.
$parts = explode('.', $max);
$parent_depth = count(explode('.', $parent->thread->value));
$n = comment_alphadecimal_to_int($parts[$parent_depth]);
}
}
// Finally, build the thread field for this new comment. To avoid
// race conditions, get a lock on the thread. If aother process already
// has the lock, just move to the next integer.
do {
$thread = $prefix . comment_int_to_alphadecimal(++$n) . '/';
} while (!lock()->acquire("comment:{$this->nid->target_id}:$thread"));
$this->threadLock = $thread;
}
if (empty($this->created->value)) {
$this->created->value = REQUEST_TIME;
}
if (empty($this->changed->value)) {
$this->changed->value = $this->created->value;
}
// We test the value with '===' because we need to modify anonymous
// users as well.
if ($this->uid->target_id === $user->uid && $user->uid) {
$this->name->value = $user->name;
}
// Add the values which aren't passed into the function.
$this->thread->value = $thread;
$this->hostname->value = \Drupal::request()->getClientIP();
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
$this->releaseThreadLock();
// Update the {node_comment_statistics} table prior to executing the hook.
$storage_controller->updateNodeStatistics($this->nid->target_id);
if ($this->status->value == COMMENT_PUBLISHED) {
module_invoke_all('comment_publish', $this);
}
}
/**
* Release the lock acquired for the thread in preSave().
*/
protected function releaseThreadLock() {
if ($this->threadLock) {
lock()->release($this->threadLock);
$this->threadLock = '';
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
$child_cids = $storage_controller->getChildCids($entities);
entity_delete_multiple('comment', $child_cids);
foreach ($entities as $id => $entity) {
$storage_controller->updateNodeStatistics($entity->nid->target_id);
}
}
/**
* {@inheritdoc}
*/

View File

@ -8,36 +8,10 @@
namespace Drupal\contact;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Controller class for contact categories.
*/
class CategoryStorageController extends ConfigStorageController {
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
if (!$update) {
entity_invoke_bundle_hook('create', 'contact_message', $entity->id());
}
elseif ($entity->original->id() != $entity->id()) {
entity_invoke_bundle_hook('rename', 'contact_message', $entity->original->id(), $entity->id());
}
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
*/
protected function postDelete($entities) {
parent::postDelete($entities);
foreach ($entities as $entity) {
entity_invoke_bundle_hook('delete', 'contact_message', $entity->id());
}
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\contact\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\contact\CategoryInterface;
@ -81,4 +82,25 @@ class Category extends ConfigEntityBase implements CategoryInterface {
*/
public $weight = 0;
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
if (!$update) {
entity_invoke_bundle_hook('create', 'contact_message', $this->id());
}
elseif ($this->original->id() != $this->id()) {
entity_invoke_bundle_hook('rename', 'contact_message', $this->original->id(), $this->id());
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $entity) {
entity_invoke_bundle_hook('delete', 'contact_message', $entity->id());
}
}
}

View File

@ -18,7 +18,7 @@ use Drupal\Core\Annotation\Translation;
* label = @Translation("Test Entity with a bundle key"),
* module = "field_test",
* controllers = {
* "storage" = "Drupal\field_test\TestEntityController",
* "storage" = "Drupal\Core\Entity\DatabaseStorageController",
* "form" = {
* "default" = "Drupal\field_test\TestEntityFormController"
* }

View File

@ -18,7 +18,7 @@ use Drupal\Core\Annotation\Translation;
* label = @Translation("Test Entity with a specified bundle"),
* module = "field_test",
* controllers = {
* "storage" = "Drupal\field_test\TestEntityController",
* "storage" = "Drupal\Core\Entity\DatabaseStorageController",
* "form" = {
* "default" = "Drupal\field_test\TestEntityFormController"
* }

View File

@ -18,7 +18,7 @@ use Drupal\Core\Annotation\Translation;
* label = @Translation("Test Entity, cacheable"),
* module = "field_test",
* controllers = {
* "storage" = "Drupal\field_test\TestEntityController"
* "storage" = "Drupal\Core\Entity\DatabaseStorageController"
* },
* field_cache = TRUE,
* base_table = "test_entity",

View File

@ -10,6 +10,7 @@ namespace Drupal\field_test\Plugin\Core\Entity;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Test entity class.
@ -19,7 +20,7 @@ use Drupal\Core\Annotation\Translation;
* label = @Translation("Test Entity"),
* module = "field_test",
* controllers = {
* "storage" = "Drupal\field_test\TestEntityController",
* "storage" = "Drupal\Core\Entity\DatabaseStorageController",
* "render" = "Drupal\Core\Entity\EntityRenderController",
* "form" = {
* "default" = "Drupal\field_test\TestEntityFormController"
@ -86,5 +87,16 @@ class TestEntity extends Entity {
public function bundle() {
return !empty($this->fttype) ? $this->fttype : $this->entityType();
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
// Allow for predefined revision ids.
if (!empty($record->use_provided_revision_id)) {
$record->ftvid = $record->use_provided_revision_id;
}
}
}

View File

@ -1,28 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\field_test\TestEntityController.
*/
namespace Drupal\field_test;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Controller class for the test entity entity types.
*/
class TestEntityController extends DatabaseStorageController {
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
*/
public function preSaveRevision(\stdClass $record, EntityInterface $entity) {
// Allow for predefined revision ids.
if (!empty($record->use_provided_revision_id)) {
$record->ftvid = $record->use_provided_revision_id;
}
}
}

View File

@ -8,76 +8,17 @@
namespace Drupal\file;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\Language;
/**
* File storage controller for files.
*/
class FileStorageController extends DatabaseStorageControllerNG {
class FileStorageController extends DatabaseStorageControllerNG implements FileStorageControllerInterface {
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::create().
*/
public function create(array $values) {
// Automatically detect filename if not set.
if (!isset($values['filename']) && isset($values['uri'])) {
$values['filename'] = drupal_basename($values['uri']);
}
// Automatically detect filemime if not set.
if (!isset($values['filemime']) && isset($values['uri'])) {
$values['filemime'] = file_get_mimetype($values['uri']);
}
return parent::create($values);
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::presave().
*/
protected function preSave(EntityInterface $entity) {
$entity->timestamp = REQUEST_TIME;
$entity->setSize(filesize($entity->getFileUri()));
if (!$entity->langcode->value) {
// Default the file's language code to none, because files are language
// neutral more often than language dependent. Until we have better
// flexible settings.
// @todo See http://drupal.org/node/258785 and followups.
$entity->langcode = Language::LANGCODE_NOT_SPECIFIED;
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preDelete().
*/
public function preDelete($entities) {
foreach ($entities as $entity) {
// Delete the actual file. Failures due to invalid files and files that
// were already deleted are logged to watchdog but ignored, the
// corresponding file entity will be deleted.
file_unmanaged_delete($entity->getFileUri());
}
// Delete corresponding file usage entries.
db_delete('file_usage')
->condition('fid', array_keys($entities), 'IN')
->execute();
}
/**
* Determines total disk space used by a single user or the whole filesystem.
*
* @param int $uid
* Optional. A user id, specifying NULL returns the total space used by all
* non-temporary files.
* @param $status
* Optional. The file status to consider. The default is to only
* consider files in status FILE_STATUS_PERMANENT.
*
* @return int
* An integer containing the number of bytes used.
* {@inheritdoc}
*/
public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) {
$query = db_select($this->entityInfo['base_table'], 'f')
$query = $this->database->select($this->entityInfo['base_table'], 'f')
->condition('f.status', $status);
$query->addExpression('SUM(f.filesize)', 'filesize');
if (isset($uid)) {
@ -87,15 +28,12 @@ class FileStorageController extends DatabaseStorageControllerNG {
}
/**
* Retrieve temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
*
* @return
* A list of files to be deleted.
* {@inheritdoc}
*/
public function retrieveTemporaryFiles() {
// Use separate placeholders for the status to avoid a bug in some versions
// of PHP. See http://drupal.org/node/352956.
return db_query('SELECT fid FROM {' . $this->entityInfo['base_table'] . '} WHERE status <> :permanent AND timestamp < :timestamp', array(
return $this->database->query('SELECT fid FROM {' . $this->entityInfo['base_table'] . '} WHERE status <> :permanent AND timestamp < :timestamp', array(
':permanent' => FILE_STATUS_PERMANENT,
':timestamp' => REQUEST_TIME - DRUPAL_MAXIMUM_TEMP_FILE_AGE
));

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Contains \Drupal\file\FileStorageControllerInterface.
*/
namespace Drupal\file;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for file entity controller classes.
*/
interface FileStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Determines total disk space used by a single user or the whole filesystem.
*
* @param int $uid
* Optional. A user id, specifying NULL returns the total space used by all
* non-temporary files.
* @param int $status
* (Optional) The file status to consider. The default is to only
* consider files in status FILE_STATUS_PERMANENT.
*
* @return int
* An integer containing the number of bytes used.
*/
public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT);
/**
* Retrieve temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
*
* @return array
* A list of files to be deleted.
*/
public function retrieveTemporaryFiles();
}

View File

@ -9,6 +9,7 @@ namespace Drupal\file\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Language\Language;
use Drupal\file\FileInterface;
@ -158,4 +159,53 @@ class File extends EntityNG implements FileInterface {
$this->get('status')->value = 0;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
// Automatically detect filename if not set.
if (!isset($values['filename']) && isset($values['uri'])) {
$values['filename'] = drupal_basename($values['uri']);
}
// Automatically detect filemime if not set.
if (!isset($values['filemime']) && isset($values['uri'])) {
$values['filemime'] = file_get_mimetype($values['uri']);
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$this->timestamp = REQUEST_TIME;
$this->setSize(filesize($this->getFileUri()));
if (!isset($this->langcode->value)) {
// Default the file's language code to none, because files are language
// neutral more often than language dependent. Until we have better
// flexible settings.
// @todo See http://drupal.org/node/258785 and followups.
$this->langcode = Language::LANGCODE_NOT_SPECIFIED;
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $entity) {
// Delete all remaining references to this file.
$file_usage = file_usage()->listUsage($entity);
if (!empty($file_usage)) {
foreach ($file_usage as $module => $usage) {
file_usage()->delete($entity, $module);
}
}
// Delete the actual file. Failures due to invalid files and files that
// were already deleted are logged to watchdog but ignored, the
// corresponding file entity will be deleted.
file_unmanaged_delete($entity->getFileUri());
}
}
}

View File

@ -1,69 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\filter\FilterFormatStorageController.
*/
namespace Drupal\filter;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines the storage controller class for Filter Format entities.
*/
class FilterFormatStorageController extends ConfigStorageController {
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::preSave().
*/
protected function preSave(EntityInterface $entity) {
parent::preSave($entity);
$entity->name = trim($entity->label());
// @todo Do not save disabled filters whose properties are identical to
// all default properties.
// Determine whether the format can be cached.
// @todo This is a derived/computed definition, not configuration.
$entity->cache = TRUE;
foreach ($entity->filters() as $filter) {
if ($filter->status && !$filter->cache) {
$entity->cache = FALSE;
}
}
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
// Clear the static caches of filter_formats() and others.
filter_formats_reset();
if ($update) {
// Clear the filter cache whenever a text format is updated.
cache('filter')->deleteTags(array('filter_format' => $entity->id()));
}
else {
// Default configuration of modules and installation profiles is allowed
// to specify a list of user roles to grant access to for the new format;
// apply the defined user role permissions when a new format is inserted
// and has a non-empty $roles property.
// Note: user_role_change_permissions() triggers a call chain back into
// filter_permission() and lastly filter_formats(), so its cache must be
// reset upfront.
if (($roles = $entity->get('roles')) && $permission = filter_permission_name($entity)) {
foreach (user_roles() as $rid => $name) {
$enabled = in_array($rid, $roles, TRUE);
user_role_change_permissions($rid, array($permission => $enabled));
}
}
}
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\filter\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\filter\FilterFormatInterface;
use Drupal\filter\FilterBag;
@ -21,7 +22,7 @@ use Drupal\filter\FilterBag;
* label = @Translation("Text format"),
* module = "filter",
* controllers = {
* "storage" = "Drupal\filter\FilterFormatStorageController"
* "storage" = "Drupal\Core\Config\Entity\ConfigStorageController"
* },
* config_prefix = "filter.format",
* entity_keys = {
@ -179,4 +180,50 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$this->name = trim($this->label());
// @todo Do not save disabled filters whose properties are identical to
// all default properties.
// Determine whether the format can be cached.
// @todo This is a derived/computed definition, not configuration.
$this->cache = TRUE;
foreach ($this->filters() as $filter) {
if ($filter->status && !$filter->cache) {
$this->cache = FALSE;
}
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Clear the static caches of filter_formats() and others.
filter_formats_reset();
if ($update) {
// Clear the filter cache whenever a text format is updated.
cache('filter')->deleteTags(array('filter_format' => $this->id()));
}
else {
// Default configuration of modules and installation profiles is allowed
// to specify a list of user roles to grant access to for the new format;
// apply the defined user role permissions when a new format is inserted
// and has a non-empty $roles property.
// Note: user_role_change_permissions() triggers a call chain back into
// filter_permission() and lastly filter_formats(), so its cache must be
// reset upfront.
if (($roles = $this->get('roles')) && $permission = filter_permission_name($this)) {
foreach (user_roles() as $rid => $name) {
$enabled = in_array($rid, $roles, TRUE);
user_role_change_permissions($rid, array($permission => $enabled));
}
}
}
}
}

View File

@ -9,8 +9,6 @@ namespace Drupal\image;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityInterface;
use Drupal\image\Plugin\Core\Entity\ImageStyle;
/**
* Defines a controller class for image styles.
@ -35,73 +33,4 @@ class ImageStyleStorageController extends ConfigStorageController {
parent::attachLoad($queried_entities, $revision_id);
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
if ($update && !empty($entity->original) && $entity->{$this->idKey} !== $entity->original->{$this->idKey}) {
// The old image style name needs flushing after a rename.
image_style_flush($entity->original);
// Update field instance settings if necessary.
$this->replaceImageStyle($entity);
}
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
*/
protected function postDelete($entities) {
foreach ($entities as $style) {
// Flush cached media for the deleted style.
image_style_flush($style);
// Check whether field instance settings need to be updated.
// In case no replacement style was specified, all image fields that are
// using the deleted style are left in a broken state.
if ($new_id = $style->get('replacementID')) {
// The deleted ID is still set as originalID.
$style->set('name', $new_id);
$this->replaceImageStyle($style);
}
}
}
/**
* Update field instance settings if the image style name is changed.
*
* @param ImageStyle $style
* The image style.
*/
protected function replaceImageStyle(ImageStyle $style) {
if ($style->id() != $style->getOriginalID()) {
$instances = field_read_instances();
// Loop through all fields searching for image fields.
foreach ($instances as $instance) {
if ($instance->getField()->type == 'image') {
$view_modes = entity_get_view_modes($instance['entity_type']);
$view_modes = array('default') + array_keys($view_modes);
foreach ($view_modes as $view_mode) {
$display = entity_get_display($instance['entity_type'], $instance['bundle'], $view_mode);
$display_options = $display->getComponent($instance['field_name']);
// Check if the formatter involves an image style.
if ($display_options && $display_options['type'] == 'image' && $display_options['settings']['image_style'] == $style->getOriginalID()) {
// Update display information for any instance using the image
// style that was just deleted.
$display_options['settings']['image_style'] = $style->id();
$display->setComponent($instance['field_name'], $display_options)
->save();
}
}
$entity_form_display = entity_get_form_display($instance['entity_type'], $instance['bundle'], 'default');
$widget_configuration = $entity_form_display->getComponent($instance['field_name']);
if ($widget_configuration['settings']['preview_image_style'] == $style->getOriginalID()) {
$widget_options['settings']['preview_image_style'] = $style->id();
$entity_form_display->setComponent($instance['field_name'], $widget_options)
->save();
}
}
}
}
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\image\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\image\ImageStyleInterface;
/**
@ -68,4 +69,73 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface {
return $this->name;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
if ($update && !empty($this->original) && $this->id() !== $this->original->id()) {
// The old image style name needs flushing after a rename.
image_style_flush($this->original);
// Update field instance settings if necessary.
static::replaceImageStyle($this);
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $style) {
// Flush cached media for the deleted style.
image_style_flush($style);
// Check whether field instance settings need to be updated.
// In case no replacement style was specified, all image fields that are
// using the deleted style are left in a broken state.
if ($new_id = $style->get('replacementID')) {
// The deleted ID is still set as originalID.
$style->set('name', $new_id);
static::replaceImageStyle($style);
}
}
}
/**
* Update field instance settings if the image style name is changed.
*
* @param \Drupal\image\Plugin\Core\Entity\ImageStyle $style
* The image style.
*/
protected static function replaceImageStyle(ImageStyle $style) {
if ($style->id() != $style->getOriginalID()) {
$instances = field_read_instances();
// Loop through all fields searching for image fields.
foreach ($instances as $instance) {
if ($instance->getField()->type == 'image') {
$view_modes = entity_get_view_modes($instance['entity_type']);
$view_modes = array('default') + array_keys($view_modes);
foreach ($view_modes as $view_mode) {
$display = entity_get_display($instance['entity_type'], $instance['bundle'], $view_mode);
$display_options = $display->getComponent($instance['field_name']);
// Check if the formatter involves an image style.
if ($display_options && $display_options['type'] == 'image' && $display_options['settings']['image_style'] == $style->getOriginalID()) {
// Update display information for any instance using the image
// style that was just deleted.
$display_options['settings']['image_style'] = $style->id();
$display->setComponent($instance['field_name'], $display_options)
->save();
}
}
$entity_form_display = entity_get_form_display($instance['entity_type'], $instance['bundle'], 'default');
$widget_configuration = $entity_form_display->getComponent($instance['field_name']);
if ($widget_configuration['settings']['preview_image_style'] == $style->getOriginalID()) {
$widget_options['settings']['preview_image_style'] = $style->id();
$entity_form_display->setComponent($instance['field_name'], $widget_options)
->save();
}
}
}
}
}
}

View File

@ -9,6 +9,8 @@ namespace Drupal\menu_link;
use Symfony\Component\Routing\Route;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Provides an interface defining a menu link entity.
@ -53,4 +55,44 @@ interface MenuLinkInterface extends ContentEntityInterface {
*/
public static function buildFromRouterItem(array $item);
/**
* Returns the route_name matching a URL.
*
* @param string $link_path
* The link path to find a route name for.
*
* @return string
* The route name.
*/
public static function findRouteName($link_path);
/**
* Sets the p1 through p9 properties for a menu link entity being saved.
*
* @param \Drupal\Core\Entity\EntityInterface $parent
* A menu link entity.
*/
public function setParents(EntityInterface $parent);
/**
* Finds a possible parent for a given menu link entity.
*
* Because the parent of a given link might not exist anymore in the database,
* we apply a set of heuristics to determine a proper parent:
*
* - use the passed parent link if specified and existing.
* - else, use the first existing link down the previous link hierarchy
* - else, for system menu links (derived from hook_menu()), reparent
* based on the path hierarchy.
*
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
* Storage controller object.
* @param array $parent_candidates
* An array of menu link entities keyed by mlid.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* A menu link entity structure of the possible parent or FALSE if no valid
* parent has been found.
*/
public function findParent(EntityStorageControllerInterface $storage_controller, array $parent_candidates = array());
}

View File

@ -13,7 +13,6 @@ use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Database\Connection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Cmf\Component\Routing\RouteProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller class for menu links.
@ -21,7 +20,7 @@ use Symfony\Component\HttpFoundation\Request;
* This extends the Drupal\entity\DatabaseStorageController class, adding
* required special handling for menu_link entities.
*/
class MenuLinkStorageController extends DatabaseStorageController {
class MenuLinkStorageController extends DatabaseStorageController implements MenuLinkStorageControllerInterface {
/**
* Indicates whether the delete operation should re-parent children items.
@ -144,6 +143,8 @@ class MenuLinkStorageController extends DatabaseStorageController {
* Overrides DatabaseStorageController::save().
*/
public function save(EntityInterface $entity) {
$entity_class = $this->entityInfo['class'];
// We return SAVED_UPDATED by default because the logic below might not
// update the entity if its values haven't changed, so returning FALSE
// would be confusing in that situation.
@ -165,7 +166,7 @@ class MenuLinkStorageController extends DatabaseStorageController {
// 'presave' hook first because we want to allow modules to alter the
// entity before all the logic from our preSave() method.
$this->invokeHook('presave', $entity);
$this->preSave($entity);
$entity->preSave($this);
// If every value in $entity->original is the same in the $entity, there
// is no reason to run the update queries or clear the caches. We use
@ -178,7 +179,7 @@ class MenuLinkStorageController extends DatabaseStorageController {
if ($return) {
if (!$entity->isNew()) {
$this->resetCache(array($entity->{$this->idKey}));
$this->postSave($entity, TRUE);
$entity->postSave($this, TRUE);
$this->invokeHook('update', $entity);
}
else {
@ -186,7 +187,7 @@ class MenuLinkStorageController extends DatabaseStorageController {
$this->resetCache();
$entity->enforceIsNew(FALSE);
$this->postSave($entity, FALSE);
$entity->postSave($this, FALSE);
$this->invokeHook('insert', $entity);
}
}
@ -206,178 +207,21 @@ class MenuLinkStorageController extends DatabaseStorageController {
}
/**
* Overrides DatabaseStorageController::preSave().
* {@inheritdoc}
*/
protected function preSave(EntityInterface $entity) {
// This is the easiest way to handle the unique internal path '<front>',
// since a path marked as external does not need to match a router path.
$entity->external = (url_is_external($entity->link_path) || $entity->link_path == '<front>') ? 1 : 0;
// Try to find a parent link. If found, assign it and derive its menu.
$parent_candidates = !empty($entity->parentCandidates) ? $entity->parentCandidates : array();
$parent = $this->findParent($entity, $parent_candidates);
if ($parent) {
$entity->plid = $parent->id();
$entity->menu_name = $parent->menu_name;
}
// If no corresponding parent link was found, move the link to the top-level.
else {
$entity->plid = 0;
}
// Directly fill parents for top-level links.
if ($entity->plid == 0) {
$entity->p1 = $entity->id();
for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
$parent_property = "p$i";
$entity->$parent_property = 0;
}
$entity->depth = 1;
}
// Otherwise, ensure that this link's depth is not beyond the maximum depth
// and fill parents based on the parent link.
else {
if ($entity->has_children && $entity->original) {
$limit = MENU_MAX_DEPTH - $this->findChildrenRelativeDepth($entity->original) - 1;
}
else {
$limit = MENU_MAX_DEPTH - 1;
}
if ($parent->depth > $limit) {
return FALSE;
}
$entity->depth = $parent->depth + 1;
$this->setParents($entity, $parent);
}
// Need to check both plid and menu_name, since plid can be 0 in any menu.
if (isset($entity->original) && ($entity->plid != $entity->original->plid || $entity->menu_name != $entity->original->menu_name)) {
$this->moveChildren($entity, $entity->original);
}
// Find the router_path.
if (empty($entity->router_path) || empty($entity->original) || (isset($entity->original) && $entity->original->link_path != $entity->link_path)) {
if ($entity->external) {
$entity->router_path = '';
}
else {
// Find the router path which will serve this path.
$entity->parts = explode('/', $entity->link_path, MENU_MAX_PARTS);
$entity->router_path = _menu_find_router_path($entity->link_path);
}
}
// Find the route_name.
if (!isset($entity->route_name)) {
$entity->route_name = $this->findRouteName($entity->link_path);
}
}
/**
* Returns the route_name matching a URL.
*
* @param string $link_path
* The link path to find a route name for.
*
* @return string
* The route name.
*/
protected function findRouteName($link_path) {
// Look up the route_name used for the given path.
$request = Request::create('/' . $link_path);
$request->attributes->set('system_path', $link_path);
try {
// Use router.dynamic instead of router, because router will call the
// legacy router which will call hook_menu() and you will get back to
// this method.
$result = \Drupal::service('router.dynamic')->matchRequest($request);
return isset($result['_route']) ? $result['_route'] : '';
}
catch (\Exception $e) {
return '';
}
}
/**
* DatabaseStorageController::postSave().
*/
function postSave(EntityInterface $entity, $update) {
// Check the has_children status of the parent.
$this->updateParentalStatus($entity);
menu_cache_clear($entity->menu_name);
if (isset($entity->original) && $entity->menu_name != $entity->original->menu_name) {
menu_cache_clear($entity->original->menu_name);
}
// Now clear the cache.
_menu_clear_page_cache();
}
/**
* Sets an internal flag that allows us to prevent the reparenting operations
* executed during deletion.
*
* @param bool $value
*/
public function preventReparenting($value = FALSE) {
public function setPreventReparenting($value = FALSE) {
$this->preventReparenting = $value;
}
/**
* Overrides DatabaseStorageController::preDelete().
* {@inheritdoc}
*/
protected function preDelete($entities) {
// Nothing to do if we don't want to reparent children.
if ($this->preventReparenting) {
return;
}
foreach ($entities as $entity) {
// Children get re-attached to the item's parent.
if ($entity->has_children) {
$children = $this->loadByProperties(array('plid' => $entity->plid));
foreach ($children as $child) {
$child->plid = $entity->plid;
$this->save($child);
}
}
}
public function getPreventReparenting() {
return $this->preventReparenting;
}
/**
* Overrides DatabaseStorageController::postDelete().
*/
protected function postDelete($entities) {
$affected_menus = array();
// Update the has_children status of the parent.
foreach ($entities as $entity) {
if (!$this->preventReparenting) {
$this->updateParentalStatus($entity);
}
// Store all menu names for which we need to clear the cache.
if (!isset($affected_menus[$entity->menu_name])) {
$affected_menus[$entity->menu_name] = $entity->menu_name;
}
}
foreach ($affected_menus as $menu_name) {
menu_cache_clear($menu_name);
}
_menu_clear_page_cache();
}
/**
* Loads updated and customized menu links for specific router paths.
*
* Note that this is a low-level method and it doesn't return fully populated
* menu link entities. (e.g. no fields are attached)
*
* @param array $router_paths
* An array of router paths.
*
* @return array
* An array of menu link objects indexed by their ids.
* {@inheritdoc}
*/
public function loadUpdatedCustomized(array $router_paths) {
$query = parent::buildQuery(NULL);
@ -403,10 +247,7 @@ class MenuLinkStorageController extends DatabaseStorageController {
}
/**
* Loads system menu link as needed by system_get_module_admin_tasks().
*
* @return array
* An array of menu link entities indexed by their IDs.
* {@inheritdoc}
*/
public function loadModuleAdminTasks() {
$query = $this->buildQuery(NULL);
@ -422,12 +263,9 @@ class MenuLinkStorageController extends DatabaseStorageController {
}
/**
* Checks and updates the 'has_children' property for the parent of a link.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
* {@inheritdoc}
*/
protected function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
// If plid == 0, there is nothing to update.
if ($entity->plid) {
// Check if at least one visible child exists in the table.
@ -451,125 +289,7 @@ class MenuLinkStorageController extends DatabaseStorageController {
}
/**
* Finds a possible parent for a given menu link entity.
*
* Because the parent of a given link might not exist anymore in the database,
* we apply a set of heuristics to determine a proper parent:
*
* - use the passed parent link if specified and existing.
* - else, use the first existing link down the previous link hierarchy
* - else, for system menu links (derived from hook_menu()), reparent
* based on the path hierarchy.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
* @param array $parent_candidates
* An array of menu link entities keyed by mlid.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* A menu link entity structure of the possible parent or FALSE if no valid
* parent has been found.
*/
protected function findParent(EntityInterface $entity, array $parent_candidates = array()) {
$parent = FALSE;
// This item is explicitely top-level, skip the rest of the parenting.
if (isset($entity->plid) && empty($entity->plid)) {
return $parent;
}
// If we have a parent link ID, try to use that.
$candidates = array();
if (isset($entity->plid)) {
$candidates[] = $entity->plid;
}
// Else, if we have a link hierarchy try to find a valid parent in there.
if (!empty($entity->depth) && $entity->depth > 1) {
for ($depth = $entity->depth - 1; $depth >= 1; $depth--) {
$parent_property = "p$depth";
$candidates[] = $entity->$parent_property;
}
}
foreach ($candidates as $mlid) {
if (isset($parent_candidates[$mlid])) {
$parent = $parent_candidates[$mlid];
}
else {
$parent = $this->load(array($mlid));
$parent = reset($parent);
}
if ($parent) {
return $parent;
}
}
// If everything else failed, try to derive the parent from the path
// hierarchy. This only makes sense for links derived from menu router
// items (ie. from hook_menu()).
if ($entity->module == 'system') {
// Find the parent - it must be unique.
$parent_path = $entity->link_path;
do {
$parent = FALSE;
$parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
$query = \Drupal::entityQuery($this->entityType);
$query
->condition('mlid', $entity->id(), '<>')
->condition('module', 'system')
// We always respect the link's 'menu_name'; inheritance for router
// items is ensured in _menu_router_build().
->condition('menu_name', $entity->menu_name)
->condition('link_path', $parent_path);
$result = $query->execute();
// Only valid if we get a unique result.
if (count($result) == 1) {
$parent = $this->load($result);
$parent = reset($parent);
}
} while ($parent === FALSE && $parent_path);
}
return $parent;
}
/**
* Sets the p1 through p9 properties for a menu link entity being saved.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
* @param \Drupal\Core\Entity\EntityInterface $parent
* A menu link entity.
*/
protected function setParents(EntityInterface $entity, EntityInterface $parent) {
$i = 1;
while ($i < $entity->depth) {
$p = 'p' . $i++;
$entity->{$p} = $parent->{$p};
}
$p = 'p' . $i++;
// The parent (p1 - p9) corresponding to the depth always equals the mlid.
$entity->{$p} = $entity->id();
while ($i <= MENU_MAX_DEPTH) {
$p = 'p' . $i++;
$entity->{$p} = 0;
}
}
/**
* Finds the depth of an item's children relative to its depth.
*
* For example, if the item has a depth of 2 and the maximum of any child in
* the menu link tree is 5, the relative depth is 3.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*
* @return int
* The relative depth, or zero.
* {@inheritdoc}
*/
public function findChildrenRelativeDepth(EntityInterface $entity) {
// @todo Since all we need is a specific field from the base table, does it
@ -593,15 +313,9 @@ class MenuLinkStorageController extends DatabaseStorageController {
}
/**
* Updates the children of a menu link that is being moved.
*
* The menu name, parents (p1 - p6), and depth are updated for all children of
* the link, and the has_children status of the previous parent is updated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
* {@inheritdoc}
*/
protected function moveChildren(EntityInterface $entity) {
public function moveChildren(EntityInterface $entity) {
$query = $this->database->update($this->entityInfo['base_table']);
$query->fields(array('menu_name' => $entity->menu_name));
@ -645,10 +359,7 @@ class MenuLinkStorageController extends DatabaseStorageController {
}
/**
* Returns the number of menu links from a menu.
*
* @param string $menu_name
* The unique name of a menu.
* {@inheritdoc}
*/
public function countMenuLinks($menu_name) {
$query = \Drupal::entityQuery($this->entityType);
@ -657,4 +368,34 @@ class MenuLinkStorageController extends DatabaseStorageController {
->count();
return $query->execute();
}
/**
* {@inheritdoc}
*/
public function getParentFromHierarchy(EntityInterface $entity) {
$parent_path = $entity->link_path;
do {
$parent = FALSE;
$parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
$query = \Drupal::entityQuery($this->entityType);
$query
->condition('mlid', $entity->id(), '<>')
->condition('module', 'system')
// We always respect the link's 'menu_name'; inheritance for router
// items is ensured in _menu_router_build().
->condition('menu_name', $entity->menu_name)
->condition('link_path', $parent_path);
$result = $query->execute();
// Only valid if we get a unique result.
if (count($result) == 1) {
$parent = $this->load($result);
$parent = reset($parent);
}
} while ($parent === FALSE && $parent_path);
return $parent;
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkStorageControllerInterface.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for menu link entity controller classes.
*/
interface MenuLinkStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Sets an internal flag that allows us to prevent the reparenting operations
* executed during deletion.
*
* @param bool $value
* TRUE if reparenting should be allowed, FALSE if it should be prevented.
*/
public function setPreventReparenting($value = FALSE);
/**
* Gets value of internal flag that allows/prevents reparenting operations
* executed during deletion.
*
* @return bool
* TRUE if reparenting is allowed, FALSE if it is prevented.
*/
public function getPreventReparenting();
/**
* Loads updated and customized menu links for specific router paths.
*
* Note that this is a low-level method and it doesn't return fully populated
* menu link entities. (e.g. no fields are attached)
*
* @param array $router_paths
* An array of router paths.
*
* @return array
* An array of menu link objects indexed by their ids.
*/
public function loadUpdatedCustomized(array $router_paths);
/**
* Loads system menu link as needed by system_get_module_admin_tasks().
*
* @return array
* An array of menu link entities indexed by their IDs.
*/
public function loadModuleAdminTasks();
/**
* Checks and updates the 'has_children' property for the parent of a link.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE);
/**
* Finds the depth of an item's children relative to its depth.
*
* For example, if the item has a depth of 2 and the maximum of any child in
* the menu link tree is 5, the relative depth is 3.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*
* @return int
* The relative depth, or zero.
*/
public function findChildrenRelativeDepth(EntityInterface $entity);
/**
* Updates the children of a menu link that is being moved.
*
* The menu name, parents (p1 - p6), and depth are updated for all children of
* the link, and the has_children status of the previous parent is updated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
public function moveChildren(EntityInterface $entity);
/**
* Returns the number of menu links from a menu.
*
* @param string $menu_name
* The unique name of a menu.
*/
public function countMenuLinks($menu_name);
/**
* Tries to derive menu link's parent from the path hierarchy.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* A menu link entity or FALSE if not valid parent was found.
*/
public function getParentFromHierarchy(EntityInterface $entity);
}

View File

@ -9,9 +9,13 @@ namespace Drupal\menu_link\Plugin\Core\Entity;
use Drupal\menu_link\MenuLinkInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity;
@ -372,4 +376,217 @@ class MenuLink extends Entity implements \ArrayAccess, MenuLinkInterface {
public function offsetUnset($offset) {
unset($this->{$offset});
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
// Nothing to do if we don't want to reparent children.
if ($storage_controller->getPreventReparenting()) {
return;
}
foreach ($entities as $entity) {
// Children get re-attached to the item's parent.
if ($entity->has_children) {
$children = $storage_controller->loadByProperties(array('plid' => $entity->plid));
foreach ($children as $child) {
$child->plid = $entity->plid;
$storage_controller->save($child);
}
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
$affected_menus = array();
// Update the has_children status of the parent.
foreach ($entities as $entity) {
if (!$storage_controller->getPreventReparenting()) {
$storage_controller->updateParentalStatus($entity);
}
// Store all menu names for which we need to clear the cache.
if (!isset($affected_menus[$entity->menu_name])) {
$affected_menus[$entity->menu_name] = $entity->menu_name;
}
}
foreach ($affected_menus as $menu_name) {
menu_cache_clear($menu_name);
}
_menu_clear_page_cache();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
// This is the easiest way to handle the unique internal path '<front>',
// since a path marked as external does not need to match a router path.
$this->external = (url_is_external($this->link_path) || $this->link_path == '<front>') ? 1 : 0;
// Try to find a parent link. If found, assign it and derive its menu.
$parent_candidates = !empty($this->parentCandidates) ? $this->parentCandidates : array();
$parent = $this->findParent($storage_controller, $parent_candidates);
if ($parent) {
$this->plid = $parent->id();
$this->menu_name = $parent->menu_name;
}
// If no corresponding parent link was found, move the link to the top-level.
else {
$this->plid = 0;
}
// Directly fill parents for top-level links.
if ($this->plid == 0) {
$this->p1 = $this->id();
for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
$parent_property = "p$i";
$this->{$parent_property} = 0;
}
$this->depth = 1;
}
// Otherwise, ensure that this link's depth is not beyond the maximum depth
// and fill parents based on the parent link.
else {
if ($this->has_children && $this->original) {
$limit = MENU_MAX_DEPTH - $storage_controller->findChildrenRelativeDepth($this->original) - 1;
}
else {
$limit = MENU_MAX_DEPTH - 1;
}
if ($parent->depth > $limit) {
return FALSE;
}
$this->depth = $parent->depth + 1;
$this->setParents($parent);
}
// Need to check both plid and menu_name, since plid can be 0 in any menu.
if (isset($this->original) && ($this->plid != $this->original->plid || $this->menu_name != $this->original->menu_name)) {
$storage_controller->moveChildren($this, $this->original);
}
// Find the router_path.
if (empty($this->router_path) || empty($this->original) || (isset($this->original) && $this->original->link_path != $this->link_path)) {
if ($this->external) {
$this->router_path = '';
}
else {
// Find the router path which will serve this path.
$this->parts = explode('/', $this->link_path, MENU_MAX_PARTS);
$this->router_path = _menu_find_router_path($this->link_path);
}
}
// Find the route_name.
if (!isset($this->route_name)) {
$this->route_name = $this::findRouteName($this->link_path);
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Check the has_children status of the parent.
$storage_controller->updateParentalStatus($this);
menu_cache_clear($this->menu_name);
if (isset($this->original) && $this->menu_name != $this->original->menu_name) {
menu_cache_clear($this->original->menu_name);
}
// Now clear the cache.
_menu_clear_page_cache();
}
/**
* {@inheritdoc}
*/
public static function findRouteName($link_path) {
// Look up the route_name used for the given path.
$request = Request::create('/' . $link_path);
$request->attributes->set('system_path', $link_path);
try {
// Use router.dynamic instead of router, because router will call the
// legacy router which will call hook_menu() and you will get back to
// this method.
$result = \Drupal::service('router.dynamic')->matchRequest($request);
return isset($result['_route']) ? $result['_route'] : '';
}
catch (\Exception $e) {
return '';
}
}
/**
* {@inheritdoc}
*/
public function setParents(EntityInterface $parent) {
$i = 1;
while ($i < $this->depth) {
$p = 'p' . $i++;
$this->{$p} = $parent->{$p};
}
$p = 'p' . $i++;
// The parent (p1 - p9) corresponding to the depth always equals the mlid.
$this->{$p} = $this->id();
while ($i <= MENU_MAX_DEPTH) {
$p = 'p' . $i++;
$this->{$p} = 0;
}
}
/**
* {@inheritdoc}
*/
public function findParent(EntityStorageControllerInterface $storage_controller, array $parent_candidates = array()) {
$parent = FALSE;
// This item is explicitely top-level, skip the rest of the parenting.
if (isset($this->plid) && empty($this->plid)) {
return $parent;
}
// If we have a parent link ID, try to use that.
$candidates = array();
if (isset($this->plid)) {
$candidates[] = $this->plid;
}
// Else, if we have a link hierarchy try to find a valid parent in there.
if (!empty($this->depth) && $this->depth > 1) {
for ($depth = $this->depth - 1; $depth >= 1; $depth--) {
$parent_property = "p$depth";
$candidates[] = $this->$parent_property;
}
}
foreach ($candidates as $mlid) {
if (isset($parent_candidates[$mlid])) {
$parent = $parent_candidates[$mlid];
}
else {
$parent = $storage_controller->load(array($mlid));
$parent = reset($parent);
}
if ($parent) {
return $parent;
}
}
// If everything else failed, try to derive the parent from the path
// hierarchy. This only makes sense for links derived from menu router
// items (ie. from hook_menu()).
if ($this->module == 'system') {
$parent = $storage_controller->getParentFromHierarchy($this);
}
return $parent;
}
}

View File

@ -103,7 +103,7 @@ function menu_link_delete_multiple(array $mlids, $force = FALSE, $prevent_repare
else {
$entities = $controller->load($mlids);
}
$controller->preventReparenting($prevent_reparenting);
$controller->setPreventReparenting($prevent_reparenting);
$controller->delete($entities);
}

View File

@ -123,63 +123,6 @@ class NodeStorageController extends DatabaseStorageControllerNG {
return $record;
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
*/
protected function preSave(EntityInterface $node) {
// Before saving the node, set changed and revision times.
$node->changed->value = REQUEST_TIME;
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
*/
protected function preSaveRevision(\stdClass $record, EntityInterface $entity) {
if ($entity->isNewRevision()) {
// When inserting either a new node or a new node revision, $node->log
// must be set because {node_field_revision}.log is a text column and
// therefore cannot have a default value. However, it might not be set at
// this point (for example, if the user submitting a node form does not
// have permission to create revisions), so we ensure that it is at least
// an empty string in that case.
// @todo Make the {node_field_revision}.log column nullable so that we
// can remove this check.
if (!isset($record->log)) {
$record->log = '';
}
}
elseif (isset($entity->original) && (!isset($record->log) || $record->log === '')) {
// If we are updating an existing node without adding a new revision, we
// need to make sure $entity->log is reset whenever it is empty.
// Therefore, this code allows us to avoid clobbering an existing log
// entry with an empty one.
$record->log = $entity->original->log;
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
*/
public function postSave(EntityInterface $node, $update) {
// Update the node access table for this node, but only if it is the
// default revision. There's no need to delete existing records if the node
// is new.
if ($node->isDefaultRevision()) {
node_access_acquire_grants($node->getBCEntity(), $update);
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preDelete().
*/
public function preDelete($entities) {
if (module_exists('search')) {
foreach ($entities as $id => $entity) {
search_reindex($entity->nid->value, 'node');
}
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
*/

View File

@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\node\NodeInterface;
@ -241,6 +242,52 @@ class Node extends EntityNG implements NodeInterface {
return $this->get('vid')->value;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
// Before saving the node, set changed and revision times.
$this->changed->value = REQUEST_TIME;
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
if ($this->newRevision) {
// When inserting either a new node or a new node revision, $node->log
// must be set because {node_field_revision}.log is a text column and
// therefore cannot have a default value. However, it might not be set at
// this point (for example, if the user submitting a node form does not
// have permission to create revisions), so we ensure that it is at least
// an empty string in that case.
// @todo Make the {node_field_revision}.log column nullable so that we
// can remove this check.
if (!isset($record->log)) {
$record->log = '';
}
}
elseif (isset($this->original) && (!isset($record->log) || $record->log === '')) {
// If we are updating an existing node without adding a new revision, we
// need to make sure $entity->log is reset whenever it is empty.
// Therefore, this code allows us to avoid clobbering an existing log
// entry with an empty one.
$record->log = $this->original->log;
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Update the node access table for this node, but only if it is the
// default revision. There's no need to delete existing records if the node
// is new.
if ($this->isDefaultRevision()) {
node_access_acquire_grants($this->getBCEntity(), $update);
}
}
/**
* {@inheritdoc}
*/
@ -252,4 +299,16 @@ class Node extends EntityNG implements NodeInterface {
return $this->bcEntity;
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
if (module_exists('search')) {
foreach ($entities as $id => $entity) {
search_reindex($entity->nid->value, 'node');
}
}
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\node_test\NodeTest.
*/
namespace Drupal\node_test;
use Drupal\node\Plugin\Core\Entity\Node;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Overrides the default node entity class for testing.
*/
class NodeTest extends Node {
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
// Allow test nodes to specify their updated ('changed') time.
}
}

View File

@ -1,25 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\node_test\NodeTestStorageController.
*/
namespace Drupal\node_test;
use Drupal\node\NodeStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a test storage controller for nodes.
*/
class NodeTestStorageController extends NodeStorageController {
/**
* {@inheritdoc}
*/
protected function preSave(EntityInterface $node) {
// Allow test nodes to specify their updated ('changed') time.
}
}

View File

@ -186,6 +186,6 @@ function node_test_node_insert(EntityInterface $node) {
*/
function node_test_entity_info_alter(&$entity_info) {
if (Drupal::state()->get('node_test.storage_controller')) {
$entity_info['node']['controllers']['storage'] = 'Drupal\node_test\NodeTestStorageController';
$entity_info['node']['class'] = 'Drupal\node_test\NodeTest';
}
}

View File

@ -95,7 +95,7 @@ class SetDelete extends ConfirmFormBase implements ControllerInterface {
// Find out how many users are directly assigned to this shortcut set, and
// make a message.
$number = $this->database->query('SELECT COUNT(*) FROM {shortcut_set_users} WHERE set_name = :name', array(':name' => $this->shortcut->id()))->fetchField();
$number = \Drupal::entityManager()->getStorageController('shortcut')->countAssignedUsers($shortcut);
$info = '';
if ($number) {
$info .= '<p>' . format_plural($number,

View File

@ -10,6 +10,7 @@ namespace Drupal\shortcut\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\shortcut\ShortcutInterface;
/**
@ -79,4 +80,65 @@ class Shortcut extends ConfigEntityBase implements ShortcutInterface {
);
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageControllerInterface $storage_controller) {
// Generate menu-compatible set name.
if (!$this->getOriginalID()) {
// Save a new shortcut set with links copied from the user's default set.
$default_set = shortcut_default_set();
// Generate a name to have no collisions with menu.
// Size of menu_name is 32 so id could be 23 = 32 - strlen('shortcut-').
$id = substr($this->id(), 0, 23);
$this->set('id', $id);
if ($default_set->id() != $id) {
foreach ($default_set->links as $link) {
$link = $link->createDuplicate();
$link->enforceIsNew();
$link->menu_name = $id;
$link->save();
$this->links[$link->uuid()] = $link;
}
}
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
// Just store the UUIDs.
foreach ($this->links as $uuid => $link) {
$this->links[$uuid] = $uuid;
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
foreach ($this->links as $uuid) {
if ($menu_link = entity_load_by_uuid('menu_link', $uuid)) {
// Do not specifically associate these links with the shortcut module,
// since other modules may make them editable via the menu system.
// However, we do need to specify the correct menu name.
$menu_link->menu_name = 'shortcut-' . $this->id();
$menu_link->plid = 0;
$menu_link->save();
}
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $entity) {
$storage_controller->deleteAssignedShortcutSets($entity);
// Next, delete the menu links for this set.
menu_delete_links('shortcut-' . $entity->id());
}
}
}

View File

@ -8,12 +8,12 @@
namespace Drupal\shortcut;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
use Drupal\shortcut\Plugin\Core\Entity\Shortcut;
/**
* Defines a storage controller for shortcut entities.
*/
class ShortcutStorageController extends ConfigStorageController {
class ShortcutStorageController extends ConfigStorageController implements ShortcutStorageControllerInterface {
/**
* Overrides \Drupal\config\ConfigStorageController::attachLoad().
@ -30,80 +30,51 @@ class ShortcutStorageController extends ConfigStorageController {
}
/**
* Overrides \Drupal\config\ConfigStorageController::create().
* {@inheritdoc}
*/
public function create(array $values) {
$entity = parent::create($values);
// Generate menu-compatible set name.
if (!$entity->getOriginalID()) {
// Save a new shortcut set with links copied from the user's default set.
$default_set = shortcut_default_set();
// Generate a name to have no collisions with menu.
// Size of menu_name is 32 so id could be 23 = 32 - strlen('shortcut-').
$id = substr($entity->id(), 0, 23);
$entity->set('id', $id);
if ($default_set->id() != $id) {
foreach ($default_set->links as $link) {
$link = $link->createDuplicate();
$link->enforceIsNew();
$link->menu_name = $id;
$link->save();
$entity->links[$link->uuid()] = $link;
}
}
}
return $entity;
public function deleteAssignedShortcutSets(Shortcut $entity) {
// First, delete any user assignments for this set, so that each of these
// users will go back to using whatever default set applies.
db_delete('shortcut_set_users')
->condition('set_name', $entity->id())
->execute();
}
/**
* Overrides \Drupal\config\ConfigStorageController::preSave().
* {@inheritdoc}
*/
public function preSave(EntityInterface $entity) {
// Just store the UUIDs.
foreach ($entity->links as $uuid => $link) {
$entity->links[$uuid] = $uuid;
}
parent::preSave($entity);
public function assignUser($shortcut_set, $account) {
db_merge('shortcut_set_users')
->key(array('uid' => $account->uid))
->fields(array('set_name' => $shortcut_set->id()))
->execute();
drupal_static_reset('shortcut_current_displayed_set');
}
/**
* Overrides \Drupal\config\ConfigStorageController::postSave().
* {@inheritdoc}
*/
function postSave(EntityInterface $entity, $update) {
// Process links in shortcut set.
foreach ($entity->links as $uuid) {
if ($menu_link = entity_load_by_uuid('menu_link', $uuid)) {
// Do not specifically associate these links with the shortcut module,
// since other modules may make them editable via the menu system.
// However, we do need to specify the correct menu name.
$menu_link->menu_name = 'shortcut-' . $entity->id();
$menu_link->plid = 0;
$menu_link->save();
}
}
parent::postSave($entity, $update);
public function unassignUser($account) {
$deleted = db_delete('shortcut_set_users')
->condition('uid', $account->uid)
->execute();
return (bool) $deleted;
}
/**
* Overrides \Drupal\Core\Entity\ConfigStorageController::preDelete().
* {@inheritdoc}
*/
protected function preDelete($entities) {
foreach ($entities as $entity) {
// First, delete any user assignments for this set, so that each of these
// users will go back to using whatever default set applies.
db_delete('shortcut_set_users')
->condition('set_name', $entity->id())
->execute();
// Next, delete the menu links for this set.
menu_delete_links('shortcut-' . $entity->id());
}
parent::preDelete($entities);
public function getAssignedToUser($account) {
$query = db_select('shortcut_set_users', 'ssu');
$query->fields('ssu', array('set_name'));
$query->condition('ssu.uid', $account->uid);
return $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function countAssignedUsers(Shortcut $shortcut) {
return db_query('SELECT COUNT(*) FROM {shortcut_set_users} WHERE set_name = :name', array(':name' => $shortcut->id()))->fetchField();
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\shortcut\ShortcutStorageControllerInterface.
*/
namespace Drupal\shortcut;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\shortcut\Plugin\Core\Entity\Shortcut;
/**
* Defines a common interface for shortcut entity controller classes.
*/
interface ShortcutStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Assigns a user to a particular shortcut set.
*
* @param \Drupal\shortcut\Plugin\Core\Entity\Shortcut $shortcut_set
* An object representing the shortcut set.
* @param $account
* A user account that will be assigned to use the set.
*/
public function assignUser($shortcut_set, $account);
/**
* Unassigns a user from any shortcut set they may have been assigned to.
*
* The user will go back to using whatever default set applies.
*
* @param $account
* A user account that will be removed from the shortcut set assignment.
*
* @return bool
* TRUE if the user was previously assigned to a shortcut set and has been
* successfully removed from it. FALSE if the user was already not assigned
* to any set.
*/
public function unassignUser($account);
/**
* Delete shortcut sets assigned to users.
*
* @param \Drupal\shortcut\Plugin\Core\Entity\Shortcut $entity
* Delete the user assigned sets belonging to this shortcut.
*/
public function deleteAssignedShortcutSets(Shortcut $entity);
/**
* Get the name of the set assigned to this user.
*
* @param \Drupal\user\Plugin\Core\Entity\User
* The user account.
*
* @return string
* The name of the shortcut set assigned to this user.
*/
public function getAssignedToUser($account);
/**
* Get the number of users who have this set assigned to them.
*
* @param \Drupal\shortcut\Plugin\Core\Entity\Shortcut $shortcut
* The shortcut to count the users assigned to.
*
* @return int
* The number of users who have this set assigned to them.
*/
public function countAssignedUsers(Shortcut $shortcut);
}

View File

@ -311,11 +311,9 @@ function shortcut_set_reset_link_weights(&$shortcut_set) {
* A user account that will be assigned to use the set.
*/
function shortcut_set_assign_user($shortcut_set, $account) {
db_merge('shortcut_set_users')
->key(array('uid' => $account->uid))
->fields(array('set_name' => $shortcut_set->id()))
->execute();
drupal_static_reset('shortcut_current_displayed_set');
Drupal::entityManager()
->getStorageController('shortcut')
->assignUser($shortcut_set, $account);
}
/**
@ -332,10 +330,9 @@ function shortcut_set_assign_user($shortcut_set, $account) {
* to any set.
*/
function shortcut_set_unassign_user($account) {
$deleted = db_delete('shortcut_set_users')
->condition('uid', $account->uid)
->execute();
return (bool) $deleted;
return (bool) Drupal::entityManager()
->getStorageController('shortcut')
->unassignUser($account);
}
/**
@ -362,10 +359,9 @@ function shortcut_current_displayed_set($account = NULL) {
}
// If none was found, try to find a shortcut set that is explicitly assigned
// to this user.
$query = db_select('shortcut_set_users', 'ssu');
$query->fields('ssu', array('set_name'));
$query->condition('ssu.uid', $account->uid);
$shortcut_set_name = $query->execute()->fetchField();
$shortcut_set_name = Drupal::entityManager()
->getStorageController('shortcut')
->getAssignedToUser($account);
if ($shortcut_set_name) {
$shortcut_set = shortcut_set_load($shortcut_set_name);
}

View File

@ -1,32 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\system\ActionStorageController.
*/
namespace Drupal\system;
use Drupal\Core\Action\ConfigurableActionInterface;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines the storage controller class for Action entities.
*/
class ActionStorageController extends ConfigStorageController {
/**
* {@inheritdoc}
*/
protected function preSave(EntityInterface $entity) {
parent::preSave($entity);
$plugin = $entity->getPlugin();
// If this plugin has any configuration, ensure that it is set.
if ($plugin instanceof ConfigurableActionInterface) {
$entity->set('configuration', $plugin->getConfiguration());
}
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\system\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\system\ActionConfigEntityInterface;
use Drupal\Core\Action\ActionBag;
use Drupal\Core\Action\ConfigurableActionInterface;
@ -22,7 +23,7 @@ use Drupal\Core\Action\ConfigurableActionInterface;
* label = @Translation("Action"),
* module = "system",
* controllers = {
* "storage" = "Drupal\system\ActionStorageController"
* "storage" = "Drupal\Core\Config\Entity\ConfigStorageController"
* },
* config_prefix = "action.action",
* entity_keys = {
@ -176,4 +177,15 @@ class Action extends ConfigEntityBase implements ActionConfigEntityInterface {
return $properties;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$plugin = $this->getPlugin();
// If this plugin has any configuration, ensure that it is set.
if ($plugin instanceof ConfigurableActionInterface) {
$this->set('configuration', $plugin->getConfiguration());
}
}
}

View File

@ -9,6 +9,7 @@ namespace Drupal\taxonomy\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Language\Language;
use Drupal\taxonomy\TermInterface;
@ -151,4 +152,46 @@ class Term extends EntityNG implements TermInterface {
unset($this->description);
unset($this->parent);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
// See if any of the term's children are about to be become orphans.
$orphans = array();
foreach (array_keys($entities) as $tid) {
if ($children = taxonomy_term_load_children($tid)) {
foreach ($children as $child) {
// If the term has multiple parents, we don't delete it.
$parents = taxonomy_term_load_parents($child->id());
// Because the parent has already been deleted, the parent count might
// be 0.
if (count($parents) <= 1) {
$orphans[] = $child->id();
}
}
}
}
// Delete term hierarchy information after looking up orphans but before
// deleting them so that their children/parent information is consistent.
$storage_controller->deleteTermHierarchy(array_keys($entities));
if (!empty($orphans)) {
entity_delete_multiple('taxonomy_term', $orphans);
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Only change the parents if a value is set, keep the existing values if
// not.
if (isset($this->parent->value)) {
$storage_controller->deleteTermHierarchy(array($this->id()));
$storage_controller->updateTermHierarchy($this);
}
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\taxonomy\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\taxonomy\VocabularyInterface;
@ -104,4 +105,78 @@ class Vocabulary extends ConfigEntityBase implements VocabularyInterface {
);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
if (!$update) {
entity_invoke_bundle_hook('create', 'taxonomy_term', $this->id());
}
elseif ($this->getOriginalID() != $this->id()) {
// Reflect machine name changes in the definitions of existing 'taxonomy'
// fields.
$fields = field_read_fields();
foreach ($fields as $field_name => $field) {
$update_field = FALSE;
if ($field['type'] == 'taxonomy_term_reference') {
foreach ($field['settings']['allowed_values'] as $key => &$value) {
if ($value['vocabulary'] == $this->getOriginalID()) {
$value['vocabulary'] = $this->id();
$update_field = TRUE;
}
}
if ($update_field) {
field_update_field($field);
}
}
}
// Update bundles.
entity_invoke_bundle_hook('rename', 'taxonomy_term', $this->getOriginalID(), $this->id());
}
$storage_controller->resetCache($update ? array($this->getOriginalID()) : array());
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
// Only load terms without a parent, child terms will get deleted too.
entity_delete_multiple('taxonomy_term', $storage_controller->getToplevelTids(array_keys($entities)));
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
$vocabularies = array();
foreach ($entities as $vocabulary) {
$vocabularies[$vocabulary->id()] = $vocabulary->id();
}
// Load all Taxonomy module fields and delete those which use only this
// vocabulary.
$taxonomy_fields = field_read_fields(array('module' => 'taxonomy'));
foreach ($taxonomy_fields as $field_name => $taxonomy_field) {
$modified_field = FALSE;
// Term reference fields may reference terms from more than one
// vocabulary.
foreach ($taxonomy_field['settings']['allowed_values'] as $key => $allowed_value) {
if (isset($vocabularies[$allowed_value['vocabulary']])) {
unset($taxonomy_field['settings']['allowed_values'][$key]);
$modified_field = TRUE;
}
}
if ($modified_field) {
if (empty($taxonomy_field['settings']['allowed_values'])) {
field_delete_field($field_name);
}
else {
// Update the field definition with the new allowed values.
field_update_field($taxonomy_field);
}
}
}
// Reset caches.
$storage_controller->resetCache(array_keys($vocabularies));
}
}

View File

@ -14,7 +14,7 @@ use Drupal\Core\Entity\DatabaseStorageControllerNG;
/**
* Defines a Controller class for taxonomy terms.
*/
class TermStorageController extends DatabaseStorageControllerNG {
class TermStorageController extends DatabaseStorageControllerNG implements TermStorageControllerInterface {
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::create().
@ -43,61 +43,6 @@ class TermStorageController extends DatabaseStorageControllerNG {
parent::buildPropertyQuery($entity_query, $values);
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
*/
protected function postDelete($entities) {
// See if any of the term's children are about to be become orphans.
$orphans = array();
foreach (array_keys($entities) as $tid) {
if ($children = taxonomy_term_load_children($tid)) {
foreach ($children as $child) {
// If the term has multiple parents, we don't delete it.
$parents = taxonomy_term_load_parents($child->id());
// Because the parent has already been deleted, the parent count might
// be 0.
if (count($parents) <= 1) {
$orphans[] = $child->id();
}
}
}
}
// Delete term hierarchy information after looking up orphans but before
// deleting them so that their children/parent information is consistent.
db_delete('taxonomy_term_hierarchy')
->condition('tid', array_keys($entities))
->execute();
if (!empty($orphans)) {
entity_delete_multiple('taxonomy_term', $orphans);
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
// Only change the parents if a value is set, keep the existing values if
// not.
if (isset($entity->parent->value)) {
db_delete('taxonomy_term_hierarchy')
->condition('tid', $entity->id())
->execute();
$query = db_insert('taxonomy_term_hierarchy')
->fields(array('tid', 'parent'));
foreach ($entity->parent as $parent) {
$query->values(array(
'tid' => $entity->id(),
'parent' => (int) $parent->value,
));
}
$query->execute();
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::resetCache().
*/
@ -167,4 +112,30 @@ class TermStorageController extends DatabaseStorageControllerNG {
);
return $properties;
}
/**
* {@inheritdoc}
*/
public function deleteTermHierarchy($tids) {
$this->database->delete('taxonomy_term_hierarchy')
->condition('tid', $tids)
->execute();
}
/**
* {@inheritdoc}
*/
public function updateTermHierarchy(EntityInterface $term) {
$query = $this->database->insert('taxonomy_term_hierarchy')
->fields(array('tid', 'parent'));
foreach ($term->parent as $parent) {
$query->values(array(
'tid' => $term->id(),
'parent' => (int) $parent->value,
));
}
$query->execute();
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\taxonomy\TermStorageControllerInterface.
*/
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for taxonomy term entity controller classes.
*/
interface TermStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Removed reference to terms from term_hierarchy.
*
* @param array
* Array of terms that need to be removed from hierarchy.
*/
public function deleteTermHierarchy($tids);
/**
* Updates terms hierarchy information with the hierarchy trail of it.
*
* @param \Drupal\Core\Entity\EntityInterface $term
* Term entity that needs to be added to term hierarchy information.
*/
public function updateTermHierarchy(EntityInterface $term);
}

View File

@ -8,91 +8,11 @@
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines a controller class for taxonomy vocabularies.
*/
class VocabularyStorageController extends ConfigStorageController {
/**
* Overrides Drupal\Core\Config\Entity\ConfigStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
if (!$update) {
entity_invoke_bundle_hook('create', 'taxonomy_term', $entity->id());
}
elseif ($entity->getOriginalID() != $entity->id()) {
// Reflect machine name changes in the definitions of existing 'taxonomy'
// fields.
$fields = field_read_fields();
foreach ($fields as $field_name => $field) {
$update_field = FALSE;
if ($field['type'] == 'taxonomy_term_reference') {
foreach ($field['settings']['allowed_values'] as $key => &$value) {
if ($value['vocabulary'] == $entity->getOriginalID()) {
$value['vocabulary'] = $entity->id();
$update_field = TRUE;
}
}
if ($update_field) {
field_update_field($field);
}
}
}
// Update bundles.
entity_invoke_bundle_hook('rename', 'taxonomy_term', $entity->getOriginalID(), $entity->id());
}
parent::postSave($entity, $update);
$this->resetCache($update ? array($entity->getOriginalID()) : array());
}
/**
* Overrides Drupal\Core\Config\Entity\ConfigStorageController::preDelete().
*/
protected function preDelete($entities) {
parent::preDelete($entities);
// Only load terms without a parent, child terms will get deleted too.
$tids = db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid IN (:vids) AND th.parent = 0', array(':vids' => array_keys($entities)))->fetchCol();
entity_delete_multiple('taxonomy_term', $tids);
}
/**
* Overrides Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
*/
protected function postDelete($entities) {
parent::postDelete($entities);
$vocabularies = array();
foreach ($entities as $vocabulary) {
$vocabularies[$vocabulary->id()] = $vocabulary->id();
}
// Load all Taxonomy module fields and delete those which use only this
// vocabulary.
$taxonomy_fields = field_read_fields(array('module' => 'taxonomy'));
foreach ($taxonomy_fields as $field_name => $taxonomy_field) {
$modified_field = FALSE;
// Term reference fields may reference terms from more than one
// vocabulary.
foreach ($taxonomy_field['settings']['allowed_values'] as $key => $allowed_value) {
if (isset($vocabularies[$allowed_value['vocabulary']])) {
unset($taxonomy_field['settings']['allowed_values'][$key]);
$modified_field = TRUE;
}
}
if ($modified_field) {
if (empty($taxonomy_field['settings']['allowed_values'])) {
field_delete_field($field_name);
}
else {
// Update the field definition with the new allowed values.
field_update_field($taxonomy_field);
}
}
}
// Reset caches.
$this->resetCache(array_keys($vocabularies));
}
class VocabularyStorageController extends ConfigStorageController implements VocabularyStorageControllerInterface {
/**
* Overrides Drupal\Core\Config\Entity\ConfigStorageController::resetCache().
@ -104,4 +24,11 @@ class VocabularyStorageController extends ConfigStorageController {
entity_info_cache_clear();
}
/**
* {@inheritdoc}
*/
public function getToplevelTids($vids) {
return db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid IN (:vids) AND th.parent = 0', array(':vids' => $vids))->fetchCol();
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\taxonomy\VocabularyStorageControllerInterface.
*/
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for taxonomy vocabulary entity controller classes.
*/
interface VocabularyStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Gets top-level term IDs of vocabularies.
*
* @param array $vids
* Array of vocabulary IDs.
*
* @return array
* Array of top-level term IDs.
*/
public function getToplevelTids($vids);
}

View File

@ -10,6 +10,7 @@ namespace Drupal\user\Plugin\Core\Entity;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\user\RoleInterface;
/**
@ -78,4 +79,23 @@ class Role extends ConfigEntityBase implements RoleInterface {
);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
if (!isset($this->weight) && ($roles = $storage_controller->load())) {
// Set a role weight to make this new role last.
$max = array_reduce($roles, function($max, $role) {
return $max > $role->weight ? $max : $role->weight;
});
$this->weight = $max + 1;
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
$storage_controller->deleteRoleReferences(array_keys($entities));
}
}

View File

@ -7,6 +7,8 @@
namespace Drupal\user\Plugin\Core\Entity;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
@ -228,6 +230,96 @@ class User extends EntityNG implements UserInterface {
unset($this->uuid);
}
/**
* {@inheritdoc}
*/
static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
if (!isset($values['created'])) {
$values['created'] = REQUEST_TIME;
}
// Users always have the authenticated user role.
$values['roles'][] = DRUPAL_AUTHENTICATED_RID;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
// Update the user password if it has changed.
if ($this->isNew() || ($this->pass->value && $this->pass->value != $this->original->pass->value)) {
// Allow alternate password hashing schemes.
$this->pass->value = \Drupal::service('password')->hash(trim($this->pass->value));
// Abort if the hashing failed and returned FALSE.
if (!$this->pass->value) {
throw new EntityMalformedException('The entity does not have a password.');
}
}
if (!$this->isNew()) {
// If the password is empty, that means it was not changed, so use the
// original password.
if (empty($this->pass->value)) {
$this->pass->value = $this->original->pass->value;
}
}
// Store account cancellation information.
foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
if (isset($this->{$key})) {
\Drupal::service('user.data')->set('user', $this->id(), substr($key, 5), $this->{$key});
}
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
if ($update) {
// If the password has been changed, delete all open sessions for the
// user and recreate the current one.
if ($this->pass->value != $this->original->pass->value) {
drupal_session_destroy_uid($this->id());
if ($this->id() == $GLOBALS['user']->uid) {
drupal_session_regenerate();
}
}
// Update user roles if changed.
if ($this->roles->getValue() != $this->original->roles->getValue()) {
$storage_controller->deleteUserRoles(array($this->id()));
$storage_controller->saveRoles($this);
}
// If the user was blocked, delete the user's sessions to force a logout.
if ($this->original->status->value != $this->status->value && $this->status->value == 0) {
drupal_session_destroy_uid($this->id());
}
// Send emails after we have the new user object.
if ($this->status->value != $this->original->status->value) {
// The user's status is changing; conditionally send notification email.
$op = $this->status->value == 1 ? 'status_activated' : 'status_blocked';
_user_mail_notify($op, $this->getBCEntity());
}
}
else {
// Save user roles.
if (count($this->roles) > 1) {
$storage_controller->saveRoles($this);
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
$uids = array_keys($entities);
\Drupal::service('user.data')->delete(NULL, $uids);
$storage_controller->deleteUserRoles($uids);
}
/**
* {@inheritdoc}
*/

View File

@ -8,26 +8,11 @@
namespace Drupal\user;
use Drupal\Core\Config\Entity\ConfigStorageController;
use Drupal\Core\Entity\EntityInterface;
/**
* Controller class for user roles.
*/
class RoleStorageController extends ConfigStorageController {
/**
* {@inheritdoc}
*/
public function preSave(EntityInterface $entity) {
if (!isset($entity->weight) && $roles = entity_load_multiple('user_role')) {
// Set a role weight to make this new role last.
$max = array_reduce($roles, function($max, $entity) {
return $max > $entity->weight ? $max : $entity->weight;
});
$entity->weight = $max + 1;
}
parent::preSave($entity);
}
class RoleStorageController extends ConfigStorageController implements RoleStorageControllerInterface {
/**
* {@inheritdoc}
@ -43,9 +28,7 @@ class RoleStorageController extends ConfigStorageController {
/**
* {@inheritdoc}
*/
protected function postDelete($entities) {
$rids = array_keys($entities);
public function deleteRoleReferences(array $rids) {
// Delete permission assignments.
db_delete('role_permission')
->condition('rid', $rids)

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\user\RoleStorageControllerInterface.
*/
namespace Drupal\user;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for roel entity controller classes.
*/
interface RoleStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Delete role references.
*
* @param array $rids
* The list of role IDs being deleted. The storage controller should
* remove permission and user references to this role.
*/
public function deleteRoleReferences(array $rids);
}

View File

@ -21,7 +21,7 @@ use Drupal\Core\Entity\DatabaseStorageControllerNG;
* This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for user objects.
*/
class UserStorageController extends DatabaseStorageControllerNG {
class UserStorageController extends DatabaseStorageControllerNG implements UserStorageControllerInterface {
/**
* Provides the password hashing service object.
@ -86,10 +86,7 @@ class UserStorageController extends DatabaseStorageControllerNG {
}
// Add any additional roles from the database.
$result = db_query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($queried_users)));
foreach ($result as $record) {
$queried_users[$record->uid]->roles[] = $record->rid;
}
$this->addRoles($queried_users);
// Call the default attachLoad() method. This will add fields and call
// hook_user_load().
@ -97,15 +94,9 @@ class UserStorageController extends DatabaseStorageControllerNG {
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::create().
* {@inheritdoc}
*/
public function create(array $values) {
if (!isset($values['created'])) {
$values['created'] = REQUEST_TIME;
}
// Users always have the authenticated user role.
$values['roles'][] = DRUPAL_AUTHENTICATED_RID;
return parent::create($values)->getBCEntity();
}
@ -126,106 +117,38 @@ class UserStorageController extends DatabaseStorageControllerNG {
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
* {@inheritdoc}
*/
protected function preSave(EntityInterface $entity) {
// Update the user password if it has changed.
if ($entity->isNew() || ($entity->pass->value && $entity->pass->value != $entity->original->pass->value)) {
// Allow alternate password hashing schemes.
$entity->pass->value = $this->password->hash(trim($entity->pass->value));
// Abort if the hashing failed and returned FALSE.
if (!$entity->pass->value) {
throw new EntityMalformedException('The entity does not have a password.');
public function saveRoles(EntityInterface $user) {
$query = $this->database->insert('users_roles')->fields(array('uid', 'rid'));
foreach ($user->roles as $role) {
if (!in_array($role->value, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
$query->values(array(
'uid' => $user->id(),
'rid' => $role->value,
));
}
}
$query->execute();
}
if (!$entity->isNew()) {
// If the password is empty, that means it was not changed, so use the
// original password.
if (empty($entity->pass->value)) {
$entity->pass->value = $entity->original->pass->value;
}
}
// Store account cancellation information.
foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
if (isset($entity->{$key})) {
$this->userData->set('user', $entity->id(), substr($key, 5), $entity->{$key});
}
/**
* {@inheritdoc}
*/
public function addRoles(array $users) {
$result = db_query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
foreach ($result as $record) {
$users[$record->uid]->roles[] = $record->rid;
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
* {@inheritdoc}
*/
protected function postSave(EntityInterface $entity, $update) {
if ($update) {
// If the password has been changed, delete all open sessions for the
// user and recreate the current one.
if ($entity->pass->value != $entity->original->pass->value) {
drupal_session_destroy_uid($entity->id());
if ($entity->id() == $GLOBALS['user']->uid) {
drupal_session_regenerate();
}
}
// Update user roles if changed.
if ($entity->roles->getValue() != $entity->original->roles->getValue()) {
db_delete('users_roles')
->condition('uid', $entity->id())
->execute();
$query = $this->database->insert('users_roles')->fields(array('uid', 'rid'));
foreach ($entity->roles as $role) {
if (!in_array($role->value, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
$query->values(array(
'uid' => $entity->id(),
'rid' => $role->value,
));
}
}
$query->execute();
}
// If the user was blocked, delete the user's sessions to force a logout.
if ($entity->original->status->value != $entity->status->value && $entity->status->value == 0) {
drupal_session_destroy_uid($entity->id());
}
// Send emails after we have the new user object.
if ($entity->status->value != $entity->original->status->value) {
// The user's status is changing; conditionally send notification email.
$op = $entity->status->value == 1 ? 'status_activated' : 'status_blocked';
_user_mail_notify($op, $entity->getBCEntity());
}
}
else {
// Save user roles.
if (count($entity->roles) > 1) {
$query = $this->database->insert('users_roles')->fields(array('uid', 'rid'));
foreach ($entity->roles as $role) {
if (!in_array($role->value, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
$query->values(array(
'uid' => $entity->id(),
'rid' => $role->value,
));
}
}
$query->execute();
}
}
}
/**
* Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
*/
protected function postDelete($entities) {
public function deleteUserRoles(array $uids) {
$this->database->delete('users_roles')
->condition('uid', array_keys($entities), 'IN')
->condition('uid', $uids)
->execute();
$this->userData->delete(NULL, array_keys($entities));
}
/**

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\user\UserStorageControllerInterface.
*/
namespace Drupal\user;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Defines a common interface for user entity controller classes.
*/
interface UserStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Add any roles from the storage to the user.
*
* @param array $users
*/
public function addRoles(array $users);
/**
* Save the user's roles.
*
* @param \Drupal\Core\Entity\EntityInterface $user
*/
public function saveRoles(EntityInterface $user);
/**
* Remove the roles of a user.
*
* @param array $uids
*/
public function deleteUserRoles(array $uids);
}

View File

@ -8,6 +8,7 @@
namespace Drupal\views\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\views\Views;
use Drupal\views_ui\ViewUI;
use Drupal\views\ViewStorageInterface;
@ -361,4 +362,63 @@ class View extends ConfigEntityBase implements ViewStorageInterface {
return $properties;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
views_invalidate_cache();
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
// If there is no information about displays available add at least the
// default display.
$values += array(
'display' => array(
'default' => array(
'display_plugin' => 'default',
'id' => 'default',
'display_title' => 'Master',
'position' => 0,
'display_options' => array(),
),
)
);
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageControllerInterface $storage_controller) {
$this->mergeDefaultDisplaysOptions();
}
/**
* {@inheritdoc}
*/
public function mergeDefaultDisplaysOptions() {
$displays = array();
foreach ($this->get('display') as $key => $options) {
$options += array(
'display_options' => array(),
'display_plugin' => NULL,
'id' => NULL,
'display_title' => '',
'position' => NULL,
);
// Add the defaults for the display.
$displays[$key] = $options;
}
// Sort the displays.
uasort($displays, function ($display1, $display2) {
if ($display1['position'] != $display2['position']) {
return $display1['position'] < $display2['position'] ? -1 : 1;
}
return 0;
});
$this->set('display', $displays);
}
}

View File

@ -35,72 +35,11 @@ class ViewStorageController extends ConfigStorageController {
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
foreach ($queried_entities as $id => $entity) {
$this->mergeDefaultDisplaysOptions($entity);
$entity->mergeDefaultDisplaysOptions();
}
parent::attachLoad($queried_entities, $revision_id);
}
/**
* Overrides Drupal\config\ConfigStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
// Clear caches.
views_invalidate_cache();
}
/**
* Overrides Drupal\config\ConfigStorageController::create().
*/
public function create(array $values) {
// If there is no information about displays available add at least the
// default display.
$values += array(
'display' => array(
'default' => array(
'display_plugin' => 'default',
'id' => 'default',
'display_title' => 'Master',
'position' => 0,
'display_options' => array(),
),
)
);
$entity = parent::create($values);
$this->mergeDefaultDisplaysOptions($entity);
return $entity;
}
/**
* Add defaults to the display options.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The view entity to attach default displays options.
*/
protected function mergeDefaultDisplaysOptions(EntityInterface $entity) {
$displays = array();
foreach ($entity->get('display') as $key => $options) {
$options += array(
'display_options' => array(),
'display_plugin' => NULL,
'id' => NULL,
'display_title' => '',
'position' => NULL,
);
// Add the defaults for the display.
$displays[$key] = $options;
}
// Sort the displays.
uasort($displays, function ($display1, $display2) {
if ($display1['position'] != $display2['position']) {
return $display1['position'] < $display2['position'] ? -1 : 1;
}
return 0;
});
$entity->set('display', $displays);
}
}

View File

@ -25,4 +25,8 @@ interface ViewStorageInterface extends \IteratorAggregate, ConfigEntityInterface
*/
public function &getDisplay($display_id);
/**
* Add defaults to the display options.
*/
public function mergeDefaultDisplaysOptions();
}

View File

@ -8,6 +8,7 @@
namespace Drupal\views_ui;
use Drupal\views\Views;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\views\ViewExecutable;
use Drupal\Core\Database\Database;
use Drupal\Core\TypedData\TypedDataInterface;
@ -1136,12 +1137,69 @@ class ViewUI implements ViewStorageInterface {
$this->storage->onChange($property_name);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageControllerInterface $storage_controller) {
$this->storage->presave($storage_controller);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
$this->storage->postSave($storage_controller, $update);
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageControllerInterface $storage_controller) {
$this->storage->postCreate($storage_controller);
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
$this->storage->preSaveRevision($storage_controller, $record);
}
/**
* {@inheritdoc}
*/
public function mergeDefaultDisplaysOptions() {
$this->storage->mergeDefaultDisplaysOptions();
}
/**
* {@inheritdoc}
*/
public function uriRelationships() {
return $this->storage->uriRelationships();
}
}