monster_menus-9.0.x-dev/src/ImportExport.php
src/ImportExport.php
<?php
namespace Drupal\monster_menus;
use Drupal\content_sync\ContentSyncManager;
use Drupal\content_sync\ContentSyncManagerInterface;
use Drupal\content_sync\Importer\ContentImporter;
use Drupal\Core\Database\Database;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\monster_menus\GetTreeIterator\MMExportIter;
use Drupal\monster_menus\MMCreatePath\MMCreatePath;
use Drupal\monster_menus\MMCreatePath\MMCreatePathCat;
use Drupal\monster_menus\MMCreatePath\MMCreatePathGroup;
use Drupal\node\NodeInterface;
use Symfony\Component\Yaml\Parser;
/**
* Functions to import/export a section of the MM tree.
*/
class ImportExport {
final public const MM_IMPORT_ADD = 'add';
final public const MM_IMPORT_UPDATE = 'update';
final public const MM_IMPORT_DELETE = 'delete';
final public const MM_IMPORT_VERSION = '2.0';
/**
* Export a section of the MM tree.
*
* @param int $mmtid
* Starting location.
* @param bool $include_nodes
* If TRUE, include node contents.
*
* @return string
* PHP source code for the exported data.
* @throws MMImportExportException
* Any error condition.
*/
public static function export($mmtid, $include_nodes = FALSE) {
if ($include_nodes && !mm_module_exists('content_sync')) {
throw new MMImportExportException('The content_sync module is required to export nodes.');
}
$params = [
Constants::MM_GET_TREE_RETURN_BLOCK => TRUE,
Constants::MM_GET_TREE_RETURN_FLAGS => TRUE,
Constants::MM_GET_TREE_RETURN_NODE_COUNT => TRUE,
Constants::MM_GET_TREE_RETURN_PERMS => TRUE,
Constants::MM_GET_TREE_ITERATOR => new MMExportIter($include_nodes),
];
mm_content_get_tree($mmtid, $params);
return $params[Constants::MM_GET_TREE_ITERATOR]->dump();
}
/**
* Import a section of the MM tree.
*
* @param string $code
* The code block, from mm_export().
* @param int $start_mmtid
* The MM Tree ID of the location at which to import. The imported section
* is added (or updated) as a child of this location.
* @param string $mode
* One of the constants:
* - MM_IMPORT_ADD: Add-only: Don't change existing nodes or pages, just add
* anything new.
* - MM_IMPORT_UPDATE: Update: Overwrite existing nodes and pages, if
* different; does not modify any pre-existing nodes/pages not part of
* the import.
* - MM_IMPORT_DELETE: Delete first: Move any existing nodes and pages to a
* recycle bin before importing.
* @param bool $test
* If TRUE, go through the motions, but do not make any changes.
* @param bool $include_nodes
* If TRUE, insert or update any nodes contained in the $code.
* @param array []|string &$stats
* (optional) Array with which to populate statistics:
* - nodes:
* An array indexed by nid, containing sub-arrays with the elements
* "message" and "vars", which describe the nodes that were acted upon.
* - pages:
* An array indexed by mmtid, containing an array of sub-arrays each with
* the elements "message" and "vars", which describe the pages that were
* acted upon.
* - groups:
* An array indexed by mmtid, containing an array of sub-arrays each with
* the elements "message" and "vars", which describe the groups that were
* acted upon.
* - errors:
* An array containing sub-arrays with the elements "message" and "vars",
* which describe any errors that occurred.
* A count of the number of pages/nodes acted upon can be derived using the
* count() function.
*
* @throws MMImportExportException
* Any error condition.
*/
public static function import($code, $start_mmtid, $mode = self::MM_IMPORT_ADD, $test = FALSE, $include_nodes = FALSE, &$stats = 'undef') {
$parser = new Parser();
try {
$parsed = $parser->parse($code);
}
catch (\Exception $e) {
throw new MMImportExportException($e->getMessage());
}
$code = ''; // Save memory.
if (!isset($parsed['version']) ||
!isset($parsed['nodes']) || !is_array($parsed['nodes']) ||
!isset($parsed['page_nodes']) || !is_array($parsed['page_nodes']) ||
!isset($parsed['pool']) || !is_array($parsed['pool']) ||
!isset($parsed['pages']) || !is_array($parsed['pages'])) {
throw new MMImportExportException('mm_import: The imported data does not include all necessary sections.');
}
if (version_compare($parsed['version'], (float) self::MM_IMPORT_VERSION, '!=')) {
throw new MMImportExportException('mm_import: Incompatible version.');
}
if ($include_nodes && !mm_module_exists('content_sync')) {
throw new MMImportExportException('mm_import: The content_sync module is required to import nodes.');
}
if ($mode == self::MM_IMPORT_DELETE && $test) {
throw new MMImportExportException('mm_import: The test option cannot be used with "delete first" mode.');
}
// Import all pages in the pool into the correct object.
$groups_root = new MMCreatePathGroup(mm_content_get(mm_content_groups_mmtid()));
foreach ($parsed['pool'] as $mmtid => $page) {
$parsed['pool'][$mmtid] = $page['type'] == 'group' ? new MMCreatePathGroup($page) : new MMCreatePathCat($page);
}
// Prepend the correct groups tree root to any permission groups.
foreach ($parsed['pool'] as $page) {
foreach ([Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ] as $m) {
if (isset($page->perms[$m]['groups']) && is_array($page->perms[$m]['groups'])) {
foreach ($page->perms[$m]['groups'] as &$g) {
if (is_array($g)) {
foreach ($g as $key => $gid) {
$g[$key] = &$parsed['pool'][$gid];
}
array_unshift($g, $groups_root);
}
}
}
}
}
/** @var MMCreatePath $mm_create_path */
$mm_create_path = \Drupal::service('monster_menus.mm_create_path');
$mm_create_path->setStats($stats);
$add_only = $mode == self::MM_IMPORT_ADD;
$is_first = TRUE;
foreach ($parsed['pages'] as $elems) {
foreach ($elems as $i => $elem) {
$elems[$i] = &$parsed['pool'][$elem];
}
if ($is_first) {
$start = new MMCreatePathCat(mm_content_get($start_mmtid));
if ($mode == self::MM_IMPORT_DELETE) {
$existing = mm_content_get(['parent' => $start_mmtid, 'alias' => $elems[0]->alias]);
if ($existing) {
mm_content_move_to_bin($existing[0]->mmtid);
mm_content_update_sort_queue();
}
}
array_unshift($elems, $start);
$is_first = FALSE;
}
$mm_create_path->createPath($elems, $test, $add_only);
}
if ($include_nodes && $parsed['page_nodes']) {
if (is_array($stats) && !empty($stats['errors'])) {
_mm_report_error('Nodes were not imported, due to errors when processing pages/groups.', [], $stats);
}
else {
/** @var ContentSyncManagerInterface $cs_manager */
$cs_manager = \Drupal::service('content_sync.manager');
$entity_type_manager = $cs_manager->getEntityTypeManager();
$cs_context = [];
$changed_uuids = [];
$created_deps = [];
if (!empty($parsed['dependencies'])) {
foreach ($parsed['dependencies'] as $key => $depend) {
[$type, $bundle, $uuid] = explode(ContentSyncManager::DELIMITER, $key);
if ($changed_uuids) {
// Change the UUIDs of any referenced entities.
array_walk_recursive($depend, function(&$val, $key) use ($changed_uuids) {
if ($key == 'target_uuid' && isset($changed_uuids[$val])) {
$val = $changed_uuids[$val];
}
});
}
if ($entities = $entity_type_manager->getStorage($type)->loadByProperties(['uuid' => $uuid])) {
// Entity already exists. If updating, do nothing.
if ($mode != self::MM_IMPORT_UPDATE) {
foreach ($entities as $entity) {
$clone = $entity->createDuplicate();
if ($test) {
// When testing, track entities that would have been created.
$created_deps[$clone->uuid()] = TRUE;
}
else {
$clone->save();
}
$changed_uuids[$uuid] = $clone->uuid();
}
}
}
else {
// Create new entity.
$entity_type = $entity_type_manager->getDefinition($type);
$depend += ['type' => $bundle];
try {
$entity = $cs_manager->getSerializer()->denormalize($depend, $entity_type->getClass(), 'yaml', $cs_context);
}
catch (\Exception $e) {
$stats['errors'][] = ['message' => 'Could not create entity @id. Error: :error', 'vars' => ['@id' => $key, ':error' => $e->getMessage()]];
continue;
}
if ($test) {
// When testing, track entities that would have been created.
$created_deps[$uuid] = TRUE;
}
else {
$entity->save();
}
}
}
}
$entity_type = $entity_type_manager->getDefinition('node');
foreach ($parsed['page_nodes'] as $mmtid => $nodes) {
$new_mmtid = $parsed['pool'][$mmtid]->mmtid;
foreach ($nodes as $nid) {
$node = $parsed['nodes'][$nid];
if (is_array($node)) {
if ($changed_uuids) {
array_walk_recursive($node, function(&$val, $key) use ($changed_uuids) {
if ($key == 'target_uuid' && isset($changed_uuids[$val])) {
$val = $changed_uuids[$val];
}
});
}
// If testing, we need to remove any references to entities that
// would have been created so that the denormalize step won't fail
// when they don't exist.
if ($created_deps) {
array_walk_recursive($node, function(&$val, $key) use ($created_deps) {
if ($key == 'target_uuid' && isset($created_deps[$val])) {
$val = NULL;
}
});
}
try {
$node = $parsed['nodes'][$nid] = $cs_manager->getSerializer()->denormalize($node, $entity_type->getClass(), 'yaml', $cs_context);
}
catch (\Exception $e) {
$stats['errors'][] = ['message' => 'Could not create node @id. Error: :error', 'vars' => ['@id' => $nid, ':error' => $e->getMessage()]];
unset($parsed['nodes'][$nid]);
continue;
}
// Create any necessary groups.
$new_groups_w = [];
foreach ($node->groups_w as $g) {
$path = [$groups_root];
foreach ($g as $gid) {
$path[] = &$parsed['pool'][$gid];
}
$mm_create_path->createPath($path, $test, $add_only);
$new_groups_w[end($path)->mmtid] = '';
}
$node->groups_w = $new_groups_w;
$node->mm_catlist = $node->_mm_import_catlist = [];
}
$node->mm_catlist[$new_mmtid] = ''; // Used by MM
$node->_mm_import_catlist[] = $new_mmtid; // Internal use
}
}
if (is_array($stats)) {
$stats['nodes'] = [];
}
/** @var ContentImporter $content_importer */
$content_importer = \Drupal::service('content_sync.importer');
foreach ($parsed['nodes'] as $nid => $node) {
/** @var NodeInterface $imported_node */
$imported_node = $content_importer->prepareEntity($node);
$imported_node->__set('mm_catlist', $node->mm_catlist);
$imported_node->__set('mm_import_test', $test);
$validations = $imported_node->validate();
foreach ($validations as $i => $validation) {
// Skip spurious "This value should not be null" errors.
if ($validation->getCode() != 'ad32d13f-c3d4-423b-909a-857b961eb720') {
$validation_message = $validation->getMessage();
$stats['errors'][] = ['message' => $validation_message->getUntranslatedString(), 'vars' => $validation_message->getArguments()];
}
}
if ($mode == self::MM_IMPORT_DELETE) {
$is_new = TRUE;
$imported_node->__set('is_new', $is_new);
$imported_node->__set('uuid', \Drupal::service('uuid')->generate());
}
else {
$exists = Database::getConnection()->query('SELECT n2.nid FROM {mm_node2tree} n2 INNER JOIN {node} n ON n.nid = n2.nid WHERE n2.mmtid IN(:mmtid[]) AND n.type = :type AND (n.uuid = :uuid OR n.nid = :nid) ORDER BY n.uuid = :uuid DESC LIMIT 1', [
':mmtid[]' => $node->_mm_import_catlist,
':type' => $node->getType(),
':uuid' => $node->uuid(),
':nid' => $nid,
])->fetchField();
if ($add_only && $exists) {
// Don't save or update.
continue;
}
if ($exists) {
// Update with a new revision.
$imported_node->enforceIsNew($is_new = FALSE);
$imported_node->__set('nid', $exists);
$imported_node->__set('uuid', $node->uuid());
$imported_node->setNewRevision();
}
else {
$is_new = TRUE;
$imported_node = $imported_node->createDuplicate();
}
}
if (!$test) {
$imported_node->save();
}
if (is_array($stats)) {
if ($test) {
$msg = $is_new ? 'Would have created @title' : 'Would have updated @link';
}
else {
$msg = $is_new ? 'Created @link' : 'Updated @link';
}
$title = mm_ui_fix_node_title($imported_node->label());
$stats['nodes'][] = [
'message' => $msg,
'vars' => ['@link' => $imported_node->id() ? Link::fromTextAndUrl($title, Url::fromRoute('entity.node.canonical', ['node' => $imported_node->id()]))->toString() : $title, '@title' => $title],
];
}
}
}
}
}
}