Unverified Commit c9562cad authored by Alex Pott's avatar Alex Pott
Browse files

fix: #3562072 Menu link content export with dependencies doesn't include parent menu link entity.

By: vishalkhode
By: phenaproxima
By: godotislate
By: alexpott
(cherry picked from commit 54bc9308)
parent 7b2531c3
Loading
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
parameters:
  menu_link_content.skip_procedural_hook_scan: true

services:
  _defaults:
    autoconfigure: true
    autowire: true
  Drupal\menu_link_content\EventSubscriber\DefaultContentSubscriber: ~
+79 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\menu_link_content\EventSubscriber;

use Drupal\Core\DefaultContent\PreExportEvent;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Subscribes to default content-related events.
 *
 * @internal
 *   Event subscribers are internal.
 */
final class DefaultContentSubscriber implements EventSubscriberInterface {

  public function __construct(
    private readonly EntityRepositoryInterface $entityRepository,
    #[AutowireServiceClosure('logger.channel.default_content')]
    private readonly \Closure $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      PreExportEvent::class => 'preExport',
    ];
  }

  /**
   * Reacts before an entity is exported.
   *
   * Adds an export callback to ensure parent menu links are marked as
   * dependencies, when exporting menu link content entities.
   *
   * @param \Drupal\Core\DefaultContent\PreExportEvent $event
   *   The event object.
   */
  public function preExport(PreExportEvent $event): void {
    if (!$event->entity instanceof MenuLinkContentInterface) {
      return;
    }

    $parentId = $event->entity->getParentId();
    if (!str_starts_with($parentId, 'menu_link_content:')) {
      return;
    }

    [, $uuid] = explode(':', $parentId);
    $parent = $this->entityRepository->loadEntityByUuid('menu_link_content', $uuid);
    if ($parent instanceof MenuLinkContentInterface) {
      $event->metadata->addDependency($parent);
      return;
    }

    $this->getLogger()->error("The parent (%parent) of menu link %uuid could not be loaded.", [
      '%parent' => $uuid,
      '%uuid' => $event->entity->uuid(),
    ]);
  }

  /**
   * Gets the logging service.
   *
   * @return \Psr\Log\LoggerInterface
   *   The logging service.
   */
  private function getLogger(): LoggerInterface {
    return ($this->logger)();
  }

}
+95 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Tests\menu_link_content\Kernel;

use ColinODell\PsrTestLogger\TestLogger;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\DefaultContent\Exporter;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\menu_link_content\EventSubscriber\DefaultContentSubscriber;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests exporting menu links in YAML format.
 */
#[Group('menu_link_content')]
#[CoversClass(DefaultContentSubscriber::class)]
#[RunTestsInSeparateProcesses]
class DefaultContentTest extends KernelTestBase {

  use ContentTypeCreationTrait;
  use NodeCreationTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'field',
    'filter',
    'link',
    'menu_link_content',
    'node',
    'system',
    'text',
    'user',
  ];

  /**
   * Tests exporting of menu link content.
   */
  public function testExportMenuLinkContent(): void {
    $this->installConfig(['filter', 'system']);
    $this->installEntitySchema('menu_link_content');
    $this->installEntitySchema('node');
    $this->installEntitySchema('user');

    $this->createContentType(['type' => 'page']);
    $parent = $this->createNode(['type' => 'page']);
    $child = $this->createNode(['type' => 'page']);
    \Drupal::service(MenuLinkManagerInterface::class)->rebuild();

    $parent_link = MenuLinkContent::create([
      'menu_name' => 'main',
      'link' => 'internal:' . $parent->toUrl()->toString(),
    ]);
    $parent_link->save();
    $child_link = MenuLinkContent::create([
      'menu_name' => 'main',
      'link' => 'internal:' . $child->toUrl()->toString(),
      'parent' => 'menu_link_content:' . $parent_link->uuid(),
    ]);
    $child_link->save();

    $logger = new TestLogger();
    \Drupal::service('logger.channel.default_content')->addLogger($logger);

    // If we export the child link, the parent should be one of its
    // dependencies.
    $data = (string) \Drupal::service(Exporter::class)->export($child_link);
    $data = Yaml::decode($data);
    $this->assertArrayHasKey($parent_link->uuid(), $data['_meta']['depends']);
    $this->assertEmpty($logger->records);

    // If we delete the parent link, exporting the child should log an error.
    $parent_link->delete();
    \Drupal::service(Exporter::class)->export($child_link);
    $predicate = function (array $record) use ($child_link, $parent_link): bool {
      return (
        $record['message'] === 'The parent (%parent) of menu link %uuid could not be loaded.' &&
        $record['context']['%parent'] === $parent_link->uuid() &&
        $record['context']['%uuid'] === $child_link->uuid()
      );
    };
    $this->assertTrue($logger->hasRecordThatPasses($predicate, RfcLogLevel::ERROR));
  }

}