diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index e60c2df9e82066a970dfc17cc33548dbcf73a2b6..621212c56f2627be153ea1ed45f42d1f5f688020 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -125,6 +125,8 @@ services: Drupal\package_manager\PathExcluder\UnknownPathExcluder: tags: - { name: event_subscriber } + calls: + - [setLogger, ['@logger.channel.package_manager']] Drupal\package_manager\PathExcluder\SiteConfigurationExcluder: arguments: $sitePath: '%site.path%' diff --git a/package_manager/src/PathExcluder/UnknownPathExcluder.php b/package_manager/src/PathExcluder/UnknownPathExcluder.php index beacdc60f7ad911999ed166023af1c0c8928abbf..1018a527f41477ff97082da7870ba72a2dacbba2 100644 --- a/package_manager/src/PathExcluder/UnknownPathExcluder.php +++ b/package_manager/src/PathExcluder/UnknownPathExcluder.php @@ -6,9 +6,14 @@ namespace Drupal\package_manager\PathExcluder; use Drupal\Component\Serialization\Json; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\package_manager\ComposerInspector; use Drupal\package_manager\Event\CollectPathsToExcludeEvent; +use Drupal\package_manager\Event\StatusCheckEvent; use Drupal\package_manager\PathLocator; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -36,7 +41,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; * at any time without warning. External code should not interact with this * class. */ -final class UnknownPathExcluder implements EventSubscriberInterface { +final class UnknownPathExcluder implements EventSubscriberInterface, LoggerAwareInterface { + + use LoggerAwareTrait; + use StringTranslationTrait; /** * Constructs a UnknownPathExcluder object. @@ -52,7 +60,9 @@ final class UnknownPathExcluder implements EventSubscriberInterface { private readonly ComposerInspector $composerInspector, private readonly PathLocator $pathLocator, private readonly ConfigFactoryInterface $configFactory, - ) {} + ) { + $this->setLogger(new NullLogger()); + } /** * {@inheritdoc} @@ -60,26 +70,28 @@ final class UnknownPathExcluder implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ CollectPathsToExcludeEvent::class => 'excludeUnknownPaths', + StatusCheckEvent::class => 'logExcludedPaths', ]; } /** - * Excludes unknown paths from stage operations. + * Returns the paths to exclude from stage operations. * - * @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event - * The event object. + * @return string[] + * The paths that should be excluded from stage operations, relative to the + * project root. * * @throws \Exception * See \Drupal\package_manager\ComposerInspector::validate(). */ - public function excludeUnknownPaths(CollectPathsToExcludeEvent $event): void { + private function getExcludedPaths(): array { // If this excluder is disabled, or the project root and web root are the - // same, there's nothing to do. + // same, we are not excluding any paths. $is_disabled = $this->configFactory->get('package_manager.settings') ->get('include_unknown_files_in_project_root'); $web_root = $this->pathLocator->getWebRoot(); if ($is_disabled || empty($web_root)) { - return; + return []; } // To determine the files to include, the installed packages must be known, @@ -115,18 +127,45 @@ final class UnknownPathExcluder implements EventSubscriberInterface { // glob() flags aren't supported on all systems. We also can't use // \Drupal\Core\File\FileSystemInterface::scanDirectory(), because it // unconditionally ignores hidden files and directories. + $files_in_project_root = []; $handle = opendir($project_root); if (empty($handle)) { throw new \RuntimeException("Could not scan for files in the project root."); } while ($entry = readdir($handle)) { - if ($entry === '.' || $entry === '..' || in_array($entry, $always_include, TRUE)) { - continue; - } - // We can add the path as-is; it's already relative to the project root. - $event->add($entry); + $files_in_project_root[] = $entry; } closedir($handle); + + return array_diff($files_in_project_root, $always_include, ['.', '..']); + } + + /** + * Excludes unknown paths from stage operations. + * + * @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent $event + * The event object. + */ + public function excludeUnknownPaths(CollectPathsToExcludeEvent $event): void { + // We can exclude the paths as-is; they are already relative to the project + // root. + $event->add(...$this->getExcludedPaths()); + } + + /** + * Logs the paths that will be excluded from stage operations. + */ + public function logExcludedPaths(): void { + $excluded_paths = $this->getExcludedPaths(); + if ($excluded_paths) { + sort($excluded_paths); + + $message = $this->t("The following paths in @project_root aren't recognized as part of your Drupal site, so to be safe, Package Manager is excluding them from all stage operations. If these files are not needed for Composer to work properly in your site, no action is needed. Otherwise, you can disable this behavior by setting the <code>package_manager.settings:include_unknown_files_in_project_root</code> config setting to <code>TRUE</code>.\n\n@list", [ + '@project_root' => $this->pathLocator->getProjectRoot(), + '@list' => implode("\n", $excluded_paths), + ]); + $this->logger->info($message); + } } /** diff --git a/package_manager/tests/src/Kernel/PathExcluder/UnknownPathExcluderTest.php b/package_manager/tests/src/Kernel/PathExcluder/UnknownPathExcluderTest.php index 6b53b1e73b26b987437c9c0bbc759e76c4b72e3d..48ed3b091ccb6c5fd7cae4638a213f46db42a3b9 100644 --- a/package_manager/tests/src/Kernel/PathExcluder/UnknownPathExcluderTest.php +++ b/package_manager/tests/src/Kernel/PathExcluder/UnknownPathExcluderTest.php @@ -4,7 +4,9 @@ declare(strict_types = 1); namespace Drupal\Tests\package_manager\Kernel\PathExcluder; +use ColinODell\PsrTestLogger\TestLogger; use Drupal\Component\FileSystem\FileSystem as DrupalFileSystem; +use Drupal\Core\Logger\RfcLogLevel; use Drupal\package_manager\PathLocator; use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase; use Symfony\Component\Filesystem\Filesystem; @@ -153,6 +155,28 @@ class UnknownPathExcluderTest extends PackageManagerKernelTestBase { } $stage = $this->createStage(); + // Files are only excluded if the web root and project root are different. + // If anything in the project root is excluded, those paths should be + // logged. + if ($use_nested_webroot) { + $logger = new TestLogger(); + $this->container->get('logger.factory') + ->get('package_manager') + ->addLogger($logger); + + $this->runStatusCheck($stage); + $this->assertTrue($logger->hasRecordThatContains("The following paths in $active_dir aren't recognized as part of your Drupal site, so to be safe, Package Manager is excluding them from all stage operations. If these files are not needed for Composer to work properly in your site, no action is needed. Otherwise, you can disable this behavior by setting the <code>package_manager.settings:include_unknown_files_in_project_root</code> config setting to <code>TRUE</code>.", RfcLogLevel::INFO)); + foreach ($unknown_files as $unknown_file) { + // If $unknown_file is in a subdirectory, only the subdirectory is going + // to be logged as an excluded path. The excluder doesn't recurse into + // subdirectories. + if (str_contains($unknown_file, '/')) { + $unknown_file = dirname($unknown_file); + } + $this->assertTrue($logger->hasRecordThatContains($unknown_file, RfcLogLevel::INFO)); + } + } + $stage->create(); $stage->require(['ext-json:*']); $stage_dir = $stage->getStageDirectory();