View source
<?php
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\DatabaseException;
use Drupal\Core\Database\Query\SelectInterface;
class MenuTreeStorage implements MenuTreeStorageInterface {
use MenuLinkFieldDefinitions;
const MAX_DEPTH = 9;
protected $connection;
protected $menuCacheBackend;
protected $cacheTagsInvalidator;
protected $table;
protected $options = [];
protected $definitions = [];
protected $serializedFields;
public function __construct(Connection $connection, CacheBackendInterface $menu_cache_backend, CacheTagsInvalidatorInterface $cache_tags_invalidator, $table, array $options = []) {
$this->connection = $connection;
$this->menuCacheBackend = $menu_cache_backend;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
$this->table = $table;
$this->options = $options;
}
public function maxDepth() {
return static::MAX_DEPTH;
}
public function resetDefinitions() {
$this->definitions = [];
}
public function rebuild(array $definitions) {
$links = [];
$children = [];
$top_links = [];
$before_menus = $this
->getMenuNames();
if ($definitions) {
foreach ($definitions as $id => $link) {
$link['discovered'] = 1;
if (!empty($link['parent'])) {
$children[$link['parent']][$id] = $id;
}
else {
$top_links[$id] = $id;
$link['parent'] = '';
}
$links[$id] = $link;
}
}
foreach ($top_links as $id) {
$this
->saveRecursive($id, $children, $links);
}
foreach ($children as $orphan_links) {
foreach ($orphan_links as $id) {
$parent = $this
->loadFull($links[$id]['parent']);
if ($parent) {
$links[$links[$id]['parent']] = $parent;
}
else {
$links[$id]['parent'] = '';
}
$this
->saveRecursive($id, $children, $links);
}
}
$result = $this
->findNoLongerExistingLinks($definitions);
if ($result) {
$this
->purgeMultiple($result);
}
$this
->resetDefinitions();
$affected_menus = $this
->getMenuNames() + $before_menus;
$cache_tags = Cache::buildTags('config:system.menu', $affected_menus, '.');
$this->cacheTagsInvalidator
->invalidateTags($cache_tags);
$this
->resetDefinitions();
$this->menuCacheBackend
->invalidateAll();
}
protected function purgeMultiple(array $ids) {
$loaded = $this
->loadFullMultiple($ids);
foreach ($loaded as $id => $link) {
if ($link['has_children']) {
$children = $this
->loadByProperties([
'parent' => $id,
]);
foreach ($children as $child) {
$child['parent'] = $link['parent'];
$this
->save($child);
}
}
}
$this
->doDeleteMultiple($ids);
}
protected function safeExecuteSelect(SelectInterface $query) {
try {
return $query
->execute();
} catch (\Exception $e) {
if ($this
->ensureTableExists()) {
return $query
->execute();
}
throw new PluginException($e
->getMessage(), 0, $e);
}
}
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;
}
protected function doSave(array $link) {
$affected_menus = [];
$query = $this->connection
->select($this->table, NULL, $this->options);
$query
->fields($this->table);
$query
->condition('id', $link['id']);
$original = $this
->safeExecuteSelect($query)
->fetchAssoc();
if ($original) {
$link['mlid'] = $original['mlid'];
$link['has_children'] = $original['has_children'];
$affected_menus[$original['menu_name']] = $original['menu_name'];
$fields = $this
->preSave($link, $original);
if (array_diff_assoc($fields, $original) == [] && array_diff_assoc($original, $fields) == [
'mlid' => $link['mlid'],
]) {
return $affected_menus;
}
}
try {
$transaction = $this->connection
->startTransaction();
if (!$original) {
$options = [
'return' => Database::RETURN_INSERT_ID,
] + $this->options;
$link['mlid'] = $this->connection
->insert($this->table, $options)
->fields([
'id' => $link['id'],
'menu_name' => $link['menu_name'],
])
->execute();
$fields = $this
->preSave($link, []);
}
$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) {
if (isset($transaction)) {
$transaction
->rollBack();
}
throw $e;
}
return $affected_menus;
}
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'];
}
}
}
$parent = $this
->findParent($link, $original);
if ($parent) {
$link['parent'] = $parent['id'];
$link['menu_name'] = $parent['menu_name'];
}
else {
$link['parent'] = '';
}
foreach ($schema_defaults as $name => $default) {
if (!isset($link[$name])) {
$link[$name] = $default;
}
}
$fields = array_intersect_key($link, $schema_fields);
asort($fields['route_parameters']);
$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);
if ($original && ($link['parent'] != $original['parent'] || $link['menu_name'] != $original['menu_name'])) {
$this
->moveChildren($fields, $original);
}
unset($fields['mlid']);
$fields['enabled'] = (int) $fields['enabled'];
$fields['expanded'] = (int) $fields['expanded'];
return $fields;
}
public function delete($id) {
$item = $this
->loadFull($id);
if ($item) {
$parent = $item['parent'];
$children = $this
->loadByProperties([
'parent' => $id,
]);
foreach ($children as $child) {
$child['parent'] = $parent;
$this
->save($child);
}
$this
->doDeleteMultiple([
$id,
]);
$this
->updateParentalStatus($item);
$this
->resetDefinitions();
$this->cacheTagsInvalidator
->invalidateTags([
'config:system.menu.' . $item['menu_name'],
]);
}
}
public function getSubtreeHeight($id) {
$original = $this
->loadFull($id);
return $original ? $this
->doFindChildrenRelativeDepth($original) + 1 : 0;
}
protected function doFindChildrenRelativeDepth(array $original) {
$query = $this->connection
->select($this->table, NULL, $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;
}
protected function setParents(array &$fields, $parent, array $original) {
if (empty($fields['parent'])) {
$fields['p1'] = $fields['mlid'];
for ($i = 2; $i <= $this
->maxDepth(); $i++) {
$fields["p{$i}"] = 0;
}
$fields['depth'] = 1;
}
else {
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++;
$fields[$p] = $fields['mlid'];
while ($i <= static::MAX_DEPTH) {
$p = 'p' . $i++;
$fields[$p] = 0;
}
}
}
protected function moveChildren($fields, $original) {
$query = $this->connection
->update($this->table, $this->options);
$query
->fields([
'menu_name' => $fields['menu_name'],
]);
$expressions = [];
for ($i = 1; $i <= $fields['depth']; $i++) {
$expressions[] = [
"p{$i}",
":p_{$i}",
[
":p_{$i}" => $fields["p{$i}"],
],
];
}
$j = $original['depth'] + 1;
while ($i <= $this
->maxDepth() && $j <= $this
->maxDepth()) {
$expressions[] = [
'p' . $i++,
'p' . $j++,
[],
];
}
while ($i <= $this
->maxDepth()) {
$expressions[] = [
'p' . $i++,
0,
[],
];
}
$shift = $fields['depth'] - $original['depth'];
if ($shift > 0) {
$expressions = array_reverse($expressions);
}
foreach ($expressions as $expression) {
$query
->expression($expression[0], $expression[1], $expression[2]);
}
$query
->expression('depth', '[depth] + :depth', [
':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();
}
protected function findParent($link, $original) {
$parent = FALSE;
if (isset($link['parent']) && empty($link['parent'])) {
return $parent;
}
$candidates = [];
if (isset($link['parent'])) {
$candidates[] = $link['parent'];
}
elseif (!empty($original['parent']) && $link['menu_name'] == $original['menu_name']) {
$candidates[] = $original['parent'];
}
foreach ($candidates as $id) {
$parent = $this
->loadFull($id);
if ($parent) {
break;
}
}
return $parent;
}
protected function updateParentalStatus(array $link) {
if (!empty($link['parent'])) {
$query = $this->connection
->select($this->table, NULL, $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([
'has_children' => $parent_has_children,
])
->condition('id', $link['parent'])
->execute();
}
}
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;
}
public function loadByProperties(array $properties) {
$query = $this->connection
->select($this->table, NULL, $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;
}
public function loadByRoute($route_name, array $route_parameters = [], $menu_name = NULL) {
asort($route_parameters);
$param_key = $route_parameters ? UrlHelper::buildQuery($route_parameters) : '';
$query = $this->connection
->select($this->table, NULL, $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);
}
$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;
}
public function loadMultiple(array $ids) {
$missing_ids = array_diff($ids, array_keys($this->definitions));
if ($missing_ids) {
$query = $this->connection
->select($this->table, NULL, $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));
}
public function load($id) {
if (isset($this->definitions[$id])) {
return $this->definitions[$id];
}
$loaded = $this
->loadMultiple([
$id,
]);
return $loaded[$id] ?? FALSE;
}
protected function loadFull($id) {
$loaded = $this
->loadFullMultiple([
$id,
]);
return $loaded[$id] ?? [];
}
protected function loadFullMultiple(array $ids) {
$query = $this->connection
->select($this->table, NULL, $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;
}
public function getRootPathIds($id) {
$subquery = $this->connection
->select($this->table, NULL, $this->options);
$subquery
->fields($this->table, [
'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, NULL, $this->options);
$query
->fields($this->table, [
'id',
]);
$query
->orderBy('depth', 'DESC');
$query
->condition('mlid', $ids, 'IN');
return $this
->safeExecuteSelect($query)
->fetchAllKeyed(0, 0);
}
return [];
}
public function getExpanded($menu_name, array $parents) {
do {
$query = $this->connection
->select($this->table, NULL, $this->options);
$query
->fields($this->table, [
'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;
}
protected function saveRecursive($id, &$children, &$links) {
if (!empty($links[$id]['parent']) && empty($links[$links[$id]['parent']])) {
$links[$id]['parent'] = '';
}
$this
->doSave($links[$id]);
if (!empty($children[$id])) {
foreach ($children[$id] as $next_id) {
$this
->saveRecursive($next_id, $children, $links);
}
}
unset($children[$id]);
}
public function loadTreeData($menu_name, MenuTreeParameters $parameters) {
$tree_cid = "tree-data:{$menu_name}:" . serialize($parameters);
$cache = $this->menuCacheBackend
->get($tree_cid);
if ($cache && isset($cache->data)) {
$data = $cache->data;
$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'] = [];
$data['route_names'] = $this
->collectRoutesAndDefinitions($data['tree'], $data['definitions']);
$this->menuCacheBackend
->set($tree_cid, $data, Cache::PERMANENT, [
'config:system.menu.' . $menu_name,
]);
unset($data['definitions']);
}
return $data;
}
protected function loadLinks($menu_name, MenuTreeParameters $parameters) {
$query = $this->connection
->select($this->table, NULL, $this->options);
$query
->fields($this->table);
if ($parameters->root !== '') {
$root = $this
->loadFull($parameters->root);
if (!$root) {
return [];
}
for ($i = 1; $i <= $root['depth']; $i++) {
$query
->condition("p{$i}", $root["p{$i}"]);
}
$menu_name = $root['menu_name'];
if (isset($parameters->minDepth)) {
$parameters->minDepth += $root['depth'];
}
else {
$parameters->minDepth = $root['depth'];
}
if (isset($parameters->maxDepth)) {
$parameters->maxDepth += $root['depth'];
}
}
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, '<=');
}
if (!empty($parameters->conditions)) {
$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;
}
protected function collectRoutesAndDefinitions(array $tree, array &$definitions) {
return array_values($this
->doCollectRoutesAndDefinitions($tree, $definitions));
}
protected function doCollectRoutesAndDefinitions(array $tree, array &$definitions) {
$route_names = [];
foreach (array_keys($tree) as $id) {
$definitions[$id] = $this->definitions[$id];
if (!empty($definitions[$id]['route_name'])) {
$route_names[$definitions[$id]['route_name']] = $definitions[$id]['route_name'];
}
if ($tree[$id]['subtree']) {
$route_names += $this
->doCollectRoutesAndDefinitions($tree[$id]['subtree'], $definitions);
}
}
return $route_names;
}
public function loadSubtreeData($id, $max_relative_depth = NULL) {
$tree = [];
$root = $this
->loadFull($id);
if (!$root) {
return $tree;
}
$parameters = new MenuTreeParameters();
$parameters
->setRoot($id)
->onlyEnabledLinks();
return $this
->loadTreeData($root['menu_name'], $parameters);
}
public function menuNameInUse($menu_name) {
$query = $this->connection
->select($this->table, NULL, $this->options);
$query
->addField($this->table, 'mlid');
$query
->condition('menu_name', $menu_name);
$query
->range(0, 1);
return (bool) $this
->safeExecuteSelect($query);
}
public function getMenuNames() {
$query = $this->connection
->select($this->table, NULL, $this->options);
$query
->addField($this->table, 'menu_name');
$query
->distinct();
return $this
->safeExecuteSelect($query)
->fetchAllKeyed(0, 0);
}
public function countMenuLinks($menu_name = NULL) {
$query = $this->connection
->select($this->table, NULL, $this->options);
if ($menu_name) {
$query
->condition('menu_name', $menu_name);
}
return $this
->safeExecuteSelect($query
->countQuery())
->fetchField();
}
public function getAllChildIds($id) {
$root = $this
->loadFull($id);
if (!$root) {
return [];
}
$query = $this->connection
->select($this->table, NULL, $this->options);
$query
->fields($this->table, [
'id',
]);
$query
->condition('menu_name', $root['menu_name']);
for ($i = 1; $i <= $root['depth']; $i++) {
$query
->condition("p{$i}", $root["p{$i}"]);
}
$query
->condition("p{$i}", 0, '>');
return $this
->safeExecuteSelect($query)
->fetchAllKeyed(0, 0);
}
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;
}
protected function doBuildTreeData(array $links, array $parents = [], $depth = 1) {
$links = array_reverse($links);
return $this
->treeDataRecursive($links, $parents, $depth);
}
protected function treeDataRecursive(array &$links, array $parents, $depth) {
$tree = [];
while ($tree_link_definition = array_pop($links)) {
$tree[$tree_link_definition['id']] = [
'definition' => $this
->prepareLink($tree_link_definition, TRUE),
'has_children' => $tree_link_definition['has_children'],
'in_active_trail' => in_array($tree_link_definition['id'], $parents),
'subtree' => [],
'depth' => $tree_link_definition['depth'],
];
$next = end($links);
if ($next && $next['depth'] > $depth) {
$tree[$tree_link_definition['id']]['subtree'] = $this
->treeDataRecursive($links, $parents, $next['depth']);
$next = end($links);
}
if (!$next || $next['depth'] < $depth) {
break;
}
}
return $tree;
}
protected function ensureTableExists() {
try {
$this->connection
->schema()
->createTable($this->table, static::schemaDefinition());
} catch (DatabaseException $e) {
} catch (\Exception $e) {
return FALSE;
}
return TRUE;
}
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;
}
protected function definitionFields() {
return array_keys($this->defaults);
}
protected static function schemaDefinition() {
$schema = [
'description' => 'Contains the menu tree hierarchy.',
'fields' => [
'menu_name' => [
'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' => [
'description' => 'The menu link ID (mlid) is the integer primary key.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'id' => [
'description' => 'Unique machine name: the plugin ID.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
],
'parent' => [
'description' => 'The plugin ID for the parent of this link.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'route_name' => [
'description' => 'The machine name of a defined Symfony Route this menu link represents.',
'type' => 'varchar_ascii',
'length' => 255,
],
'route_param_key' => [
'description' => 'An encoded string of route parameters for loading by route.',
'type' => 'varchar',
'length' => 255,
],
'route_parameters' => [
'description' => 'Serialized array of route parameters of this menu link.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
],
'url' => [
'description' => 'The external path this link points to (when not using a route).',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'title' => [
'description' => 'The serialized title for the link. May be a TranslatableMarkup.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
],
'description' => [
'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' => [
'description' => 'The class for this link plugin.',
'type' => 'text',
'not null' => FALSE,
],
'options' => [
'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' => [
'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' => [
'description' => 'A flag for whether the link should be rendered in menus. (0 = a disabled menu link that may be shown on admin screens, 1 = a normal, visible link)',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
'size' => 'small',
],
'discovered' => [
'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' => [
'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' => [
'description' => 'Link weight among links in the same menu at the same depth.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
'metadata' => [
'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' => [
'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' => [
'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' => [
'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' => [
'description' => 'The second mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p3' => [
'description' => 'The third mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p4' => [
'description' => 'The fourth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p5' => [
'description' => 'The fifth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p6' => [
'description' => 'The sixth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p7' => [
'description' => 'The seventh mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p8' => [
'description' => 'The eighth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'p9' => [
'description' => 'The ninth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'form_class' => [
'description' => 'meh',
'type' => 'varchar',
'length' => 255,
],
],
'indexes' => [
'menu_parents' => [
'menu_name',
'p1',
'p2',
'p3',
'p4',
'p5',
'p6',
'p7',
'p8',
'p9',
],
'menu_parent_expand_child' => [
'menu_name',
'expanded',
'has_children',
[
'parent',
16,
],
],
'route_values' => [
[
'route_name',
32,
],
[
'route_param_key',
16,
],
],
],
'primary key' => [
'mlid',
],
'unique keys' => [
'id' => [
'id',
],
],
];
return $schema;
}
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');
$query
->orderBy('depth', 'DESC');
$result = $query
->execute()
->fetchCol();
}
else {
$result = [];
}
return $result;
}
protected function doDeleteMultiple(array $ids) {
$this->connection
->delete($this->table, $this->options)
->condition('id', $ids, 'IN')
->execute();
}
}