Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 44 |
CRAP | |
0.00% |
0 / 831 |
MenuTreeStorage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 44 |
26732 | |
0.00% |
0 / 831 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
|||
maxDepth | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
resetDefinitions | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
rebuild | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 43 |
|||
purgeMultiple | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 12 |
|||
safeExecuteSelect | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 10 |
|||
save | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
|||
doSave | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 32 |
|||
preSave | |
0.00% |
0 / 1 |
182 | |
0.00% |
0 / 40 |
|||
delete | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 14 |
|||
getSubtreeHeight | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
doFindChildrenRelativeDepth | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 11 |
|||
setParents | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 31 |
|||
moveChildren | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 27 |
|||
findParent | |
0.00% |
0 / 1 |
72 | |
0.00% |
0 / 19 |
|||
updateParentalStatus | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 15 |
|||
prepareLink | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 11 |
|||
loadByProperties | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 15 |
|||
loadByRoute | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 18 |
|||
loadMultiple | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
|||
load | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
loadFull | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
loadFullMultiple | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 13 |
|||
getRootPathIds | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 14 |
|||
getExpanded | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 14 |
|||
saveRecursive | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 11 |
|||
loadTreeData | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 19 |
|||
loadLinks | |
0.00% |
0 / 1 |
342 | |
0.00% |
0 / 62 |
|||
collectRoutesAndDefinitions | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
doCollectRoutesAndDefinitions | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 12 |
|||
loadSubtreeData | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 9 |
|||
menuNameInUse | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
|||
getMenuNames | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
countMenuLinks | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
getAllChildIds | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 13 |
|||
loadAllChildren | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
doBuildTreeData | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
treeDataRecursive | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 20 |
|||
ensureTableExists | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 14 |
|||
serializedFields | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 10 |
|||
definitionFields | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
schemaDefinition | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 233 |
|||
findNoLongerExistingLinks | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 13 |
|||
doDeleteMultiple | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Menu\MenuTreeStorage. | |
*/ | |
namespace Drupal\Core\Menu; | |
use Drupal\Component\Plugin\Exception\PluginException; | |
use Drupal\Component\Utility\UrlHelper; | |
use Drupal\Core\Cache\Cache; | |
use Drupal\Core\Cache\CacheBackendInterface; | |
use Drupal\Core\Cache\CacheTagsInvalidatorInterface; | |
use Drupal\Core\Database\Connection; | |
use Drupal\Core\Database\Database; | |
use Drupal\Core\Database\Query\SelectInterface; | |
use Drupal\Core\Database\SchemaObjectExistsException; | |
/** | |
* Provides a menu tree storage using the database. | |
*/ | |
class MenuTreeStorage implements MenuTreeStorageInterface { | |
/** | |
* The maximum depth of a menu links tree. | |
*/ | |
const MAX_DEPTH = 9; | |
/** | |
* The database connection. | |
* | |
* @var \Drupal\Core\Database\Connection | |
*/ | |
protected $connection; | |
/** | |
* Cache backend instance for the extracted tree data. | |
* | |
* @var \Drupal\Core\Cache\CacheBackendInterface | |
*/ | |
protected $menuCacheBackend; | |
/** | |
* The cache tags invalidator. | |
* | |
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface | |
*/ | |
protected $cacheTagsInvalidator; | |
/** | |
* The database table name. | |
* | |
* @var string | |
*/ | |
protected $table; | |
/** | |
* Additional database connection options to use in queries. | |
* | |
* @var array | |
*/ | |
protected $options = array(); | |
/** | |
* Stores definitions that have already been loaded for better performance. | |
* | |
* An array of plugin definition arrays, keyed by plugin ID. | |
* | |
* @var array | |
*/ | |
protected $definitions = array(); | |
/** | |
* List of serialized fields. | |
* | |
* @var array | |
*/ | |
protected $serializedFields; | |
/** | |
* List of plugin definition fields. | |
* | |
* @todo Decide how to keep these field definitions in sync. | |
* https://www.drupal.org/node/2302085 | |
* | |
* @see \Drupal\Core\Menu\MenuLinkManager::$defaults | |
* | |
* @var array | |
*/ | |
protected $definitionFields = array( | |
'menu_name', | |
'route_name', | |
'route_parameters', | |
'url', | |
'title', | |
'description', | |
'parent', | |
'weight', | |
'options', | |
'expanded', | |
'enabled', | |
'provider', | |
'metadata', | |
'class', | |
'form_class', | |
'id', | |
); | |
/** | |
* Constructs a new \Drupal\Core\Menu\MenuTreeStorage. | |
* | |
* @param \Drupal\Core\Database\Connection $connection | |
* A Database connection to use for reading and writing configuration data. | |
* @param \Drupal\Core\Cache\CacheBackendInterface $menu_cache_backend | |
* Cache backend instance for the extracted tree data. | |
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator | |
* The cache tags invalidator. | |
* @param string $table | |
* A database table name to store configuration data in. | |
* @param array $options | |
* (optional) Any additional database connection options to use in queries. | |
*/ | |
public function __construct(Connection $connection, CacheBackendInterface $menu_cache_backend, CacheTagsInvalidatorInterface $cache_tags_invalidator, $table, array $options = array()) { | |
$this->connection = $connection; | |
$this->menuCacheBackend = $menu_cache_backend; | |
$this->cacheTagsInvalidator = $cache_tags_invalidator; | |
$this->table = $table; | |
$this->options = $options; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function maxDepth() { | |
return static::MAX_DEPTH; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function resetDefinitions() { | |
$this->definitions = array(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function rebuild(array $definitions) { | |
$links = array(); | |
$children = array(); | |
$top_links = array(); | |
// Fetch the list of existing menus, in case some are not longer populated | |
// after the rebuild. | |
$before_menus = $this->getMenuNames(); | |
if ($definitions) { | |
foreach ($definitions as $id => $link) { | |
// Flag this link as discovered, i.e. saved via rebuild(). | |
$link['discovered'] = 1; | |
// Note: The parent we set here might be just stored in the {menu_tree} | |
// table, so it will not end up in $top_links. Therefore the later loop | |
// on the orphan links, will handle those cases. | |
if (!empty($link['parent'])) { | |
$children[$link['parent']][$id] = $id; | |
} | |
else { | |
// A top level link - we need them to root our tree. | |
$top_links[$id] = $id; | |
$link['parent'] = ''; | |
} | |
$links[$id] = $link; | |
} | |
} | |
foreach ($top_links as $id) { | |
$this->saveRecursive($id, $children, $links); | |
} | |
// Handle any children we didn't find starting from top-level links. | |
foreach ($children as $orphan_links) { | |
foreach ($orphan_links as $id) { | |
// Check for a parent that is not loaded above since only internal links | |
// are loaded above. | |
$parent = $this->loadFull($links[$id]['parent']); | |
// If there is a parent add it to the links to be used in | |
// ::saveRecursive(). | |
if ($parent) { | |
$links[$links[$id]['parent']] = $parent; | |
} | |
else { | |
// Force it to the top level. | |
$links[$id]['parent'] = ''; | |
} | |
$this->saveRecursive($id, $children, $links); | |
} | |
} | |
$result = $this->findNoLongerExistingLinks($definitions); | |
// Remove all such items. | |
if ($result) { | |
$this->purgeMultiple($result); | |
} | |
$this->resetDefinitions(); | |
$affected_menus = $this->getMenuNames() + $before_menus; | |
// Invalidate any cache tagged with any menu name. | |
$cache_tags = Cache::buildTags('config:system.menu', $affected_menus, '.'); | |
$this->cacheTagsInvalidator->invalidateTags($cache_tags); | |
$this->resetDefinitions(); | |
// Every item in the cache bin should have one of the menu cache tags but it | |
// is not guaranteed, so invalidate everything in the bin. | |
$this->menuCacheBackend->invalidateAll(); | |
} | |
/** | |
* Purges multiple menu links that no longer exist. | |
* | |
* @param array $ids | |
* An array of menu link IDs. | |
*/ | |
protected function purgeMultiple(array $ids) { | |
$loaded = $this->loadFullMultiple($ids); | |
foreach ($loaded as $id => $link) { | |
if ($link['has_children']) { | |
$children = $this->loadByProperties(array('parent' => $id)); | |
foreach ($children as $child) { | |
$child['parent'] = $link['parent']; | |
$this->save($child); | |
} | |
} | |
} | |
$this->doDeleteMultiple($ids); | |
} | |
/** | |
* Executes a select query while making sure the database table exists. | |
* | |
* @param \Drupal\Core\Database\Query\SelectInterface $query | |
* The select object to be executed. | |
* | |
* @return \Drupal\Core\Database\StatementInterface|null | |
* A prepared statement, or NULL if the query is not valid. | |
* | |
* @throws \Exception | |
* Thrown if the table could not be created or the database connection | |
* failed. | |
*/ | |
protected function safeExecuteSelect(SelectInterface $query) { | |
try { | |
return $query->execute(); | |
} | |
catch (\Exception $e) { | |
// If there was an exception, try to create the table. | |
if ($this->ensureTableExists()) { | |
return $query->execute(); | |
} | |
// Some other failure that we can not recover from. | |
throw $e; | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function save(array $link) { | |
$affected_menus = $this->doSave($link); | |
$this->resetDefinitions(); | |
$cache_tags = Cache::buildTags('config:system.menu', $affected_menus, '.'); | |
$this->cacheTagsInvalidator->invalidateTags($cache_tags); | |
return $affected_menus; | |
} | |
/** | |
* Saves a link without clearing caches. | |
* | |
* @param array $link | |
* A definition, according to $definitionFields, for a | |
* \Drupal\Core\Menu\MenuLinkInterface plugin. | |
* | |
* @return array | |
* The menu names affected by the save operation. This will be one menu | |
* name if the link is saved to the sane menu, or two if it is saved to a | |
* new menu. | |
* | |
* @throws \Exception | |
* Thrown if the storage back-end does not exist and could not be created. | |
* @throws \Drupal\Component\Plugin\Exception\PluginException | |
* Thrown if the definition is invalid, for example, if the specified parent | |
* would cause the links children to be moved to greater than the maximum | |
* depth. | |
*/ | |
protected function doSave(array $link) { | |
$original = $this->loadFull($link['id']); | |
// @todo Should we just return here if the link values match the original | |
// values completely? | |
// https://www.drupal.org/node/2302137 | |
$affected_menus = array(); | |
$transaction = $this->connection->startTransaction(); | |
try { | |
if ($original) { | |
$link['mlid'] = $original['mlid']; | |
$link['has_children'] = $original['has_children']; | |
$affected_menus[$original['menu_name']] = $original['menu_name']; | |
} | |
else { | |
// Generate a new mlid. | |
$options = array('return' => Database::RETURN_INSERT_ID) + $this->options; | |
$link['mlid'] = $this->connection->insert($this->table, $options) | |
->fields(array('id' => $link['id'], 'menu_name' => $link['menu_name'])) | |
->execute(); | |
} | |
$fields = $this->preSave($link, $original); | |
// We may be moving the link to a new menu. | |
$affected_menus[$fields['menu_name']] = $fields['menu_name']; | |
$query = $this->connection->update($this->table, $this->options); | |
$query->condition('mlid', $link['mlid']); | |
$query->fields($fields) | |
->execute(); | |
if ($original) { | |
$this->updateParentalStatus($original); | |
} | |
$this->updateParentalStatus($link); | |
} | |
catch (\Exception $e) { | |
$transaction->rollback(); | |
throw $e; | |
} | |
return $affected_menus; | |
} | |
/** | |
* Fills in all the fields the database save needs, using the link definition. | |
* | |
* @param array $link | |
* The link definition to be updated. | |
* @param array $original | |
* The link definition before the changes. May be empty if not found. | |
* | |
* @return array | |
* The values which will be stored. | |
* | |
* @throws \Drupal\Component\Plugin\Exception\PluginException | |
* Thrown when the specific depth exceeds the maximum. | |
*/ | |
protected function preSave(array &$link, array $original) { | |
static $schema_fields, $schema_defaults; | |
if (empty($schema_fields)) { | |
$schema = static::schemaDefinition(); | |
$schema_fields = $schema['fields']; | |
foreach ($schema_fields as $name => $spec) { | |
if (isset($spec['default'])) { | |
$schema_defaults[$name] = $spec['default']; | |
} | |
} | |
} | |
// Try to find a parent link. If found, assign it and derive its menu. | |
$parent = $this->findParent($link, $original); | |
if ($parent) { | |
$link['parent'] = $parent['id']; | |
$link['menu_name'] = $parent['menu_name']; | |
} | |
else { | |
$link['parent'] = ''; | |
} | |
// If no corresponding parent link was found, move the link to the | |
// top-level. | |
foreach ($schema_defaults as $name => $default) { | |
if (!isset($link[$name])) { | |
$link[$name] = $default; | |
} | |
} | |
$fields = array_intersect_key($link, $schema_fields); | |
// Sort the route parameters so that the query string will be the same. | |
asort($fields['route_parameters']); | |
// Since this will be urlencoded, it's safe to store and match against a | |
// text field. | |
$fields['route_param_key'] = $fields['route_parameters'] ? UrlHelper::buildQuery($fields['route_parameters']) : ''; | |
foreach ($this->serializedFields() as $name) { | |
if (isset($fields[$name])) { | |
$fields[$name] = serialize($fields[$name]); | |
} | |
} | |
$this->setParents($fields, $parent, $original); | |
// Need to check both parent and menu_name, since parent can be empty in any | |
// menu. | |
if ($original && ($link['parent'] != $original['parent'] || $link['menu_name'] != $original['menu_name'])) { | |
$this->moveChildren($fields, $original); | |
} | |
// We needed the mlid above, but not in the update query. | |
unset($fields['mlid']); | |
// Cast Booleans to int, if needed. | |
$fields['enabled'] = (int) $fields['enabled']; | |
$fields['expanded'] = (int) $fields['expanded']; | |
return $fields; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function delete($id) { | |
// Children get re-attached to the menu link's parent. | |
$item = $this->loadFull($id); | |
// It's possible the link is already deleted. | |
if ($item) { | |
$parent = $item['parent']; | |
$children = $this->loadByProperties(array('parent' => $id)); | |
foreach ($children as $child) { | |
$child['parent'] = $parent; | |
$this->save($child); | |
} | |
$this->doDeleteMultiple([$id]); | |
$this->updateParentalStatus($item); | |
// Many children may have moved. | |
$this->resetDefinitions(); | |
$this->cacheTagsInvalidator->invalidateTags(['config:system.menu.' . $item['menu_name']]); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getSubtreeHeight($id) { | |
$original = $this->loadFull($id); | |
return $original ? $this->doFindChildrenRelativeDepth($original) + 1 : 0; | |
} | |
/** | |
* Finds the relative depth of this link's deepest child. | |
* | |
* @param array $original | |
* The parent definition used to find the depth. | |
* | |
* @return int | |
* Returns the relative depth. | |
*/ | |
protected function doFindChildrenRelativeDepth(array $original) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->addField($this->table, 'depth'); | |
$query->condition('menu_name', $original['menu_name']); | |
$query->orderBy('depth', 'DESC'); | |
$query->range(0, 1); | |
for ($i = 1; $i <= static::MAX_DEPTH && $original["p$i"]; $i++) { | |
$query->condition("p$i", $original["p$i"]); | |
} | |
$max_depth = $this->safeExecuteSelect($query)->fetchField(); | |
return ($max_depth > $original['depth']) ? $max_depth - $original['depth'] : 0; | |
} | |
/** | |
* Sets the materialized path field values based on the parent. | |
* | |
* @param array $fields | |
* The menu link. | |
* @param array|false $parent | |
* The parent menu link. | |
* @param array $original | |
* The original menu link. | |
*/ | |
protected function setParents(array &$fields, $parent, array $original) { | |
// Directly fill parents for top-level links. | |
if (empty($fields['parent'])) { | |
$fields['p1'] = $fields['mlid']; | |
for ($i = 2; $i <= $this->maxDepth(); $i++) { | |
$fields["p$i"] = 0; | |
} | |
$fields['depth'] = 1; | |
} | |
// Otherwise, ensure that this link's depth is not beyond the maximum depth | |
// and fill parents based on the parent link. | |
else { | |
// @todo We want to also check $original['has_children'] here, but that | |
// will be 0 even if there are children if those are not enabled. | |
// has_children is really just the rendering hint. So, we either need | |
// to define another column (has_any_children), or do the extra query. | |
// https://www.drupal.org/node/2302149 | |
if ($original) { | |
$limit = $this->maxDepth() - $this->doFindChildrenRelativeDepth($original) - 1; | |
} | |
else { | |
$limit = $this->maxDepth() - 1; | |
} | |
if ($parent['depth'] > $limit) { | |
throw new PluginException("The link with ID {$fields['id']} or its children exceeded the maximum depth of {$this->maxDepth()}"); | |
} | |
$fields['depth'] = $parent['depth'] + 1; | |
$i = 1; | |
while ($i < $fields['depth']) { | |
$p = 'p' . $i++; | |
$fields[$p] = $parent[$p]; | |
} | |
$p = 'p' . $i++; | |
// The parent (p1 - p9) corresponding to the depth always equals the mlid. | |
$fields[$p] = $fields['mlid']; | |
while ($i <= static::MAX_DEPTH) { | |
$p = 'p' . $i++; | |
$fields[$p] = 0; | |
} | |
} | |
} | |
/** | |
* Re-parents a link's children when the link itself is moved. | |
* | |
* @param array $fields | |
* The changed menu link. | |
* @param array $original | |
* The original menu link. | |
*/ | |
protected function moveChildren($fields, $original) { | |
$query = $this->connection->update($this->table, $this->options); | |
$query->fields(array('menu_name' => $fields['menu_name'])); | |
$expressions = array(); | |
for ($i = 1; $i <= $fields['depth']; $i++) { | |
$expressions[] = array("p$i", ":p_$i", array(":p_$i" => $fields["p$i"])); | |
} | |
$j = $original['depth'] + 1; | |
while ($i <= $this->maxDepth() && $j <= $this->maxDepth()) { | |
$expressions[] = array('p' . $i++, 'p' . $j++, array()); | |
} | |
while ($i <= $this->maxDepth()) { | |
$expressions[] = array('p' . $i++, 0, array()); | |
} | |
$shift = $fields['depth'] - $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', $original['menu_name']); | |
for ($i = 1; $i <= $this->maxDepth() && $original["p$i"]; $i++) { | |
$query->condition("p$i", $original["p$i"]); | |
} | |
$query->execute(); | |
} | |
/** | |
* Loads the parent definition if it exists. | |
* | |
* @param array $link | |
* The link definition to find the parent of. | |
* @param array|false $original | |
* The original link that might be used to find the parent if the parent | |
* is not set on the $link, or FALSE if the original could not be loaded. | |
* | |
* @return array|false | |
* Returns a definition array, or FALSE if no parent was found. | |
*/ | |
protected function findParent($link, $original) { | |
$parent = FALSE; | |
// This item is explicitly top-level, skip the rest of the parenting. | |
if (isset($link['parent']) && empty($link['parent'])) { | |
return $parent; | |
} | |
// If we have a parent link ID, try to use that. | |
$candidates = array(); | |
if (isset($link['parent'])) { | |
$candidates[] = $link['parent']; | |
} | |
elseif (!empty($original['parent']) && $link['menu_name'] == $original['menu_name']) { | |
// Otherwise, fall back to the original parent. | |
$candidates[] = $original['parent']; | |
} | |
foreach ($candidates as $id) { | |
$parent = $this->loadFull($id); | |
if ($parent) { | |
break; | |
} | |
} | |
return $parent; | |
} | |
/** | |
* Sets has_children for the link's parent if it has visible children. | |
* | |
* @param array $link | |
* The link to get a parent ID from. | |
*/ | |
protected function updateParentalStatus(array $link) { | |
// If parent is empty, there is nothing to update. | |
if (!empty($link['parent'])) { | |
// Check if at least one visible child exists in the table. | |
$query = $this->connection->select($this->table, $this->options); | |
$query->addExpression('1'); | |
$query->range(0, 1); | |
$query | |
->condition('menu_name', $link['menu_name']) | |
->condition('parent', $link['parent']) | |
->condition('enabled', 1); | |
$parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0; | |
$this->connection->update($this->table, $this->options) | |
->fields(array('has_children' => $parent_has_children)) | |
->condition('id', $link['parent']) | |
->execute(); | |
} | |
} | |
/** | |
* Prepares a link by unserializing values and saving the definition. | |
* | |
* @param array $link | |
* The data loaded in the query. | |
* @param bool $intersect | |
* If TRUE, filter out values that are not part of the actual definition. | |
* | |
* @return array | |
* The prepared link data. | |
*/ | |
protected function prepareLink(array $link, $intersect = FALSE) { | |
foreach ($this->serializedFields() as $name) { | |
if (isset($link[$name])) { | |
$link[$name] = unserialize($link[$name]); | |
} | |
} | |
if ($intersect) { | |
$link = array_intersect_key($link, array_flip($this->definitionFields())); | |
} | |
$this->definitions[$link['id']] = $link; | |
return $link; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function loadByProperties(array $properties) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table, $this->definitionFields()); | |
foreach ($properties as $name => $value) { | |
if (!in_array($name, $this->definitionFields(), TRUE)) { | |
$fields = implode(', ', $this->definitionFields()); | |
throw new \InvalidArgumentException("An invalid property name, $name was specified. Allowed property names are: $fields."); | |
} | |
$query->condition($name, $value); | |
} | |
$loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); | |
foreach ($loaded as $id => $link) { | |
$loaded[$id] = $this->prepareLink($link); | |
} | |
return $loaded; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function loadByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) { | |
// Sort the route parameters so that the query string will be the same. | |
asort($route_parameters); | |
// Since this will be urlencoded, it's safe to store and match against a | |
// text field. | |
// @todo Standardize an efficient way to load by route name and parameters | |
// in place of system path. https://www.drupal.org/node/2302139 | |
$param_key = $route_parameters ? UrlHelper::buildQuery($route_parameters) : ''; | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table, $this->definitionFields()); | |
$query->condition('route_name', $route_name); | |
$query->condition('route_param_key', $param_key); | |
if ($menu_name) { | |
$query->condition('menu_name', $menu_name); | |
} | |
// Make the ordering deterministic. | |
$query->orderBy('depth'); | |
$query->orderBy('weight'); | |
$query->orderBy('id'); | |
$loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); | |
foreach ($loaded as $id => $link) { | |
$loaded[$id] = $this->prepareLink($link); | |
} | |
return $loaded; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function loadMultiple(array $ids) { | |
$missing_ids = array_diff($ids, array_keys($this->definitions)); | |
if ($missing_ids) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table, $this->definitionFields()); | |
$query->condition('id', $missing_ids, 'IN'); | |
$loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); | |
foreach ($loaded as $id => $link) { | |
$this->definitions[$id] = $this->prepareLink($link); | |
} | |
} | |
return array_intersect_key($this->definitions, array_flip($ids)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function load($id) { | |
if (isset($this->definitions[$id])) { | |
return $this->definitions[$id]; | |
} | |
$loaded = $this->loadMultiple(array($id)); | |
return isset($loaded[$id]) ? $loaded[$id] : FALSE; | |
} | |
/** | |
* Loads all table fields, not just those that are in the plugin definition. | |
* | |
* @param string $id | |
* The menu link ID. | |
* | |
* @return array | |
* The loaded menu link definition or an empty array if not be found. | |
*/ | |
protected function loadFull($id) { | |
$loaded = $this->loadFullMultiple(array($id)); | |
return isset($loaded[$id]) ? $loaded[$id] : array(); | |
} | |
/** | |
* Loads all table fields for multiple menu link definitions by ID. | |
* | |
* @param array $ids | |
* The IDs to load. | |
* | |
* @return array | |
* The loaded menu link definitions. | |
*/ | |
protected function loadFullMultiple(array $ids) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table); | |
$query->condition('id', $ids, 'IN'); | |
$loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); | |
foreach ($loaded as &$link) { | |
foreach ($this->serializedFields() as $name) { | |
if (isset($link[$name])) { | |
$link[$name] = unserialize($link[$name]); | |
} | |
} | |
} | |
return $loaded; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getRootPathIds($id) { | |
$subquery = $this->connection->select($this->table, $this->options); | |
// @todo Consider making this dynamic based on static::MAX_DEPTH or from the | |
// schema if that is generated using static::MAX_DEPTH. | |
// https://www.drupal.org/node/2302043 | |
$subquery->fields($this->table, array('p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9')); | |
$subquery->condition('id', $id); | |
$result = current($subquery->execute()->fetchAll(\PDO::FETCH_ASSOC)); | |
$ids = array_filter($result); | |
if ($ids) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table, array('id')); | |
$query->orderBy('depth', 'DESC'); | |
$query->condition('mlid', $ids, 'IN'); | |
// @todo Cache this result in memory if we find it is being used more | |
// than once per page load. https://www.drupal.org/node/2302185 | |
return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); | |
} | |
return array(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getExpanded($menu_name, array $parents) { | |
// @todo Go back to tracking in state or some other way which menus have | |
// expanded links? https://www.drupal.org/node/2302187 | |
do { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table, array('id')); | |
$query->condition('menu_name', $menu_name); | |
$query->condition('expanded', 1); | |
$query->condition('has_children', 1); | |
$query->condition('enabled', 1); | |
$query->condition('parent', $parents, 'IN'); | |
$query->condition('id', $parents, 'NOT IN'); | |
$result = $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); | |
$parents += $result; | |
} while (!empty($result)); | |
return $parents; | |
} | |
/** | |
* Saves menu links recursively. | |
* | |
* @param string $id | |
* The definition ID. | |
* @param array $children | |
* An array of IDs of child links collected by parent ID. | |
* @param array $links | |
* An array of all definitions keyed by ID. | |
*/ | |
protected function saveRecursive($id, &$children, &$links) { | |
if (!empty($links[$id]['parent']) && empty($links[$links[$id]['parent']])) { | |
// Invalid parent ID, so remove it. | |
$links[$id]['parent'] = ''; | |
} | |
$this->doSave($links[$id]); | |
if (!empty($children[$id])) { | |
foreach ($children[$id] as $next_id) { | |
$this->saveRecursive($next_id, $children, $links); | |
} | |
} | |
// Remove processed link names so we can find stragglers. | |
unset($children[$id]); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function loadTreeData($menu_name, MenuTreeParameters $parameters) { | |
// Build the cache ID; sort 'expanded' and 'conditions' to prevent duplicate | |
// cache items. | |
sort($parameters->expandedParents); | |
asort($parameters->conditions); | |
$tree_cid = "tree-data:$menu_name:" . serialize($parameters); | |
$cache = $this->menuCacheBackend->get($tree_cid); | |
if ($cache && isset($cache->data)) { | |
$data = $cache->data; | |
// Cache the definitions in memory so they don't need to be loaded again. | |
$this->definitions += $data['definitions']; | |
unset($data['definitions']); | |
} | |
else { | |
$links = $this->loadLinks($menu_name, $parameters); | |
$data['tree'] = $this->doBuildTreeData($links, $parameters->activeTrail, $parameters->minDepth); | |
$data['definitions'] = array(); | |
$data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']); | |
$this->menuCacheBackend->set($tree_cid, $data, Cache::PERMANENT, ['config:system.menu.' . $menu_name]); | |
// The definitions were already added to $this->definitions in | |
// $this->doBuildTreeData() | |
unset($data['definitions']); | |
} | |
return $data; | |
} | |
/** | |
* Loads links in the given menu, according to the given tree parameters. | |
* | |
* @param string $menu_name | |
* A menu name. | |
* @param \Drupal\Core\Menu\MenuTreeParameters $parameters | |
* The parameters to determine which menu links to be loaded into a tree. | |
* This method will set the absolute minimum depth, which is used in | |
* MenuTreeStorage::doBuildTreeData(). | |
* | |
* @return array | |
* A flat array of menu links that are part of the menu. Each array element | |
* is an associative array of information about the menu link, containing | |
* the fields from the {menu_tree} table. This array must be ordered | |
* depth-first. | |
*/ | |
protected function loadLinks($menu_name, MenuTreeParameters $parameters) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table); | |
// Allow a custom root to be specified for loading a menu link tree. If | |
// omitted, the default root (i.e. the actual root, '') is used. | |
if ($parameters->root !== '') { | |
$root = $this->loadFull($parameters->root); | |
// If the custom root does not exist, we cannot load the links below it. | |
if (!$root) { | |
return array(); | |
} | |
// When specifying a custom root, we only want to find links whose | |
// parent IDs match that of the root; that's how we ignore the rest of the | |
// tree. In other words: we exclude everything unreachable from the | |
// custom root. | |
for ($i = 1; $i <= $root['depth']; $i++) { | |
$query->condition("p$i", $root["p$i"]); | |
} | |
// When specifying a custom root, the menu is determined by that root. | |
$menu_name = $root['menu_name']; | |
// If the custom root exists, then we must rewrite some of our | |
// parameters; parameters are relative to the root (default or custom), | |
// but the queries require absolute numbers, so adjust correspondingly. | |
if (isset($parameters->minDepth)) { | |
$parameters->minDepth += $root['depth']; | |
} | |
else { | |
$parameters->minDepth = $root['depth']; | |
} | |
if (isset($parameters->maxDepth)) { | |
$parameters->maxDepth += $root['depth']; | |
} | |
} | |
// If no minimum depth is specified, then set the actual minimum depth, | |
// depending on the root. | |
if (!isset($parameters->minDepth)) { | |
if ($parameters->root !== '' && $root) { | |
$parameters->minDepth = $root['depth']; | |
} | |
else { | |
$parameters->minDepth = 1; | |
} | |
} | |
for ($i = 1; $i <= $this->maxDepth(); $i++) { | |
$query->orderBy('p' . $i, 'ASC'); | |
} | |
$query->condition('menu_name', $menu_name); | |
if (!empty($parameters->expandedParents)) { | |
$query->condition('parent', $parameters->expandedParents, 'IN'); | |
} | |
if (isset($parameters->minDepth) && $parameters->minDepth > 1) { | |
$query->condition('depth', $parameters->minDepth, '>='); | |
} | |
if (isset($parameters->maxDepth)) { | |
$query->condition('depth', $parameters->maxDepth, '<='); | |
} | |
// Add custom query conditions, if any were passed. | |
if (!empty($parameters->conditions)) { | |
// Only allow conditions that are testing definition fields. | |
$parameters->conditions = array_intersect_key($parameters->conditions, array_flip($this->definitionFields())); | |
$serialized_fields = $this->serializedFields(); | |
foreach ($parameters->conditions as $column => $value) { | |
if (is_array($value)) { | |
$operator = $value[1]; | |
$value = $value[0]; | |
} | |
else { | |
$operator = '='; | |
} | |
if (in_array($column, $serialized_fields)) { | |
$value = serialize($value); | |
} | |
$query->condition($column, $value, $operator); | |
} | |
} | |
$links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); | |
return $links; | |
} | |
/** | |
* Traverses the menu tree and collects all the route names and definitions. | |
* | |
* @param array $tree | |
* The menu tree you wish to operate on. | |
* @param array $definitions | |
* An array to accumulate definitions by reference. | |
* | |
* @return array | |
* Array of route names, with all values being unique. | |
*/ | |
protected function collectRoutesAndDefinitions(array $tree, array &$definitions) { | |
return array_values($this->doCollectRoutesAndDefinitions($tree, $definitions)); | |
} | |
/** | |
* Collects all the route names and definitions. | |
* | |
* @param array $tree | |
* A menu link tree from MenuTreeStorage::doBuildTreeData() | |
* @param array $definitions | |
* The collected definitions which are populated by reference. | |
* | |
* @return array | |
* The collected route names. | |
*/ | |
protected function doCollectRoutesAndDefinitions(array $tree, array &$definitions) { | |
$route_names = array(); | |
foreach (array_keys($tree) as $id) { | |
$definitions[$id] = $this->definitions[$id]; | |
if (!empty($definition['route_name'])) { | |
$route_names[$definition['route_name']] = $definition['route_name']; | |
} | |
if ($tree[$id]['subtree']) { | |
$route_names += $this->doCollectRoutesAndDefinitions($tree[$id]['subtree'], $definitions); | |
} | |
} | |
return $route_names; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function loadSubtreeData($id, $max_relative_depth = NULL) { | |
$tree = array(); | |
$root = $this->loadFull($id); | |
if (!$root) { | |
return $tree; | |
} | |
$parameters = new MenuTreeParameters(); | |
$parameters->setRoot($id)->onlyEnabledLinks(); | |
return $this->loadTreeData($root['menu_name'], $parameters); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function menuNameInUse($menu_name) { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->addField($this->table, 'mlid'); | |
$query->condition('menu_name', $menu_name); | |
$query->range(0, 1); | |
return (bool) $this->safeExecuteSelect($query); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getMenuNames() { | |
$query = $this->connection->select($this->table, $this->options); | |
$query->addField($this->table, 'menu_name'); | |
$query->distinct(); | |
return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function countMenuLinks($menu_name = NULL) { | |
$query = $this->connection->select($this->table, $this->options); | |
if ($menu_name) { | |
$query->condition('menu_name', $menu_name); | |
} | |
return $this->safeExecuteSelect($query->countQuery())->fetchField(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getAllChildIds($id) { | |
$root = $this->loadFull($id); | |
if (!$root) { | |
return array(); | |
} | |
$query = $this->connection->select($this->table, $this->options); | |
$query->fields($this->table, array('id')); | |
$query->condition('menu_name', $root['menu_name']); | |
for ($i = 1; $i <= $root['depth']; $i++) { | |
$query->condition("p$i", $root["p$i"]); | |
} | |
// The next p column should not be empty. This excludes the root link. | |
$query->condition("p$i", 0, '>'); | |
return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function loadAllChildren($id, $max_relative_depth = NULL) { | |
$parameters = new MenuTreeParameters(); | |
$parameters->setRoot($id)->excludeRoot()->setMaxDepth($max_relative_depth)->onlyEnabledLinks(); | |
$links = $this->loadLinks(NULL, $parameters); | |
foreach ($links as $id => $link) { | |
$links[$id] = $this->prepareLink($link); | |
} | |
return $links; | |
} | |
/** | |
* Prepares the data for calling $this->treeDataRecursive(). | |
*/ | |
protected function doBuildTreeData(array $links, array $parents = array(), $depth = 1) { | |
// Reverse the array so we can use the more efficient array_pop() function. | |
$links = array_reverse($links); | |
return $this->treeDataRecursive($links, $parents, $depth); | |
} | |
/** | |
* Builds the data representing a menu tree. | |
* | |
* The function is a bit complex because the rendering of a link depends on | |
* the next menu link. | |
* | |
* @param array $links | |
* A flat array of menu links that are part of the menu. Each array element | |
* is an associative array of information about the menu link, containing | |
* the fields from the $this->table. This array must be ordered | |
* depth-first. MenuTreeStorage::loadTreeData() includes a sample query. | |
* | |
* @param array $parents | |
* An array of the menu link ID values that are in the path from the current | |
* page to the root of the menu tree. | |
* @param int $depth | |
* The minimum depth to include in the returned menu tree. | |
* | |
* @return array | |
* The fully built tree. | |
* | |
* @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData() | |
*/ | |
protected function treeDataRecursive(array &$links, array $parents, $depth) { | |
$tree = array(); | |
while ($tree_link_definition = array_pop($links)) { | |
$tree[$tree_link_definition['id']] = array( | |
'definition' => $this->prepareLink($tree_link_definition, TRUE), | |
'has_children' => $tree_link_definition['has_children'], | |
// We need to determine if we're on the path to root so we can later | |
// build the correct active trail. | |
'in_active_trail' => in_array($tree_link_definition['id'], $parents), | |
'subtree' => array(), | |
'depth' => $tree_link_definition['depth'], | |
); | |
// Look ahead to the next link, but leave it on the array so it's | |
// available to other recursive function calls if we return or build a | |
// sub-tree. | |
$next = end($links); | |
// Check whether the next link is the first in a new sub-tree. | |
if ($next && $next['depth'] > $depth) { | |
// Recursively call doBuildTreeData to build the sub-tree. | |
$tree[$tree_link_definition['id']]['subtree'] = $this->treeDataRecursive($links, $parents, $next['depth']); | |
// Fetch next link after filling the sub-tree. | |
$next = end($links); | |
} | |
// Determine if we should exit the loop and return. | |
if (!$next || $next['depth'] < $depth) { | |
break; | |
} | |
} | |
return $tree; | |
} | |
/** | |
* Checks if the tree table exists and create it if not. | |
* | |
* @return bool | |
* TRUE if the table was created, FALSE otherwise. | |
* | |
* @throws \Drupal\Component\Plugin\Exception\PluginException | |
* If a database error occurs. | |
*/ | |
protected function ensureTableExists() { | |
try { | |
if (!$this->connection->schema()->tableExists($this->table)) { | |
$this->connection->schema()->createTable($this->table, static::schemaDefinition()); | |
return TRUE; | |
} | |
} | |
catch (SchemaObjectExistsException $e) { | |
// If another process has already created the config table, attempting to | |
// recreate it will throw an exception. In this case just catch the | |
// exception and do nothing. | |
return TRUE; | |
} | |
catch (\Exception $e) { | |
throw new PluginException($e->getMessage(), NULL, $e); | |
} | |
return FALSE; | |
} | |
/** | |
* Determines serialized fields in the storage. | |
* | |
* @return array | |
* A list of fields that are serialized in the database. | |
*/ | |
protected function serializedFields() { | |
if (empty($this->serializedFields)) { | |
$schema = static::schemaDefinition(); | |
foreach ($schema['fields'] as $name => $field) { | |
if (!empty($field['serialize'])) { | |
$this->serializedFields[] = $name; | |
} | |
} | |
} | |
return $this->serializedFields; | |
} | |
/** | |
* Determines fields that are part of the plugin definition. | |
* | |
* @return array | |
* The list of the subset of fields that are part of the plugin definition. | |
*/ | |
protected function definitionFields() { | |
return $this->definitionFields; | |
} | |
/** | |
* Defines the schema for the tree table. | |
* | |
* @return array | |
* The schema API definition for the SQL storage table. | |
*/ | |
protected static function schemaDefinition() { | |
$schema = array( | |
'description' => 'Contains the menu tree hierarchy.', | |
'fields' => array( | |
'menu_name' => array( | |
'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.", | |
'type' => 'varchar_ascii', | |
'length' => 32, | |
'not null' => TRUE, | |
'default' => '', | |
), | |
'mlid' => array( | |
'description' => 'The menu link ID (mlid) is the integer primary key.', | |
'type' => 'serial', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
), | |
'id' => array( | |
'description' => 'Unique machine name: the plugin ID.', | |
'type' => 'varchar_ascii', | |
'length' => 255, | |
'not null' => TRUE, | |
), | |
'parent' => array( | |
'description' => 'The plugin ID for the parent of this link.', | |
'type' => 'varchar_ascii', | |
'length' => 255, | |
'not null' => TRUE, | |
'default' => '', | |
), | |
'route_name' => array( | |
'description' => 'The machine name of a defined Symfony Route this menu item represents.', | |
'type' => 'varchar_ascii', | |
'length' => 255, | |
), | |
'route_param_key' => array( | |
'description' => 'An encoded string of route parameters for loading by route.', | |
'type' => 'varchar', | |
'length' => 255, | |
), | |
'route_parameters' => array( | |
'description' => 'Serialized array of route parameters of this menu link.', | |
'type' => 'blob', | |
'size' => 'big', | |
'not null' => FALSE, | |
'serialize' => TRUE, | |
), | |
'url' => array( | |
'description' => 'The external path this link points to (when not using a route).', | |
'type' => 'varchar', | |
'length' => 255, | |
'not null' => TRUE, | |
'default' => '', | |
), | |
'title' => array( | |
'description' => 'The serialized title for the link. May be a TranslatableMarkup.', | |
'type' => 'blob', | |
'size' => 'big', | |
'not null' => FALSE, | |
'serialize' => TRUE, | |
), | |
'description' => array( | |
'description' => 'The serialized description of this link - used for admin pages and title attribute. May be a TranslatableMarkup.', | |
'type' => 'blob', | |
'size' => 'big', | |
'not null' => FALSE, | |
'serialize' => TRUE, | |
), | |
'class' => array( | |
'description' => 'The class for this link plugin.', | |
'type' => 'text', | |
'not null' => FALSE, | |
), | |
'options' => array( | |
'description' => 'A serialized array of URL options, such as a query string or HTML attributes.', | |
'type' => 'blob', | |
'size' => 'big', | |
'not null' => FALSE, | |
'serialize' => TRUE, | |
), | |
'provider' => array( | |
'description' => 'The name of the module that generated this link.', | |
'type' => 'varchar_ascii', | |
'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH, | |
'not null' => TRUE, | |
'default' => 'system', | |
), | |
'enabled' => array( | |
'description' => 'A flag for whether the link should be rendered in menus. (0 = a disabled menu item that may be shown on admin screens, 1 = a normal, visible link)', | |
'type' => 'int', | |
'not null' => TRUE, | |
'default' => 1, | |
'size' => 'small', | |
), | |
'discovered' => array( | |
'description' => 'A flag for whether the link was discovered, so can be purged on rebuild', | |
'type' => 'int', | |
'not null' => TRUE, | |
'default' => 0, | |
'size' => 'small', | |
), | |
'expanded' => array( | |
'description' => 'Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)', | |
'type' => 'int', | |
'not null' => TRUE, | |
'default' => 0, | |
'size' => 'small', | |
), | |
'weight' => array( | |
'description' => 'Link weight among links in the same menu at the same depth.', | |
'type' => 'int', | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'metadata' => array( | |
'description' => 'A serialized array of data that may be used by the plugin instance.', | |
'type' => 'blob', | |
'size' => 'big', | |
'not null' => FALSE, | |
'serialize' => TRUE, | |
), | |
'has_children' => array( | |
'description' => 'Flag indicating whether any enabled links have this link as a parent (1 = enabled children exist, 0 = no enabled children).', | |
'type' => 'int', | |
'not null' => TRUE, | |
'default' => 0, | |
'size' => 'small', | |
), | |
'depth' => array( | |
'description' => 'The depth relative to the top level. A link with empty parent will have depth == 1.', | |
'type' => 'int', | |
'not null' => TRUE, | |
'default' => 0, | |
'size' => 'small', | |
), | |
'p1' => array( | |
'description' => 'The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the parent link mlid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p2' => array( | |
'description' => 'The second mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p3' => array( | |
'description' => 'The third mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p4' => array( | |
'description' => 'The fourth mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p5' => array( | |
'description' => 'The fifth mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p6' => array( | |
'description' => 'The sixth mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p7' => array( | |
'description' => 'The seventh mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p8' => array( | |
'description' => 'The eighth mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'p9' => array( | |
'description' => 'The ninth mlid in the materialized path. See p1.', | |
'type' => 'int', | |
'unsigned' => TRUE, | |
'not null' => TRUE, | |
'default' => 0, | |
), | |
'form_class' => array( | |
'description' => 'meh', | |
'type' => 'varchar', | |
'length' => 255, | |
), | |
), | |
'indexes' => array( | |
'menu_parents' => array( | |
'menu_name', | |
'p1', | |
'p2', | |
'p3', | |
'p4', | |
'p5', | |
'p6', | |
'p7', | |
'p8', | |
'p9', | |
), | |
// @todo Test this index for effectiveness. | |
// https://www.drupal.org/node/2302197 | |
'menu_parent_expand_child' => array( | |
'menu_name', 'expanded', | |
'has_children', | |
array('parent', 16), | |
), | |
'route_values' => array( | |
array('route_name', 32), | |
array('route_param_key', 16), | |
), | |
), | |
'primary key' => array('mlid'), | |
'unique keys' => array( | |
'id' => array('id'), | |
), | |
); | |
return $schema; | |
} | |
/** | |
* Find any previously discovered menu links that no longer exist. | |
* | |
* @param array $definitions | |
* The new menu link definitions. | |
* @return array | |
* A list of menu link IDs that no longer exist. | |
*/ | |
protected function findNoLongerExistingLinks(array $definitions) { | |
if ($definitions) { | |
$query = $this->connection->select($this->table, NULL, $this->options); | |
$query->addField($this->table, 'id'); | |
$query->condition('discovered', 1); | |
$query->condition('id', array_keys($definitions), 'NOT IN'); | |
// Starting from links with the greatest depth will minimize the amount | |
// of re-parenting done by the menu storage. | |
$query->orderBy('depth', 'DESC'); | |
$result = $query->execute()->fetchCol(); | |
} | |
else { | |
$result = array(); | |
} | |
return $result; | |
} | |
/** | |
* Purge menu links from the database. | |
* | |
* @param array $ids | |
* A list of menu link IDs to be purged. | |
*/ | |
protected function doDeleteMultiple(array $ids) { | |
$this->connection->delete($this->table, $this->options) | |
->condition('id', $ids, 'IN') | |
->execute(); | |
} | |
} |