Controller class for menu links.
This extends the Drupal\entity\DatabaseStorageController class, adding required special handling for menu_link entities.
Expanded class hierarchy of MenuLinkStorageController
class MenuLinkStorageController extends DatabaseStorageController {
/**
* Indicates whether the delete operation should re-parent children items.
*
* @var bool
*/
protected $preventReparenting = FALSE;
/**
* Holds an array of router item schema fields.
*
* @var array
*/
protected static $routerItemFields = array();
/**
* The route provider service.
*
* @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Overrides DatabaseStorageController::__construct().
*
* @param string $entity_type
* The entity type for which the instance is created.
* @param array $entity_info
* An array of entity info for the entity type.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
* @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
* The route provider service.
*/
public function __construct($entity_type, array $entity_info, Connection $database, RouteProviderInterface $route_provider) {
parent::__construct($entity_type, $entity_info, $database);
$this->routeProvider = $route_provider;
if (empty(static::$routerItemFields)) {
static::$routerItemFields = array_diff(drupal_schema_fields_sql('menu_router'), array(
'weight',
));
}
}
/**
* {@inheritdoc}
*/
public function create(array $values) {
// The bundle of menu links being the menu name is not enforced but is the
// default behavior if no bundle is set.
if (!isset($values['bundle']) && isset($values['menu_name'])) {
$values['bundle'] = $values['menu_name'];
}
return parent::create($values);
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) {
return new static($entity_type, $entity_info, $container
->get('database'), $container
->get('router.route_provider'));
}
/**
* Overrides DatabaseStorageController::buildQuery().
*/
protected function buildQuery($ids, $revision_id = FALSE) {
$query = parent::buildQuery($ids, $revision_id);
// Specify additional fields from the {menu_router} table.
$query
->leftJoin('menu_router', 'm', 'base.router_path = m.path');
$query
->fields('m', static::$routerItemFields);
return $query;
}
/**
* Overrides DatabaseStorageController::attachLoad().
*
* @todo Don't call parent::attachLoad() at all because we want to be able to
* control the entity load hooks.
*/
protected function attachLoad(&$menu_links, $load_revision = FALSE) {
$routes = array();
foreach ($menu_links as &$menu_link) {
$menu_link->options = unserialize($menu_link->options);
// Use the weight property from the menu link.
$menu_link->router_item['weight'] = $menu_link->weight;
// By default use the menu_name as type.
$menu_link->bundle = $menu_link->menu_name;
// For all links that have an associated route, load the route object now
// and save it on the object. That way we avoid a select N+1 problem later.
if ($menu_link->route_name) {
$routes[$menu_link
->id()] = $menu_link->route_name;
}
}
// Now mass-load any routes needed and associate them.
if ($routes) {
$route_objects = $this->routeProvider
->getRoutesByNames($routes);
foreach ($routes as $entity_id => $route) {
// Not all stored routes will be valid on load.
if (isset($route_objects[$route])) {
$menu_links[$entity_id]
->setRouteObject($route_objects[$route]);
}
}
}
parent::attachLoad($menu_links, $load_revision);
}
/**
* Overrides DatabaseStorageController::save().
*/
public function save(EntityInterface $entity) {
// 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.
$return = SAVED_UPDATED;
$transaction = $this->database
->startTransaction();
try {
// Load the stored entity, if any.
if (!$entity
->isNew() && !isset($entity->original)) {
$entity->original = entity_load_unchanged($this->entityType, $entity
->id());
}
if ($entity
->isNew()) {
$entity->mlid = $this->database
->insert($this->entityInfo['base_table'])
->fields(array(
'menu_name' => 'tools',
))
->execute();
$entity
->enforceIsNew();
}
// Unlike the save() method from DatabaseStorageController, we invoke the
// '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);
// 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
// array_intersect_key() with the $entity as the first parameter because
// $entity may have additional keys left over from building a router entry.
// The intersect removes the extra keys, allowing a meaningful comparison.
if ($entity
->isNew() || array_intersect_key(get_object_vars($entity), get_object_vars($entity->original)) != get_object_vars($entity->original)) {
$return = drupal_write_record($this->entityInfo['base_table'], $entity, $this->idKey);
if ($return) {
if (!$entity
->isNew()) {
$this
->resetCache(array(
$entity->{$this->idKey},
));
$this
->postSave($entity, TRUE);
$this
->invokeHook('update', $entity);
}
else {
$return = SAVED_NEW;
$this
->resetCache();
$entity
->enforceIsNew(FALSE);
$this
->postSave($entity, FALSE);
$this
->invokeHook('insert', $entity);
}
}
}
// Ignore slave server temporarily.
db_ignore_slave();
unset($entity->original);
return $return;
} catch (\Exception $e) {
$transaction
->rollback();
watchdog_exception($this->entityType, $e);
throw new EntityStorageException($e
->getMessage(), $e
->getCode(), $e);
}
}
/**
* Overrides DatabaseStorageController::preSave().
*/
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;
}
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;
}
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) {
$this->preventReparenting = $value;
}
/**
* Overrides DatabaseStorageController::preDelete().
*/
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);
}
}
}
}
/**
* 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.
*/
public function loadUpdatedCustomized(array $router_paths) {
$query = parent::buildQuery(NULL);
$query
->condition(db_or()
->condition('updated', 1)
->condition(db_and()
->condition('router_path', $router_paths, 'NOT IN')
->condition('external', 0)
->condition('customized', 1)));
$query_result = $query
->execute();
if (!empty($this->entityInfo['class'])) {
// We provide the necessary arguments for PDO to create objects of the
// specified entity class.
// @see Drupal\Core\Entity\EntityInterface::__construct()
$query_result
->setFetchMode(\PDO::FETCH_CLASS, $this->entityInfo['class'], array(
array(),
$this->entityType,
));
}
return $query_result
->fetchAllAssoc($this->idKey);
}
/**
* 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() {
$query = $this
->buildQuery(NULL);
$query
->condition('base.link_path', 'admin/%', 'LIKE')
->condition('base.hidden', 0, '>=')
->condition('base.module', 'system')
->condition('m.number_parts', 1, '>')
->condition('m.page_callback', 'system_admin_menu_block_page', '<>');
$ids = $query
->execute()
->fetchCol(1);
return $this
->load($ids);
}
/**
* Checks and updates the 'has_children' property for the parent of a link.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
protected 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.
$query = \Drupal::entityQuery($this->entityType);
$query
->condition('menu_name', $entity->menu_name)
->condition('hidden', 0)
->condition('plid', $entity->plid)
->count();
if ($exclude) {
$query
->condition('mlid', $entity
->id(), '<>');
}
$parent_has_children = (bool) $query
->execute() ? 1 : 0;
$this->database
->update('menu_links')
->fields(array(
'has_children' => $parent_has_children,
))
->condition('mlid', $entity->plid)
->execute();
}
}
/**
* 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')
->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.
*/
public function findChildrenRelativeDepth(EntityInterface $entity) {
// @todo Since all we need is a specific field from the base table, does it
// make sense to convert to EFQ?
$query = $this->database
->select('menu_links');
$query
->addField('menu_links', 'depth');
$query
->condition('menu_name', $entity->menu_name);
$query
->orderBy('depth', 'DESC');
$query
->range(0, 1);
$i = 1;
$p = 'p1';
while ($i <= MENU_MAX_DEPTH && $entity->{$p}) {
$query
->condition($p, $entity->{$p});
$p = 'p' . ++$i;
}
$max_depth = $query
->execute()
->fetchField();
return $max_depth > $entity->depth ? $max_depth - $entity->depth : 0;
}
/**
* 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.
*/
protected function moveChildren(EntityInterface $entity) {
$query = $this->database
->update($this->entityInfo['base_table']);
$query
->fields(array(
'menu_name' => $entity->menu_name,
));
$p = 'p1';
$expressions = array();
for ($i = 1; $i <= $entity->depth; $p = 'p' . ++$i) {
$expressions[] = array(
$p,
":p_{$i}",
array(
":p_{$i}" => $entity->{$p},
),
);
}
$j = $entity->original->depth + 1;
while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
$expressions[] = array(
'p' . $i++,
'p' . $j++,
array(),
);
}
while ($i <= MENU_MAX_DEPTH) {
$expressions[] = array(
'p' . $i++,
0,
array(),
);
}
$shift = $entity->depth - $entity->original->depth;
if ($shift > 0) {
// The order of expressions must be reversed so the new values don't
// overwrite the old ones before they can be used because "Single-table
// UPDATE assignments are generally evaluated from left to right"
// @see http://dev.mysql.com/doc/refman/5.0/en/update.html
$expressions = array_reverse($expressions);
}
foreach ($expressions as $expression) {
$query
->expression($expression[0], $expression[1], $expression[2]);
}
$query
->expression('depth', 'depth + :depth', array(
':depth' => $shift,
));
$query
->condition('menu_name', $entity->original->menu_name);
$p = 'p1';
for ($i = 1; $i <= MENU_MAX_DEPTH && $entity->original->{$p}; $p = 'p' . ++$i) {
$query
->condition($p, $entity->original->{$p});
}
$query
->execute();
// Check the has_children status of the parent, while excluding this item.
$this
->updateParentalStatus($entity->original, TRUE);
}
/**
* Returns the number of menu links from a menu.
*
* @param string $menu_name
* The unique name of a menu.
*/
public function countMenuLinks($menu_name) {
$query = \Drupal::entityQuery($this->entityType);
$query
->condition('menu_name', $menu_name)
->count();
return $query
->execute();
}
}
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
DatabaseStorageController:: |
protected | property |
Whether this entity type should use the static cache. Overrides EntityStorageControllerBase:: |
|
DatabaseStorageController:: |
protected | property | Active database connection. | |
DatabaseStorageController:: |
protected | property | An array of field information, i.e. containing definitions. | |
DatabaseStorageController:: |
protected | property | Static cache of field definitions per bundle. | |
DatabaseStorageController:: |
protected | property | Name of entity's revision database table field, if it supports revisions. | |
DatabaseStorageController:: |
protected | property | The table that stores revisions, if the entity supports revisions. | |
DatabaseStorageController:: |
public | function | Defines the base properties of the entity type. | 8 |
DatabaseStorageController:: |
protected | function | Builds an entity query. | 1 |
DatabaseStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::delete(). Overrides EntityStorageControllerInterface:: |
1 |
DatabaseStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::deleteRevision(). Overrides EntityStorageControllerInterface:: |
|
DatabaseStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::getFieldDefinitions(). Overrides EntityStorageControllerInterface:: |
|
DatabaseStorageController:: |
public | function | Implements \Drupal\Core\Entity\EntityStorageControllerInterface::getQueryServiceName(). | |
DatabaseStorageController:: |
protected | function | Invokes a hook on behalf of the entity. | 1 |
DatabaseStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::load(). Overrides EntityStorageControllerInterface:: |
|
DatabaseStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::loadByProperties(). Overrides EntityStorageControllerInterface:: |
|
DatabaseStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::loadRevision(). Overrides EntityStorageControllerInterface:: |
|
DatabaseStorageController:: |
protected | function | Act on a revision before being saved. | 3 |
DatabaseStorageController:: |
protected | function | Saves an entity revision. | 1 |
EntityStorageControllerBase:: |
protected | property | Static cache of entities. | |
EntityStorageControllerBase:: |
protected | property | Array of information about the entity. | |
EntityStorageControllerBase:: |
protected | property | Entity type for this controller instance. | |
EntityStorageControllerBase:: |
protected | property | Additional arguments to pass to hook_TYPE_load(). | |
EntityStorageControllerBase:: |
protected | property | Name of the entity's ID field in the entity database table. | |
EntityStorageControllerBase:: |
protected | property | Name of entity's UUID database table field, if it supports UUIDs. | 1 |
EntityStorageControllerBase:: |
protected | function | Gets entities from the static cache. | |
EntityStorageControllerBase:: |
protected | function | Stores entities in the static entity cache. | |
EntityStorageControllerBase:: |
public | function |
Loads an unchanged entity from the database. Overrides EntityStorageControllerInterface:: |
|
EntityStorageControllerBase:: |
public | function |
Resets the internal, static entity cache. Overrides EntityStorageControllerInterface:: |
3 |
EntityStorageControllerInterface:: |
public | function | Gets the name of the service for the query for this entity storage. | 1 |
MenuLinkStorageController:: |
protected | property | Indicates whether the delete operation should re-parent children items. | |
MenuLinkStorageController:: |
protected | property | The route provider service. | |
MenuLinkStorageController:: |
protected static | property | Holds an array of router item schema fields. | |
MenuLinkStorageController:: |
protected | function |
Overrides DatabaseStorageController::attachLoad(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
protected | function |
Overrides DatabaseStorageController::buildQuery(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
public | function | Returns the number of menu links from a menu. | |
MenuLinkStorageController:: |
public | function |
Implements \Drupal\Core\Entity\EntityStorageControllerInterface::create(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
public static | function |
Instantiates a new instance of this entity controller. Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
public | function | Finds the depth of an item's children relative to its depth. | |
MenuLinkStorageController:: |
protected | function | Finds a possible parent for a given menu link entity. | |
MenuLinkStorageController:: |
protected | function | Returns the route_name matching a URL. | |
MenuLinkStorageController:: |
public | function | Loads system menu link as needed by system_get_module_admin_tasks(). | |
MenuLinkStorageController:: |
public | function | Loads updated and customized menu links for specific router paths. | |
MenuLinkStorageController:: |
protected | function | Updates the children of a menu link that is being moved. | |
MenuLinkStorageController:: |
protected | function |
Overrides DatabaseStorageController::postDelete(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
function |
DatabaseStorageController::postSave(). Overrides DatabaseStorageController:: |
||
MenuLinkStorageController:: |
protected | function |
Overrides DatabaseStorageController::preDelete(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
protected | function |
Overrides DatabaseStorageController::preSave(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
public | function | Sets an internal flag that allows us to prevent the reparenting operations executed during deletion. | |
MenuLinkStorageController:: |
public | function |
Overrides DatabaseStorageController::save(). Overrides DatabaseStorageController:: |
|
MenuLinkStorageController:: |
protected | function | Sets the p1 through p9 properties for a menu link entity being saved. | |
MenuLinkStorageController:: |
protected | function | Checks and updates the 'has_children' property for the parent of a link. | |
MenuLinkStorageController:: |
public | function |
Overrides DatabaseStorageController::__construct(). Overrides DatabaseStorageController:: |