From 119836ad232a6a9bbd16adceea20085af95c0dac Mon Sep 17 00:00:00 2001
From: Wim Leers <44946-wimleers@users.noreply.drupalcode.org>
Date: Tue, 9 May 2023 15:59:41 +0000
Subject: [PATCH] =?UTF-8?q?Issue=20#3342817=20by=20Wim=20Leers,=20phenapro?=
 =?UTF-8?q?xima,=20tedbow:=20Decide=20which=20classes=20should=20be=20inte?=
 =?UTF-8?q?rnal=20and/or=20final=20=E2=80=94=20delete=20ExcludedPathsTrait?=
 =?UTF-8?q?,=20make=20CollectPathsToExcludeEvent=20richer?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/Event/CollectPathsToExcludeEvent.php  | 86 +++++++++++++++-
 .../src/Event/ExcludedPathsTrait.php          | 29 ------
 package_manager/src/Event/PostApplyEvent.php  |  2 +-
 package_manager/src/Event/PostCreateEvent.php |  2 +-
 .../src/Event/PostDestroyEvent.php            |  2 +-
 .../src/Event/PostRequireEvent.php            |  2 +-
 package_manager/src/Event/PreApplyEvent.php   | 19 +++-
 package_manager/src/Event/PreCreateEvent.php  | 19 +++-
 package_manager/src/Event/PreDestroyEvent.php |  2 +-
 package_manager/src/Event/PreRequireEvent.php |  2 +-
 .../src/Event/StatusCheckEvent.php            |  2 +-
 .../src/Exception/StageOwnershipException.php |  2 +-
 .../src/PathExcluder/GitExcluder.php          | 19 ++--
 .../src/PathExcluder/NodeModulesExcluder.php  | 20 +---
 .../src/PathExcluder/PathExclusionsTrait.php  | 99 -------------------
 .../SiteConfigurationExcluder.php             | 17 +---
 .../src/PathExcluder/SiteFilesExcluder.php    | 19 +---
 .../PathExcluder/SqliteDatabaseExcluder.php   | 19 ++--
 .../src/PathExcluder/TestSiteExcluder.php     | 19 +---
 .../src/PathExcluder/UnknownPathExcluder.php  | 17 ++--
 .../PathExcluder/VendorHardeningExcluder.php  | 14 +--
 package_manager/src/StageBase.php             |  2 +-
 package_manager/src/StatusCheckTrait.php      | 11 ++-
 .../src/Validator/ComposerValidator.php       |  2 +-
 .../Validator/DuplicateInfoFileValidator.php  |  2 +-
 .../Validator/StageNotInActiveValidator.php   |  2 +-
 .../src/Validator/SymlinkValidator.php        |  2 +-
 .../SqliteDatabaseExcluderTest.php            |  9 +-
 src/AutomaticUpdatesServiceProvider.php       |  7 +-
 src/ReleaseChooser.php                        |  5 +
 src/UpdateStage.php                           |  5 +
 src/Validator/RequestedUpdateValidator.php    |  7 +-
 .../StagedDatabaseUpdateValidator.php         |  2 +-
 33 files changed, 200 insertions(+), 268 deletions(-)
 delete mode 100644 package_manager/src/Event/ExcludedPathsTrait.php
 delete mode 100644 package_manager/src/PathExcluder/PathExclusionsTrait.php

diff --git a/package_manager/src/Event/CollectPathsToExcludeEvent.php b/package_manager/src/Event/CollectPathsToExcludeEvent.php
index fce262b015..facece949a 100644
--- a/package_manager/src/Event/CollectPathsToExcludeEvent.php
+++ b/package_manager/src/Event/CollectPathsToExcludeEvent.php
@@ -5,7 +5,9 @@ declare(strict_types = 1);
 namespace Drupal\package_manager\Event;
 
 use Drupal\package_manager\StageBase;
+use Drupal\package_manager\PathLocator;
 use PhpTuf\ComposerStager\Domain\Value\PathList\PathListInterface;
+use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use PhpTuf\ComposerStager\Infrastructure\Value\PathList\PathList;
 
 /**
@@ -14,7 +16,7 @@ use PhpTuf\ComposerStager\Infrastructure\Value\PathList\PathList;
  * These paths are excluded by Composer Stager and are never copied into the
  * stage directory from the active directory, or vice-versa.
  */
