monster_menus-9.0.x-dev/src/CheckOrphanNodes.php
src/CheckOrphanNodes.php
<?php
/**
* @file
*
* Find all nodes not associated with an MM page and optionally assign them to a
* page in /-system for possible recovery.
*/
namespace Drupal\monster_menus;
use Drupal\Core\Database\Query\TableSortExtender;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Link;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Drush\Drush;
use Psr\Log\LogLevel;
class CheckOrphanNodes {
use StringTranslationTrait;
use MessengerTrait;
/**
* Send output to the current web page (do not use in a batch script). This
* is the default method, and is used by the admin/mm/orphan-nodes menu entry.
*/
final public const OUTPUT_MODE_TABLE = 'table';
/**
* The \Drupal::logger() function (suitable for cron)
*/
final public const OUTPUT_MODE_WATCHDOG = 'watchdog';
/**
* Print the messages to standard i/o.
*/
final public const OUTPUT_MODE_PRINT = 'print';
/**
* Use when called by drush.
*/
final public const OUTPUT_MODE_DRUSH = 'drush';
/**
* The current output mode.
*
* @var string
*/
private $outputMode = self::OUTPUT_MODE_TABLE;
/**
* The database connection.
*
* @var Connection
*/
protected $database;
/**
* Constructs a CheckOrphanNodes object.
*
* @param Connection $database
* The database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* Set the output mode.
*
* @param string $mode
* One of the OUTPUT_MODE_* constants.
* @return CheckOrphanNodes
* The object, for chaining.
*/
public function setOutputMode($mode = self::OUTPUT_MODE_TABLE) {
$this->outputMode = $mode;
return $this;
}
/**
* Returns the table column header.
*
* @return array
*/
function header() {
return [
['data' => $this->t('Title'), 'field' => 'fd.title'],
['data' => $this->t('Type'), 'field' => 'fd.type'],
['data' => $this->t('Owner'), 'field' => 'fd.uid'],
['data' => $this->t('Created'), 'field' => 'fd.created'],
['data' => $this->t('Changed'), 'field' => 'fd.changed', 'sort' => 'desc'],
];
}
/**
* Find all nodes not associated with an MM page and optionally assign them to
* a page in /-system for possible recovery.
*
* @param bool $fix
* If TRUE, associate orphans with a standard page.
* @return array|null
*/
function check($fix = FALSE) {
$per_page = 20;
$table = [];
$db = Database::getConnection();
$query = $db->select('node', 'n')
->fields('n');
$query->fields($query->join('node_field_data', 'fd', 'fd.nid = n.nid'));
$query->leftJoin('mm_node2tree', 'n2', 'n2.nid = n.nid');
$query->leftJoin('mm_tree', 't', 't.mmtid = n2.mmtid');
$query = $query->isNull('t.mmtid')
->extend(TableSortExtender::class)
->orderByHeader($this->header());
$clone = clone($query);
$count = $clone->countQuery()->execute()->fetchField();
$final_error = FALSE;
if (!$count) {
$final = $this->t('No nodes were found without page assignments.');
}
else {
$strings = [
'@page' => Constants::MM_ENTRY_ALIAS_SYSTEM . '/' . Constants::MM_ENTRY_ALIAS_LOST_FOUND,
':link' => Url::fromUri('internal:/' . Constants::MM_ENTRY_ALIAS_SYSTEM . '/' . Constants::MM_ENTRY_ALIAS_LOST_FOUND)->toString(),
'@title' => mm_content_expand_name(Constants::MM_ENTRY_NAME_LOST_FOUND),
];
if ($fix) {
$final = \Drupal::translation()->formatPlural($count, '1 node was assigned, based on its type, to a subpage of <a href=":link">@title</a>.', '@count nodes were assigned, based on their type, to subpages of <a href=":link">@title</a>.', $strings);
}
else {
$final = \Drupal::translation()->formatPlural($count, 'Found 1 node without page assignments.', 'Found @count nodes without page assignments.', $strings);
if (!$fix) {
$click_msgs = [
self::OUTPUT_MODE_TABLE => 'Click here to <a href=":url">assign the node(s) to a recovery page.</a>',
self::OUTPUT_MODE_DRUSH => 'Use "drush mm-fix-orphan-nodes" to assign the node(s) to a recovery page.',
];
$click = $click_msgs[$this->outputMode] ?? 'Go to :url to assign the node(s) to a recovery page.';
$final = new FormattableMarkup(':found ' . $click, [':url' => Url::fromRoute('monster_menus.admin_orphan_nodes', [], ['query' => ['_fix' => 1]])->toString(), ':found' => $final->render()]);
}
}
if ($this->outputMode == self::OUTPUT_MODE_TABLE && !$fix) {
$query = $query->extend(PagerSelectExtender::class)->limit($per_page);
}
$query = $query->execute();
foreach ($query as $row) {
// Note: $row is the result of a direct query, so it is not a Node, but a
// StdClass.
if (!($type = NodeType::load($row->type))) {
$type_name = $this->t('Missing Content Type - @type', ['@type' => $row->type]);
}
else {
$type_name = $type->label();
}
$message = 'Node @nid has no page assignments';
if ($fix) {
$alias = preg_replace('[^\-\w]', '', $row->type);
if (ValidateSortIndex::createLostAndFound($mmtid, $message, $strings, ['alias' => $alias, 'name' => $type_name])) {
$final = $this->t($message, $strings);
$final_error = TRUE;
// Exit foreach.
break;
}
// Since it's possible for an orphan to have an entry in mm_node2tree
// which points to a nonexistent page, remove all pages first.
$db->delete('mm_node2tree')
->condition('nid', $row->nid)
->execute();
$rec = ['nid' => $row->nid, 'mmtid' => $mmtid];
\Drupal::database()->insert('mm_node2tree')->fields($rec)->execute();
$strings['@subpage'] = $alias;
$message = 'Node @nid assigned to @page/@subpage';
}
$strings['@nid'] = $row->nid;
switch ($this->outputMode) {
case self::OUTPUT_MODE_WATCHDOG:
\Drupal::logger('mm')->error($message, $strings);
break;
case self::OUTPUT_MODE_TABLE:
if (!$fix) {
$title = trim($row->title);
if (empty($title)) {
$title = $this->t('(Title not provided)');
}
$table[$row->nid] = [
Link::fromTextAndUrl($title, Url::fromRoute('entity.node.canonical', ['node' => $row->nid]))->toString(),
$type_name,
$this->t('@user (@uid)', ['@user' => mm_content_uid2name($row->uid), '@uid' => $row->uid]),
mm_format_date($row->created, 'short'),
mm_format_date($row->changed, 'short'),
];
}
break;
case self::OUTPUT_MODE_PRINT:
print $this->t($message, $strings) . "\n";
break;
case self::OUTPUT_MODE_DRUSH:
$tmsg = $this->t($message, $strings);
if ($fix) {
Drush::service('logger')->notice($tmsg);
}
else {
Drush::service('logger')->warning($tmsg);
}
break;
}
}
}
switch ($this->outputMode) {
case self::OUTPUT_MODE_WATCHDOG:
\Drupal::logger('mm')->notice($final, []);
break;
case self::OUTPUT_MODE_TABLE:
if ($final_error) {
$this->messenger()->addError($final);
}
else {
$this->messenger()->addStatus($final);
}
if ($table) {
return [
[
'#type' => 'table',
'#header' => $this->header(),
'#rows' => $table,
],
['#type' => 'pager'],
];
}
return [];
case self::OUTPUT_MODE_PRINT:
print $final . "\n";
break;
case self::OUTPUT_MODE_DRUSH:
if ($final_error) {
Drush::service('logger')->error($final);
}
else {
Drush::service('logger')->log(LogLevel::NOTICE, $final);
}
break;
}
}
}