-class CollectPathsToExcludeEvent extends StageEvent implements PathListInterface {
+final class CollectPathsToExcludeEvent extends StageEvent implements PathListInterface {
 
   /**
    * The list of paths to exclude.
@@ -24,9 +26,20 @@ class CollectPathsToExcludeEvent extends StageEvent implements PathListInterface
   protected PathListInterface $pathList;
 
   /**
-   * {@inheritdoc}
+   * Constructs a CollectPathsToExcludeEvent object.
+   *
+   * @param \Drupal\package_manager\StageBase $stage
+   *   The stage which fired this event.
+   * @param \Drupal\package_manager\PathLocator $pathLocator
+   *   The path locator service.
+   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $pathFactory
+   *   The path factory service.
    */
-  public function __construct(StageBase $stage) {
+  public function __construct(
+    StageBase $stage,
+    protected PathLocator $pathLocator,
+    protected PathFactoryInterface $pathFactory
+  ) {
     parent::__construct($stage);
     $this->pathList = new PathList([]);
   }
@@ -45,4 +58,71 @@ class CollectPathsToExcludeEvent extends StageEvent implements PathListInterface
     return $this->pathList->getAll();
   }
 
+  /**
+   * Flags paths to be ignored, relative to the web root.
+   *
+   * This should only be used for paths that, if they exist at all, are
+   * *guaranteed* to exist within the web root.
+   *
+   * @param string[] $paths
+   *   The paths to ignore. These should be relative to the web root, and will
+   *   be made relative to the project root.
+   */
+  public function addPathsRelativeToWebRoot(array $paths): void {
+    $web_root = $this->pathLocator->getWebRoot();
+    if ($web_root) {
+      $web_root .= '/';
+    }
+
+    foreach ($paths as $path) {
+      // Make the path relative to the project root by prefixing the web root.
+      $this->add([$web_root . $path]);
+    }
+  }
+
+  /**
+   * Flags paths to be ignored, relative to the project root.
+   *
+   * @param string[] $paths
+   *   The paths to ignore. Absolute paths will be made relative to the project
+   *   root; relative paths will be assumed to already be relative to the
+   *   project root, and ignored as given.
+   */
+  public function addPathsRelativeToProjectRoot(array $paths): void {
+    $project_root = $this->pathLocator->getProjectRoot();
+
+    foreach ($paths as $path) {
+      if ($this->pathFactory->create($path)->isAbsolute()) {
+        if (!str_starts_with($path, $project_root)) {
+          throw new \LogicException("$path is not inside the project root: $project_root.");
+        }
+      }
+
+      // Make absolute paths relative to the project root.
+      $path = str_replace($project_root, '', $path);
+      $path = ltrim($path, '/');
+      $this->add([$path]);
+    }
+  }
+
+  /**
+   * Finds all directories in the project root matching the given name.
+   *
+   * @param string $directory_name
+   *   The directory name to scan for.
+   *
+   * @return string[]
+   *   All discovered absolute paths matching the given directory name.
+   */
+  public function scanForDirectoriesByName(string $directory_name): array {
+    $flags = \FilesystemIterator::UNIX_PATHS;
+    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+    $directories_tree = new \RecursiveDirectoryIterator($this->pathLocator->getProjectRoot(), $flags);
+    $filtered_directories = new \RecursiveIteratorIterator($directories_tree, \RecursiveIteratorIterator::SELF_FIRST);
+    $matched_directories = new \CallbackFilterIterator($filtered_directories,
+      fn (\RecursiveDirectoryIterator $current) => $current->isDir() && $current->getFilename() === $directory_name
+    );
+    return array_keys(iterator_to_array($matched_directories));
+  }
+
 }
diff --git a/package_manager/src/Event/ExcludedPathsTrait.php b/package_manager/src/Event/ExcludedPathsTrait.php
deleted file mode 100644
index 8e850b0af3..0000000000
--- a/package_manager/src/Event/ExcludedPathsTrait.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\package_manager\Event;
-
-/**
- * Common functionality for events which can collect excluded paths.
- */
-trait ExcludedPathsTrait {
-
-  /**
-   * Paths to exclude from the update.
-   *
-   * @var string[]
-   */
-  protected $excludedPaths = [];
-
-  /**
-   * Returns the paths to exclude from the current operation.
-   *
-   * @return string[]
-   *   The paths to exclude.
-   */
-  public function getExcludedPaths(): array {
-    return array_unique($this->excludedPaths);
-  }
-
-}
diff --git a/package_manager/src/Event/PostApplyEvent.php b/package_manager/src/Event/PostApplyEvent.php
index b7ce5c2709..a83057cf72 100644
--- a/package_manager/src/Event/PostApplyEvent.php
+++ b/package_manager/src/Event/PostApplyEvent.php
@@ -7,5 +7,5 @@ namespace Drupal\package_manager\Event;
 /**
  * Event fired after staged changes are synced to the active directory.
  */
-class PostApplyEvent extends StageEvent {
+final class PostApplyEvent extends StageEvent {
 }
diff --git a/package_manager/src/Event/PostCreateEvent.php b/package_manager/src/Event/PostCreateEvent.php
index 96ced349d3..b5db572884 100644
--- a/package_manager/src/Event/PostCreateEvent.php
+++ b/package_manager/src/Event/PostCreateEvent.php
@@ -7,5 +7,5 @@ namespace Drupal\package_manager\Event;
 /**
  * Event fired after a stage directory has been created.
  */
-class PostCreateEvent extends StageEvent {
+final class PostCreateEvent extends StageEvent {
 }
diff --git a/package_manager/src/Event/PostDestroyEvent.php b/package_manager/src/Event/PostDestroyEvent.php
index 51d72b49c0..b68d214f9e 100644
--- a/package_manager/src/Event/PostDestroyEvent.php
+++ b/package_manager/src/Event/PostDestroyEvent.php
@@ -12,5 +12,5 @@ namespace Drupal\package_manager\Event;
  *
  * @see \Drupal\package_manager\StageBase::destroy()
  */
-class PostDestroyEvent extends StageEvent {
+final class PostDestroyEvent extends StageEvent {
 }
diff --git a/package_manager/src/Event/PostRequireEvent.php b/package_manager/src/Event/PostRequireEvent.php
index 3ead93c825..ed92adb40b 100644
--- a/package_manager/src/Event/PostRequireEvent.php
+++ b/package_manager/src/Event/PostRequireEvent.php
@@ -7,7 +7,7 @@ namespace Drupal\package_manager\Event;
 /**
  * Event fired after packages are updated to the stage directory.
  */
-class PostRequireEvent extends StageEvent {
+final class PostRequireEvent extends StageEvent {
 
   use RequireEventTrait;
 
diff --git a/package_manager/src/Event/PreApplyEvent.php b/package_manager/src/Event/PreApplyEvent.php
index 87fbb05848..9dac5cc54d 100644
--- a/package_manager/src/Event/PreApplyEvent.php
+++ b/package_manager/src/Event/PreApplyEvent.php
@@ -9,9 +9,24 @@ use Drupal\package_manager\StageBase;
 /**
  * Event fired before staged changes are synced to the active directory.
  */
-class PreApplyEvent extends PreOperationStageEvent {
+final class PreApplyEvent extends PreOperationStageEvent {
 
-  use ExcludedPathsTrait;
+  /**
+   * Paths to exclude from the update.
+   *
+   * @var string[]
+   */
+  protected array $excludedPaths = [];
+
+  /**
+   * Returns the paths to exclude from the current operation.
+   *
+   * @return string[]
+   *   The paths to exclude.
+   */
+  public function getExcludedPaths(): array {
+    return array_unique($this->excludedPaths);
+  }
 
   /**
    * Constructs a PreApplyEvent object.
diff --git a/package_manager/src/Event/PreCreateEvent.php b/package_manager/src/Event/PreCreateEvent.php
index 2a369c62ce..9c775cadb4 100644
--- a/package_manager/src/Event/PreCreateEvent.php
+++ b/package_manager/src/Event/PreCreateEvent.php
@@ -9,9 +9,24 @@ use Drupal\package_manager\StageBase;
 /**
  * Event fired before a stage directory is created.
  */
-class PreCreateEvent extends PreOperationStageEvent {
+final class PreCreateEvent extends PreOperationStageEvent {
 
-  use ExcludedPathsTrait;
+  /**
+   * Paths to exclude from the update.
+   *
+   * @var string[]
+   */
+  protected array $excludedPaths = [];
+
+  /**
+   * Returns the paths to exclude from the current operation.
+   *
+   * @return string[]
+   *   The paths to exclude.
+   */
+  public function getExcludedPaths(): array {
+    return array_unique($this->excludedPaths);
+  }
 
   /**
    * Constructs a PreCreateEvent object.
diff --git a/package_manager/src/Event/PreDestroyEvent.php b/package_manager/src/Event/PreDestroyEvent.php
index e9108f7daf..d51e2a76e9 100644
--- a/package_manager/src/Event/PreDestroyEvent.php
+++ b/package_manager/src/Event/PreDestroyEvent.php
@@ -12,5 +12,5 @@ namespace Drupal\package_manager\Event;
  *
  * @see \Drupal\package_manager\StageBase::destroy()
  */
-class PreDestroyEvent extends PreOperationStageEvent {
+final class PreDestroyEvent extends PreOperationStageEvent {
 }
diff --git a/package_manager/src/Event/PreRequireEvent.php b/package_manager/src/Event/PreRequireEvent.php
index c9b9905a42..a38478ab34 100644
--- a/package_manager/src/Event/PreRequireEvent.php
+++ b/package_manager/src/Event/PreRequireEvent.php
@@ -7,7 +7,7 @@ namespace Drupal\package_manager\Event;
 /**
  * Event fired before packages are updated to the stage directory.
  */
-class PreRequireEvent extends PreOperationStageEvent {
+final class PreRequireEvent extends PreOperationStageEvent {
 
   use RequireEventTrait;
 
diff --git a/package_manager/src/Event/StatusCheckEvent.php b/package_manager/src/Event/StatusCheckEvent.php
index 443e2a8856..41dc77cde7 100644
--- a/package_manager/src/Event/StatusCheckEvent.php
+++ b/package_manager/src/Event/StatusCheckEvent.php
@@ -15,7 +15,7 @@ use Drupal\system\SystemManager;
  * The event's stage will be set with the type of stage that will perform the
  * operations. The stage may or may not be currently in use.
  */
-class StatusCheckEvent extends PreOperationStageEvent {
+final class StatusCheckEvent extends PreOperationStageEvent {
 
   /**
    * Returns paths to exclude or NULL if a base requirement is not fulfilled.
diff --git a/package_manager/src/Exception/StageOwnershipException.php b/package_manager/src/Exception/StageOwnershipException.php
index ad1f176e61..a1dc2608e9 100644
--- a/package_manager/src/Exception/StageOwnershipException.php
+++ b/package_manager/src/Exception/StageOwnershipException.php
@@ -9,5 +9,5 @@ namespace Drupal\package_manager\Exception;
  *
  * Should not be thrown by external code.
  */
-class StageOwnershipException extends StageException {
+final class StageOwnershipException extends StageException {
 }
diff --git a/package_manager/src/PathExcluder/GitExcluder.php b/package_manager/src/PathExcluder/GitExcluder.php
index f8e2e77c38..7bc54d4350 100644
--- a/package_manager/src/PathExcluder/GitExcluder.php
+++ b/package_manager/src/PathExcluder/GitExcluder.php
@@ -7,7 +7,6 @@ namespace Drupal\package_manager\PathExcluder;
 use Drupal\package_manager\ComposerInspector;
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
 use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -20,22 +19,18 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 final class GitExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
   /**
    * Constructs a GitExcluder object.
    *
-   * @param \Drupal\package_manager\PathLocator $path_locator
+   * @param \Drupal\package_manager\PathLocator $pathLocator
    *   The path locator service.
    * @param \Drupal\package_manager\ComposerInspector $composerInspector
    *   The Composer inspector service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
    */
-  public function __construct(PathLocator $path_locator, private readonly ComposerInspector $composerInspector, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
+  public function __construct(
+    private readonly PathLocator $pathLocator,
+    private readonly ComposerInspector $composerInspector
+  ) {}
 
   /**
    * {@inheritdoc}
@@ -77,7 +72,7 @@ final class GitExcluder implements EventSubscriberInterface {
         $installed_paths[] = $package->path;
       }
     }
-    $paths = $this->scanForDirectoriesByName('.git');
+    $paths = $event->scanForDirectoriesByName('.git');
     foreach ($paths as $git_directory) {
       // Don't exclude any `.git` directory that is directly under an installed
       // package's path, since it means Composer probably installed that package
@@ -87,7 +82,7 @@ final class GitExcluder implements EventSubscriberInterface {
         $paths_to_exclude[] = $git_directory;
       }
     }
-    $this->excludeInProjectRoot($event, $paths_to_exclude);
+    $event->addPathsRelativeToProjectRoot($paths_to_exclude);
   }
 
 }
diff --git a/package_manager/src/PathExcluder/NodeModulesExcluder.php b/package_manager/src/PathExcluder/NodeModulesExcluder.php
index 9f07bf205f..a954a0c103 100644
--- a/package_manager/src/PathExcluder/NodeModulesExcluder.php
+++ b/package_manager/src/PathExcluder/NodeModulesExcluder.php
@@ -5,8 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\package_manager\PathExcluder;
 
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
-use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -19,21 +17,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 class NodeModulesExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
-  /**
-   * Constructs a NodeModulesExcluder object.
-   *
-   * @param \Drupal\package_manager\PathLocator $path_locator
-   *   The path locator service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
-   */
-  public function __construct(PathLocator $path_locator, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
-
   /**
    * Excludes node_modules directories from stage operations.
    *
@@ -41,8 +24,7 @@ class NodeModulesExcluder implements EventSubscriberInterface {
    *   The event object.
    */
   public function excludeNodeModulesFiles(CollectPathsToExcludeEvent $event): void {
-    $paths = $this->scanForDirectoriesByName('node_modules');
-    $this->excludeInProjectRoot($event, $paths);
+    $event->addPathsRelativeToProjectRoot($event->scanForDirectoriesByName('node_modules'));
   }
 
   /**
diff --git a/package_manager/src/PathExcluder/PathExclusionsTrait.php b/package_manager/src/PathExcluder/PathExclusionsTrait.php
deleted file mode 100644
index 5a3bb9183e..0000000000
--- a/package_manager/src/PathExcluder/PathExclusionsTrait.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\package_manager\PathExcluder;
-
-use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
-
-/**
- * Contains methods for excluding paths from stage operations.
- */
-trait PathExclusionsTrait {
-
-  /**
-   * The path locator service.
-   *
-   * @var \Drupal\package_manager\PathLocator
-   */
-  protected $pathLocator;
-
-  /**
-   * The path factory service.
-   *
-   * @var \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface
-   */
-  protected $pathFactory;
-
-  /**
-   * Flags paths to be excluded, relative to the web root.
-   *
-   * This should only be used for paths that, if they exist at all, are
-   * *guaranteed* to exist within the web root.
-   *
-   * @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent|\Drupal\package_manager\Event\StageEvent $event
-   *   The event object.
-   * @param string[] $paths
-   *   The paths to exclude. These should be relative to the web root, and will
-   *   be made relative to the project root.
-   */
-  protected function excludeInWebRoot(CollectPathsToExcludeEvent $event, array $paths): void {
-    $web_root = $this->pathLocator->getWebRoot();
-    if ($web_root) {
-      $web_root .= '/';
-    }
-
-    foreach ($paths as $path) {
-      // Make the path relative to the project root by prefixing the web root.
-      $event->add([$web_root . $path]);
-    }
-  }
-
-  /**
-   * Flags paths to be excluded, relative to the project root.
-   *
-   * @param \Drupal\package_manager\Event\CollectPathsToExcludeEvent|\Drupal\package_manager\Event\StageEvent $event
-   *   The event object.
-   * @param string[] $paths
-   *   The paths to exclude. Absolute paths will be made relative to the project
-   *   root; relative paths will be assumed to already be relative to the
-   *   project root, and excluded as given.
-   */
-  protected function excludeInProjectRoot(CollectPathsToExcludeEvent $event, array $paths): void {
-    $project_root = $this->pathLocator->getProjectRoot();
-
-    foreach ($paths as $path) {
-      if ($this->pathFactory->create($path)->isAbsolute()) {
-        if (!str_starts_with($path, $project_root)) {
-          throw new \LogicException("$path is not inside the project root: $project_root.");
-        }
-      }
-
-      // Make absolute paths relative to the project root.
-      $path = str_replace($project_root, '', $path);
-      $path = ltrim($path, '/');
-      $event->add([$path]);
-    }
-  }
-
-  /**
-   * Finds all directories in the project root matching the given name.
-   *
-   * @param string $directory_name
-   *   The directory name to scan for.
-   *
-   * @return string[]
-   *   All discovered absolute paths matching the given directory name.
-   */
-  protected function scanForDirectoriesByName(string $directory_name): array {
-    $flags = \FilesystemIterator::UNIX_PATHS;
-    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
-    $directories_tree = new \RecursiveDirectoryIterator($this->pathLocator->getProjectRoot(), $flags);
-    $filtered_directories = new \RecursiveIteratorIterator($directories_tree, \RecursiveIteratorIterator::SELF_FIRST);
-    $matched_directories = new \CallbackFilterIterator($filtered_directories,
-      fn (\RecursiveDirectoryIterator $current) => $current->isDir() && $current->getFilename() === $directory_name
-    );
-    return array_keys(iterator_to_array($matched_directories));
-  }
-
-}
diff --git a/package_manager/src/PathExcluder/SiteConfigurationExcluder.php b/package_manager/src/PathExcluder/SiteConfigurationExcluder.php
index 92b0b43004..7a4df3b2d0 100644
--- a/package_manager/src/PathExcluder/SiteConfigurationExcluder.php
+++ b/package_manager/src/PathExcluder/SiteConfigurationExcluder.php
@@ -5,8 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\package_manager\PathExcluder;
 
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
-use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -19,22 +17,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 class SiteConfigurationExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
   /**
-   * Constructs an ExcludedPathsSubscriber.
+   * Constructs an SiteConfigurationExcluder.
    *
    * @param string $sitePath
    *   The current site path, relative to the Drupal root.
-   * @param \Drupal\package_manager\PathLocator $path_locator
-   *   The path locator service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
    */
-  public function __construct(protected string $sitePath, PathLocator $path_locator, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
+  public function __construct(protected string $sitePath) {}
 
   /**
    * Excludes site configuration files from stage operations.
@@ -57,7 +46,7 @@ class SiteConfigurationExcluder implements EventSubscriberInterface {
       $paths[] = $this->sitePath . '/' . $settings_file;
       $paths[] = 'sites/default/' . $settings_file;
     }
-    $this->excludeInWebRoot($event, $paths);
+    $event->addPathsRelativeToWebRoot($paths);
   }
 
   /**
diff --git a/package_manager/src/PathExcluder/SiteFilesExcluder.php b/package_manager/src/PathExcluder/SiteFilesExcluder.php
index ce23ee3756..cf0a71cb03 100644
--- a/package_manager/src/PathExcluder/SiteFilesExcluder.php
+++ b/package_manager/src/PathExcluder/SiteFilesExcluder.php
@@ -7,8 +7,6 @@ namespace Drupal\package_manager\PathExcluder;
 use Drupal\Core\StreamWrapper\LocalStream;
 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
-use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\Filesystem\Filesystem;
 
@@ -22,29 +20,18 @@ use Symfony\Component\Filesystem\Filesystem;
  */
 final class SiteFilesExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
   /**
    * Constructs a SiteFilesExcluder object.
    *
-   * @param \Drupal\package_manager\PathLocator $path_locator
-   *   The path locator service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
    *   The stream wrapper manager service.
    * @param \Symfony\Component\Filesystem\Filesystem $fileSystem
    *   The Symfony file system service.
    */
   public function __construct(
-    PathLocator $path_locator,
-    PathFactoryInterface $path_factory,
     private readonly StreamWrapperManagerInterface $streamWrapperManager,
     private readonly Filesystem $fileSystem
-  ) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
+  ) {}
 
   /**
    * {@inheritdoc}
@@ -72,10 +59,10 @@ final class SiteFilesExcluder implements EventSubscriberInterface {
         $path = $wrapper->getDirectoryPath();
 
         if ($this->fileSystem->isAbsolutePath($path)) {
-          $this->excludeInProjectRoot($event, [realpath($path)]);
+          $event->addPathsRelativeToProjectRoot([realpath($path)]);
         }
         else {
-          $this->excludeInWebRoot($event, [$path]);
+          $event->addPathsRelativeToWebRoot([$path]);
         }
       }
     }
diff --git a/package_manager/src/PathExcluder/SqliteDatabaseExcluder.php b/package_manager/src/PathExcluder/SqliteDatabaseExcluder.php
index 42bb065516..cfcf1fc95b 100644
--- a/package_manager/src/PathExcluder/SqliteDatabaseExcluder.php
+++ b/package_manager/src/PathExcluder/SqliteDatabaseExcluder.php
@@ -7,7 +7,6 @@ namespace Drupal\package_manager\PathExcluder;
 use Drupal\Core\Database\Connection;
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
 use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -20,22 +19,20 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 class SqliteDatabaseExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
   /**
    * Constructs a SqliteDatabaseExcluder object.
    *
-   * @param \Drupal\package_manager\PathLocator $path_locator
+   * @param \Drupal\package_manager\PathLocator $pathLocator
    *   The path locator service.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
    */
-  public function __construct(PathLocator $path_locator, protected Connection $database, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
+  public function __construct(
+    private readonly PathLocator $pathLocator,
+    // TRICKY: this cannot be private nor readonly for testing purposes.
+    // @see \Drupal\Tests\package_manager\Kernel\PathExcluder\SqliteDatabaseExcluderTest::mockDatabase()
+    protected Connection $database
+  ) {}
 
   /**
    * {@inheritdoc}
@@ -61,7 +58,7 @@ class SqliteDatabaseExcluder implements EventSubscriberInterface {
       if (str_starts_with($options['database'], '/') && !str_starts_with($options['database'], $this->pathLocator->getProjectRoot())) {
         return;
       }
-      $this->excludeInProjectRoot($event, [
+      $event->addPathsRelativeToProjectRoot([
         $options['database'],
         $options['database'] . '-shm',
         $options['database'] . '-wal',
diff --git a/package_manager/src/PathExcluder/TestSiteExcluder.php b/package_manager/src/PathExcluder/TestSiteExcluder.php
index f3a5080f36..59b9b2edd9 100644
--- a/package_manager/src/PathExcluder/TestSiteExcluder.php
+++ b/package_manager/src/PathExcluder/TestSiteExcluder.php
@@ -5,8 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\package_manager\PathExcluder;
 
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
-use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -19,21 +17,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 final class TestSiteExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
-  /**
-   * Constructs a TestSiteExcluder object.
-   *
-   * @param \Drupal\package_manager\PathLocator $path_locator
-   *   The path locator service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
-   */
-  public function __construct(PathLocator $path_locator, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -52,7 +35,7 @@ final class TestSiteExcluder implements EventSubscriberInterface {
   public function excludeTestSites(CollectPathsToExcludeEvent $event): void {
     // Always exclude automated test directories. If they exist, they will be in
     // the web root.
-    $this->excludeInWebRoot($event, ['sites/simpletest']);
+    $event->addPathsRelativeToWebRoot(['sites/simpletest']);
   }
 
 }
diff --git a/package_manager/src/PathExcluder/UnknownPathExcluder.php b/package_manager/src/PathExcluder/UnknownPathExcluder.php
index 9150bc3c0a..e7295b1891 100644
--- a/package_manager/src/PathExcluder/UnknownPathExcluder.php
+++ b/package_manager/src/PathExcluder/UnknownPathExcluder.php
@@ -8,7 +8,6 @@ use Drupal\Component\Serialization\Json;
 use Drupal\package_manager\ComposerInspector;
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
 use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -31,22 +30,18 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 final class UnknownPathExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
   /**
    * Constructs a UnknownPathExcluder object.
    *
    * @param \Drupal\package_manager\ComposerInspector $composerInspector
    *   The Composer inspector service.
-   * @param \Drupal\package_manager\PathLocator $path_locator
+   * @param \Drupal\package_manager\PathLocator $pathLocator
    *   The path locator service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
    */
-  public function __construct(private readonly ComposerInspector $composerInspector, PathLocator $path_locator, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
+  public function __construct(
+    private readonly ComposerInspector $composerInspector,
+    private readonly PathLocator $pathLocator,
+  ) {}
 
   /**
    * {@inheritdoc}
@@ -93,7 +88,7 @@ final class UnknownPathExcluder implements EventSubscriberInterface {
         $paths[] = $path_in_project_root;
       }
     }
-    $this->excludeInProjectRoot($event, $paths);
+    $event->addPathsRelativeToProjectRoot($paths);
   }
 
   /**
diff --git a/package_manager/src/PathExcluder/VendorHardeningExcluder.php b/package_manager/src/PathExcluder/VendorHardeningExcluder.php
index 509516390a..2d46271b20 100644
--- a/package_manager/src/PathExcluder/VendorHardeningExcluder.php
+++ b/package_manager/src/PathExcluder/VendorHardeningExcluder.php
@@ -6,7 +6,6 @@ namespace Drupal\package_manager\PathExcluder;
 
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
 use Drupal\package_manager\PathLocator;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -19,20 +18,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 final class VendorHardeningExcluder implements EventSubscriberInterface {
 
-  use PathExclusionsTrait;
-
   /**
    * Constructs a VendorHardeningExcluder object.
    *
-   * @param \Drupal\package_manager\PathLocator $path_locator
+   * @param \Drupal\package_manager\PathLocator $pathLocator
    *   The path locator service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
-   *   The path factory service.
    */
-  public function __construct(PathLocator $path_locator, PathFactoryInterface $path_factory) {
-    $this->pathLocator = $path_locator;
-    $this->pathFactory = $path_factory;
-  }
+  public function __construct(private readonly PathLocator $pathLocator) {}
 
   /**
    * {@inheritdoc}
@@ -54,7 +46,7 @@ final class VendorHardeningExcluder implements EventSubscriberInterface {
     // is present, it may have written security hardening files in the vendor
     // directory. They should always be excluded.
     $vendor_dir = $this->pathLocator->getVendorDirectory();
-    $this->excludeInProjectRoot($event, [
+    $event->addPathsRelativeToProjectRoot([
       $vendor_dir . '/web.config',
       $vendor_dir . '/.htaccess',
     ]);
diff --git a/package_manager/src/StageBase.php b/package_manager/src/StageBase.php
index af98358181..49d57c828e 100644
--- a/package_manager/src/StageBase.php
+++ b/package_manager/src/StageBase.php
@@ -263,7 +263,7 @@ abstract class StageBase implements LoggerAwareInterface {
    * @see ::apply()
    */
   protected function getPathsToExclude(): array {
-    $event = new CollectPathsToExcludeEvent($this);
+    $event = new CollectPathsToExcludeEvent($this, $this->pathLocator, $this->pathFactory);
     try {
       $this->eventDispatcher->dispatch($event);
     }
diff --git a/package_manager/src/StatusCheckTrait.php b/package_manager/src/StatusCheckTrait.php
index 3de0a1b6b2..4bad25258d 100644
--- a/package_manager/src/StatusCheckTrait.php
+++ b/package_manager/src/StatusCheckTrait.php
@@ -6,6 +6,7 @@ namespace Drupal\package_manager;
 
 use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
 use Drupal\package_manager\Event\StatusCheckEvent;
+use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 /**
@@ -25,15 +26,21 @@ trait StatusCheckTrait {
    *   The stage to run the status check for.
    * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   (optional) The event dispatcher service.
+   * @param \Drupal\package_manager\PathLocator $path_locator
+   *   (optional) The path locator service.
+   * @param \PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface $path_factory
+   *   (optional) The path factory service.
    *
    * @return \Drupal\package_manager\ValidationResult[]
    *   The results of the status check. If a readiness check was also done,
    *   its results will be included.
    */
-  protected function runStatusCheck(StageBase $stage, EventDispatcherInterface $event_dispatcher = NULL): array {
+  protected function runStatusCheck(StageBase $stage, EventDispatcherInterface $event_dispatcher = NULL, PathLocator $path_locator = NULL, PathFactoryInterface $path_factory = NULL): array {
     $event_dispatcher ??= \Drupal::service('event_dispatcher');
+    $path_locator ??= \Drupal::service(PathLocator::class);
+    $path_factory ??= \Drupal::service(PathFactoryInterface::class);
     try {
-      $paths_to_exclude_event = new CollectPathsToExcludeEvent($stage);
+      $paths_to_exclude_event = new CollectPathsToExcludeEvent($stage, $path_locator, $path_factory);
       $event_dispatcher->dispatch($paths_to_exclude_event);
       $event = new StatusCheckEvent($stage, $paths_to_exclude_event->getAll());
     }
diff --git a/package_manager/src/Validator/ComposerValidator.php b/package_manager/src/Validator/ComposerValidator.php
index 09a1123748..e4aa6ced61 100644
--- a/package_manager/src/Validator/ComposerValidator.php
+++ b/package_manager/src/Validator/ComposerValidator.php
@@ -21,7 +21,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  *   at any time without warning. External code should not interact with this
  *   class.
  */
-class ComposerValidator implements EventSubscriberInterface {
+final class ComposerValidator implements EventSubscriberInterface {
 
   use BaseRequirementValidatorTrait;
   use StringTranslationTrait;
diff --git a/package_manager/src/Validator/DuplicateInfoFileValidator.php b/package_manager/src/Validator/DuplicateInfoFileValidator.php
index beea0bc313..644d37bde4 100644
--- a/package_manager/src/Validator/DuplicateInfoFileValidator.php
+++ b/package_manager/src/Validator/DuplicateInfoFileValidator.php
@@ -18,7 +18,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  *   at any time without warning. External code should not interact with this
  *   class.
  */
-class DuplicateInfoFileValidator implements EventSubscriberInterface {
+final class DuplicateInfoFileValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
diff --git a/package_manager/src/Validator/StageNotInActiveValidator.php b/package_manager/src/Validator/StageNotInActiveValidator.php
index 956641452b..d38831097e 100644
--- a/package_manager/src/Validator/StageNotInActiveValidator.php
+++ b/package_manager/src/Validator/StageNotInActiveValidator.php
@@ -18,7 +18,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  *   at any time without warning. External code should not interact with this
  *   class.
  */
-class StageNotInActiveValidator implements EventSubscriberInterface {
+final class StageNotInActiveValidator implements EventSubscriberInterface {
 
   use BaseRequirementValidatorTrait {
     getSubscribedEvents as protected getSubscribedEventsFromTrait;
diff --git a/package_manager/src/Validator/SymlinkValidator.php b/package_manager/src/Validator/SymlinkValidator.php
index 12d84007a9..e30ac93f3c 100644
--- a/package_manager/src/Validator/SymlinkValidator.php
+++ b/package_manager/src/Validator/SymlinkValidator.php
@@ -22,7 +22,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  *   at any time without warning. External code should not interact with this
  *   class.
  */
-class SymlinkValidator implements EventSubscriberInterface {
+final class SymlinkValidator implements EventSubscriberInterface {
 
   use BaseRequirementValidatorTrait;
 
diff --git a/package_manager/tests/src/Kernel/PathExcluder/SqliteDatabaseExcluderTest.php b/package_manager/tests/src/Kernel/PathExcluder/SqliteDatabaseExcluderTest.php
index 7d75dbb659..05fa39946e 100644
--- a/package_manager/tests/src/Kernel/PathExcluder/SqliteDatabaseExcluderTest.php
+++ b/package_manager/tests/src/Kernel/PathExcluder/SqliteDatabaseExcluderTest.php
@@ -10,6 +10,7 @@ use Drupal\package_manager\Event\CollectPathsToExcludeEvent;
 use Drupal\package_manager\PathExcluder\SqliteDatabaseExcluder;
 use Drupal\package_manager\PathLocator;
 use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
+use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 
 /**
  * @covers \Drupal\package_manager\PathExcluder\SqliteDatabaseExcluder
@@ -144,22 +145,24 @@ class SqliteDatabaseExcluderTest extends PackageManagerKernelTestBase {
    * @dataProvider providerPathProcessing
    */
   public function testPathProcessing(string $database_path, array $expected_exclusions): void {
+    $path_locator = $this->container->get(PathLocator::class);
+    $path_factory = $this->container->get(PathFactoryInterface::class);
     // If the database path should be treated as absolute, prefix it with the
     // path of the active directory.
     if (str_starts_with($database_path, '/')) {
-      $database_path = $this->container->get(PathLocator::class)->getProjectRoot() . $database_path;
+      $database_path = $path_locator->getProjectRoot() . $database_path;
     }
     $this->mockDatabase([
       'database' => $database_path,
     ]);
 
-    $event = new CollectPathsToExcludeEvent($this->createStage());
+    $event = new CollectPathsToExcludeEvent($this->createStage(), $path_locator, $path_factory);
     // Invoke the event subscriber directly, so we can check that the database
     // was correctly excluded.
     $this->container->get(SqliteDatabaseExcluder::class)
       ->excludeDatabaseFiles($event);
     // All of the expected exclusions should be flagged.
-    $this->assertEmpty(array_diff($expected_exclusions, $event->getAll()));
+    $this->assertEquals($expected_exclusions, $event->getAll());
   }
 
 }
diff --git a/src/AutomaticUpdatesServiceProvider.php b/src/AutomaticUpdatesServiceProvider.php
index cdd97cf7ba..cc6c502523 100644
--- a/src/AutomaticUpdatesServiceProvider.php
+++ b/src/AutomaticUpdatesServiceProvider.php
@@ -10,8 +10,13 @@ use Drupal\Core\DependencyInjection\ServiceProviderBase;
 
 /**
  * Modifies container services for Automatic Updates.
+ *
+ * @internal
+ *   This is an internal part of Automatic Updates and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
  */
-class AutomaticUpdatesServiceProvider extends ServiceProviderBase {
+final class AutomaticUpdatesServiceProvider extends ServiceProviderBase {
 
   /**
    * {@inheritdoc}
diff --git a/src/ReleaseChooser.php b/src/ReleaseChooser.php
index 83523493e7..caa8cc19f9 100644
--- a/src/ReleaseChooser.php
+++ b/src/ReleaseChooser.php
@@ -12,6 +12,11 @@ use Drupal\Core\Extension\ExtensionVersion;
 
 /**
  * Defines a class to choose a release of Drupal core to update to.
+ *
+ * @internal
+ *   This is an internal part of Automatic Updates and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
  */
 final class ReleaseChooser {
 
diff --git a/src/UpdateStage.php b/src/UpdateStage.php
index 9cb2501a8e..f5bf4e0f66 100644
--- a/src/UpdateStage.php
+++ b/src/UpdateStage.php
@@ -25,6 +25,11 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  * changing the constraint for either 'drupal/core' or 'drupal/core-recommended'
  * in the project-level composer.json. If neither package is directly required
  * in the project-level composer.json, a requirement will be added.
+ *
+ * @internal
+ *   This is an internal part of Automatic Updates and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
  */
 class UpdateStage extends StageBase {
 
diff --git a/src/Validator/RequestedUpdateValidator.php b/src/Validator/RequestedUpdateValidator.php
index 60e09c88d1..fc3bcea5c9 100644
--- a/src/Validator/RequestedUpdateValidator.php
+++ b/src/Validator/RequestedUpdateValidator.php
@@ -14,8 +14,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
  * Validates that requested packages have been updated.
+ *
+ * @internal
+ *   This is an internal part of Automatic Updates and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
  */
-class RequestedUpdateValidator implements EventSubscriberInterface {
+final class RequestedUpdateValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
diff --git a/src/Validator/StagedDatabaseUpdateValidator.php b/src/Validator/StagedDatabaseUpdateValidator.php
index b3a3bb7854..a2998417dd 100644
--- a/src/Validator/StagedDatabaseUpdateValidator.php
+++ b/src/Validator/StagedDatabaseUpdateValidator.php
@@ -18,7 +18,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  *   at any time without warning. External code should not interact with this
  *   class.
  */
-class StagedDatabaseUpdateValidator implements EventSubscriberInterface {
+final class StagedDatabaseUpdateValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
-- 
GitLab