diff --git a/composer.json b/composer.json
index 63b08d9a2d..521b4bb388 100644
--- a/composer.json
+++ b/composer.json
@@ -21,7 +21,7 @@
         "composer/composer": "^1.9.1",
         "drupal/coder": "^8.3.7",
         "mikey179/vfsstream": "^1.6.8",
-        "phpunit/phpunit": "^8.4.1",
+        "phpunit/phpunit": "^8.4.1 || ^9",
         "phpspec/prophecy": "^1.7",
         "symfony/css-selector": "^4.4",
         "symfony/phpunit-bridge": "^5.1.4",
@@ -86,14 +86,15 @@
     "autoload-dev": {
         "psr-4": {
             "Drupal\\Composer\\": "composer"
-        }
+        },
+        "files": [ "core/includes/class_aliases_dev.php" ]
     },
     "scripts": {
         "pre-install-cmd": "Drupal\\Composer\\Composer::ensureComposerVersion",
         "pre-update-cmd": "Drupal\\Composer\\Composer::ensureComposerVersion",
         "pre-autoload-dump": "Drupal\\Core\\Composer\\Composer::preAutoloadDump",
         "drupal-phpunit-upgrade-check": "Drupal\\Core\\Composer\\Composer::upgradePHPUnit",
-        "drupal-phpunit-upgrade": "@composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies --no-progress",
+        "drupal-phpunit-upgrade": "@composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies --no-progress --ignore-platform-reqs",
         "post-update-cmd": [
             "Drupal\\Composer\\Composer::generateMetapackages"
         ],
diff --git a/composer.lock b/composer.lock
index 11b5e08089..2e8eacede9 100644
--- a/composer.lock
+++ b/composer.lock
@@ -486,7 +486,7 @@
             "dist": {
                 "type": "path",
                 "url": "core",
-                "reference": "5bd6798a64831fa08a343a14a0ee47127c4cb99f"
+                "reference": "568e11b75180f79954b390e34e79aff5a08246e5"
             },
             "require": {
                 "asm89/stack-cors": "^1.1",
@@ -529,7 +529,7 @@
                 "symfony/translation": "^4.4",
                 "symfony/validator": "^4.4",
                 "symfony/yaml": "^4.4",
-                "twig/twig": "^2.12.0",
+                "twig/twig": "^2.13.0",
                 "typo3/phar-stream-wrapper": "^3.1.3"
             },
             "conflict": {
@@ -709,6 +709,9 @@
                     "lib/Drupal/Core/DrupalKernelInterface.php",
                     "lib/Drupal/Core/Installer/InstallerRedirectTrait.php",
                     "lib/Drupal/Core/Site/Settings.php"
+                ],
+                "files": [
+                    "includes/class_aliases.php"
                 ]
             },
             "scripts": {
diff --git a/composer/Metapackage/DevDependencies/composer.json b/composer/Metapackage/DevDependencies/composer.json
index 3c10730009..a97129a60f 100644
--- a/composer/Metapackage/DevDependencies/composer.json
+++ b/composer/Metapackage/DevDependencies/composer.json
@@ -16,7 +16,7 @@
         "justinrainbow/json-schema": "^5.2",
         "mikey179/vfsstream": "^1.6.8",
         "phpspec/prophecy": "^1.7",
-        "phpunit/phpunit": "^8.4.1",
+        "phpunit/phpunit": "^8.4.1 || ^9",
         "symfony/browser-kit": "^4.4",
         "symfony/css-selector": "^4.4",
         "symfony/dom-crawler": "^4.4 !=4.4.5",
diff --git a/core/.cspell.json b/core/.cspell.json
index ee5bc110ca..64c36e2fe6 100644
--- a/core/.cspell.json
+++ b/core/.cspell.json
@@ -9,6 +9,7 @@
       "lib/Drupal/Component/Diff/**",
       "lib/Drupal/Component/Transliteration/data/**",
       "lib/Drupal/Core/File/MimeType/ExtensionMimeTypeGuesser.php",
+      "lib/Drupal/Core/Php8/**",
       "modules/**/Migrate*Test.php",
       "modules/color/preview.html",
       "modules/color/tests/modules/color_test/themes/color_test_theme/color/preview.html",
diff --git a/core/composer.json b/core/composer.json
index cf64c7800f..6691ec372c 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -31,7 +31,7 @@
         "symfony/polyfill-iconv": "^1.0",
         "symfony/yaml": "^4.4",
         "typo3/phar-stream-wrapper": "^3.1.3",
-        "twig/twig": "^2.12.0",
+        "twig/twig": "^2.13.0",
         "doctrine/reflection": "^1.1",
         "doctrine/annotations": "^1.4",
         "guzzlehttp/guzzle": "^6.5.2",
@@ -196,7 +196,8 @@
             "lib/Drupal/Core/DrupalKernelInterface.php",
             "lib/Drupal/Core/Installer/InstallerRedirectTrait.php",
             "lib/Drupal/Core/Site/Settings.php"
-        ]
+        ],
+        "files": [ "includes/class_aliases.php" ]
     },
     "config": {
         "preferred-install": "dist"
diff --git a/core/includes/class_aliases.php b/core/includes/class_aliases.php
new file mode 100644
index 0000000000..46d064f1ca
--- /dev/null
+++ b/core/includes/class_aliases.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Class aliases for different versions of PHP.
+ *
+ * @see core/composer.json
+ */
+
+if (PHP_VERSION_ID >= 80000) {
+  // Class aliases necessary for PHP8.
+  // @todo remove.
+  class_alias('\Drupal\Core\Php8\Doctrine\Reflection\StaticReflectionClass', '\Doctrine\Common\Reflection\StaticReflectionClass');
+}
diff --git a/core/includes/class_aliases_dev.php b/core/includes/class_aliases_dev.php
new file mode 100644
index 0000000000..876634f6ec
--- /dev/null
+++ b/core/includes/class_aliases_dev.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Class aliases for development for different versions of PHP.
+ *
+ * @see composer.json
+ */
+
+// These should work regardless of PHP version.
+class_alias('\Drupal\Core\Php8\Phpspec\Prophecy\ClassMirror', '\Prophecy\Doubler\Generator\ClassMirror');
+class_alias('\Drupal\Core\Php8\Phpdocumentor\ReflectionDocBlock\StandardTagFactory', '\phpDocumentor\Reflection\DocBlock\StandardTagFactory');
+class_alias('\Drupal\Core\Php8\Behat\MinkSelenium2Driver\Selenium2Driver', '\Behat\Mink\Driver\Selenium2Driver');
diff --git a/core/lib/Drupal/Component/Utility/ArgumentsResolver.php b/core/lib/Drupal/Component/Utility/ArgumentsResolver.php
index 0bd53b214e..0fc5d49925 100644
--- a/core/lib/Drupal/Component/Utility/ArgumentsResolver.php
+++ b/core/lib/Drupal/Component/Utility/ArgumentsResolver.php
@@ -69,7 +69,7 @@ public function getArguments(callable $callable) {
    *   Thrown when there is a missing parameter.
    */
   protected function getArgument(\ReflectionParameter $parameter) {
-    $parameter_type_hint = $parameter->getClass();
+    $parameter_type_hint = Reflection::getParameterClassName($parameter);
     $parameter_name = $parameter->getName();
 
     // If the argument exists and is NULL, return it, regardless of
@@ -79,6 +79,7 @@ protected function getArgument(\ReflectionParameter $parameter) {
     }
 
     if ($parameter_type_hint) {
+      $parameter_type_hint = new \ReflectionClass($parameter_type_hint);
       // If the argument exists and complies with the type hint, return it.
       if (isset($this->objects[$parameter_name]) && is_object($this->objects[$parameter_name]) && $parameter_type_hint->isInstance($this->objects[$parameter_name])) {
         return $this->objects[$parameter_name];
diff --git a/core/lib/Drupal/Component/Utility/Reflection.php b/core/lib/Drupal/Component/Utility/Reflection.php
new file mode 100644
index 0000000000..8745593bc2
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/Reflection.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Provides helper methods for reflection.
+ */
+final class Reflection {
+
+  /**
+   * Gets the parameter's class name.
+   *
+   * @param \ReflectionParameter $parameter
+   *   The parameter.
+   *
+   * @return string|null
+   *   The parameter's class name or NULL if the parameter is not a class.
+   */
+  public static function getParameterClassName(\ReflectionParameter $parameter) : ?string {
+    $name = NULL;
+    if ($parameter->hasType() && !$parameter->getType()->isBuiltin()) {
+      $name = $parameter->getType()->getName();
+      $lc_name = strtolower($name);
+      switch ($lc_name) {
+        case 'self':
+          return $parameter->getDeclaringClass()->getName();
+
+        case 'parent':
+          return ($parent = $parameter->getDeclaringClass()->getParentClass()) ? $parent->name : NULL;
+      }
+    }
+    return $name;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php
index 6b7d5899d6..2f60ab9d0f 100644
--- a/core/lib/Drupal/Core/Composer/Composer.php
+++ b/core/lib/Drupal/Core/Composer/Composer.php
@@ -310,7 +310,7 @@ public static function upgradePHPUnit(Event $event) {
       return;
     }
 
-    // If the PHP version is 7.3 or above and PHPUnit is less than version 7
+    // If the PHP version is 7.4 or above and PHPUnit is less than version 9
     // call the drupal-phpunit-upgrade script to upgrade PHPUnit.
     if (!static::upgradePHPUnitCheck($phpunit_package->getVersion())) {
       $event->getComposer()
@@ -332,7 +332,7 @@ public static function upgradePHPUnit(Event $event) {
    *   TRUE if the PHPUnit needs to be upgraded, FALSE if not.
    */
   public static function upgradePHPUnitCheck($phpunit_version) {
-    return !(version_compare(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, '7.3') >= 0 && version_compare($phpunit_version, '7.0') < 0);
+    return !(version_compare(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, '7.4') >= 0 && version_compare($phpunit_version, '9.0') < 0);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Database/Php7StatementInterface.php b/core/lib/Drupal/Core/Database/Php7StatementInterface.php
new file mode 100644
index 0000000000..9c42998693
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Php7StatementInterface.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Core\Database;
+
+/**
+ * Compatibility layer for \Drupal\Core\Database\StatementInterface.
+ *
+ * @internal
+ */
+interface Php7StatementInterface {
+
+  /**
+   * Fetches the next row from a result set.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   *   Default to what was specified by setFetchMode().
+   * @param int $cursor_orientation
+   *   Not implemented in all database drivers, don't use.
+   * @param int $cursor_offset
+   *   Not implemented in all database drivers, don't use.
+   *
+   * @return
+   *   A result, formatted according to $mode.
+   */
+  public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL);
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $column_index
+   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
+   * @param $constructor_arguments
+   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *
+   * @return
+   *   An array of results.
+   */
+  public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL);
+
+  /**
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $a1
+   *   An option depending of the fetch mode specified by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   * @param $a2
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
+   */
+  public function setFetchMode($mode, $a1 = NULL, $a2 = []);
+
+}
diff --git a/core/lib/Drupal/Core/Database/Php7StatementTrait.php b/core/lib/Drupal/Core/Database/Php7StatementTrait.php
new file mode 100644
index 0000000000..9b1d092c49
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Php7StatementTrait.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Core\Database;
+
+/**
+ * Compatibility layer for \Drupal\Core\Database\StatementInterface.
+ */
+trait Php7StatementTrait {
+
+  /**
+   * Fetches the next row from a result set.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   *   Default to what was specified by setFetchMode().
+   * @param int $cursor_orientation
+   *   Not implemented in all database drivers, don't use.
+   * @param int $cursor_offset
+   *   Not implemented in all database drivers, don't use.
+   *
+   * @return
+   *   A result, formatted according to $mode.
+   */
+  public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {
+    return $this->doFetch($mode, $cursor_orientation, $cursor_offset);
+  }
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $column_index
+   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
+   * @param $constructor_arguments
+   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *
+   * @return
+   *   An array of results.
+   */
+  public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
+    // Call \PDOStatement::fetchAll to fetch all rows.
+    // \PDOStatement is picky about the number of arguments in some cases so we
+    // need to be pass the exact number of arguments we where given.
+    switch (func_num_args()) {
+      case 0:
+        return $this->doFetchAll();
+
+      case 1:
+        return $this->doFetchAll($mode);
+
+      case 2:
+        return $this->doFetchAll($mode, $column_index);
+
+      case 3:
+      default:
+        return $this->doFetchAll($mode, $column_index, $constructor_arguments);
+    }
+  }
+
+  /**
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $a1
+   *   An option depending of the fetch mode specified by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   * @param $a2
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
+   */
+  public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
+    // Call \PDOStatement::setFetchMode to set fetch mode.
+    // \PDOStatement is picky about the number of arguments in some cases so we
+    // need to be pass the exact number of arguments we where given.
+    switch (func_num_args()) {
+      case 1:
+        return $this->doSetFetchMode($mode);
+
+      case 2:
+        return $this->doSetFetchMode($mode, $a1);
+
+      case 3:
+      default:
+        return $this->doSetFetchMode($mode, $a1, $a2);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Database/Php8StatementInterface.php b/core/lib/Drupal/Core/Database/Php8StatementInterface.php
new file mode 100644
index 0000000000..a132329e97
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Php8StatementInterface.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Core\Database;
+
+/**
+ * Compatibility layer for \Drupal\Core\Database\StatementInterface.
+ *
+ * @internal
+ */
+interface Php8StatementInterface {
+
+  /**
+   * Fetches the next row from a result set.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   *   Default to what was specified by setFetchMode().
+   * @param int $cursor_orientation
+   *   Not implemented in all database drivers, don't use.
+   * @param int $cursor_offset
+   *   Not implemented in all database drivers, don't use.
+   *
+   * @return
+   *   A result, formatted according to $mode.
+   */
+  public function fetch(int $mode = \PDO::FETCH_BOTH, int $cursor_orientation = \PDO::FETCH_ORI_NEXT, int $cursor_offset = 0);
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param mixed $args
+   *   Mixed arguments depending on the $mode.
+   *
+   * @return
+   *   An array of results.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetchAll()
+   */
+  public function fetchAll(int $mode = \PDO::FETCH_BOTH, ...$args);
+
+  /**
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param mixed $params
+   *   The first argument is an option depending of the fetch mode specified
+   *   by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   *   If $mode is PDO::FETCH_CLASS, the second argument is passed to the
+   *   constructor.
+   */
+  public function setFetchMode(int $mode, ...$params);
+
+}
diff --git a/core/lib/Drupal/Core/Database/Php8StatementTrait.php b/core/lib/Drupal/Core/Database/Php8StatementTrait.php
new file mode 100644
index 0000000000..3ffe61cc4d
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Php8StatementTrait.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\Core\Database;
+
+/**
+ * Compatibility layer for \Drupal\Core\Database\StatementInterface.
+ */
+trait Php8StatementTrait {
+
+  /**
+   * Fetches the next row from a result set.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants. Defaults to what was specified by
+   *   setFetchMode(). Note the default value on the implementation is different
+   *   from the default value on \PDO::fetch() for PHP 8 in order to allow
+   *   \Drupal\Core\Database\StatementPrefetch to behave the same on PHP 7 & 8.
+   * @param int $cursor_orientation
+   *   Not implemented in all database drivers, don't use.
+   * @param int $cursor_offset
+   *   Not implemented in all database drivers, don't use.
+   *
+   * @return
+   *   A result, formatted according to $mode.
+   */
+  public function fetch(int $mode = 0, int $cursor_orientation = \PDO::FETCH_ORI_NEXT, int $cursor_offset = 0) {
+    return $this->doFetch($mode, $cursor_orientation, $cursor_offset);
+  }
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param mixed $args
+   *   Mixed arguments depending on the $mode.
+   *
+   * @return
+   *   An array of results.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetchAll()
+   */
+  public function fetchAll(int $mode = \PDO::FETCH_BOTH, ...$args) {
+    // Call \PDOStatement::fetchAll to fetch all rows.
+    // \PDOStatement is picky about the number of arguments in some cases so we
+    // need to be pass the exact number of arguments we where given.
+    switch (func_num_args()) {
+      case 0:
+        return $this->doFetchAll();
+
+      case 1:
+        return $this->doFetchAll($mode);
+
+      case 2:
+        return $this->doFetchAll($mode, $args[0]);
+
+      case 3:
+      default:
+        $arg1 = array_shift($args);
+        return $this->doFetchAll($mode, $arg1, ...$args);
+    }
+  }
+
+  /**
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param mixed $params
+   *   The first argument is an option depending of the fetch mode specified
+   *   by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   *   If $mode is PDO::FETCH_CLASS, the second argument is passed to the
+   *   constructor.
+   */
+  public function setFetchMode(int $mode, ...$params) {
+    // Call \PDOStatement::setFetchMode to set fetch mode.
+    // \PDOStatement is picky about the number of arguments in some cases so we
+    // need to be pass the exact number of arguments we where given.
+    switch (func_num_args()) {
+      case 1:
+        return $this->doSetFetchMode($mode);
+
+      case 2:
+        return $this->doSetFetchMode($mode, $params[0]);
+
+      case 3:
+      default:
+        $a1 = array_shift($params);
+        return $this->doSetFetchMode($mode, $a1, ...$params);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Database/Statement.php b/core/lib/Drupal/Core/Database/Statement.php
index c6a120d611..70d389174c 100644
--- a/core/lib/Drupal/Core/Database/Statement.php
+++ b/core/lib/Drupal/Core/Database/Statement.php
@@ -2,6 +2,13 @@
 
 namespace Drupal\Core\Database;
 
+if (PHP_VERSION_ID >= 80000) {
+  class_alias('\Drupal\Core\Database\Php8StatementTrait', '\Drupal\Core\Database\StatementTrait');
+}
+else {
+  class_alias('\Drupal\Core\Database\Php7StatementTrait', '\Drupal\Core\Database\StatementTrait');
+}
+
 /**
  * Default implementation of StatementInterface.
  *
@@ -14,6 +21,7 @@
  * @see http://php.net/pdostatement
  */
 class Statement extends \PDOStatement implements StatementInterface {
+  use StatementTrait;
 
   /**
    * Reference to the database connection object for this statement.
@@ -144,9 +152,26 @@ public function rowCount() {
   }
 
   /**
-   * {@inheritdoc}
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $a1
+   *   An option depending of the fetch mode specified by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   * @param $a2
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::setFetchMode()
+   * @see \Drupal\Core\Database\Php8StatementTrait::setFetchMode()
    */
-  public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
+  public function doSetFetchMode($mode, $a1 = NULL, $a2 = []) {
     // Call \PDOStatement::setFetchMode to set fetch mode.
     // \PDOStatement is picky about the number of arguments in some cases so we
     // need to be pass the exact number of arguments we where given.
@@ -164,9 +189,39 @@ public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
   }
 
   /**
-   * {@inheritdoc}
+   * Fetches the next row from a result set.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants. Default to what was specified by
+   *   setFetchMode().
+   *
+   * @return
+   *   A result.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetch()
+   * @see \Drupal\Core\Database\Php8StatementTrait::fetch()
+   */
+  public function doFetch($mode) {
+    return parent::fetch($mode);
+  }
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $column_index
+   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
+   * @param $constructor_arguments
+   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *
+   * @return
+   *   An array of results.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetchAll()
+   * @see \Drupal\Core\Database\Php8StatementTrait::fetchAll()
    */
-  public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
+  public function doFetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
     // Call \PDOStatement::fetchAll to fetch all rows.
     // \PDOStatement is picky about the number of arguments in some cases so we
     // need to be pass the exact number of arguments we where given.
diff --git a/core/lib/Drupal/Core/Database/StatementEmpty.php b/core/lib/Drupal/Core/Database/StatementEmpty.php
index a87bca920a..784d27e218 100644
--- a/core/lib/Drupal/Core/Database/StatementEmpty.php
+++ b/core/lib/Drupal/Core/Database/StatementEmpty.php
@@ -2,6 +2,13 @@
 
 namespace Drupal\Core\Database;
 
+if (PHP_VERSION_ID >= 80000) {
+  class_alias('\Drupal\Core\Database\Php8StatementTrait', '\Drupal\Core\Database\StatementTrait');
+}
+else {
+  class_alias('\Drupal\Core\Database\Php7StatementTrait', '\Drupal\Core\Database\StatementTrait');
+}
+
 /**
  * Empty implementation of a database statement.
  *
@@ -14,6 +21,7 @@
  * @see \Drupal\search\SearchQuery
  */
 class StatementEmpty implements \Iterator, StatementInterface {
+  use StatementTrait;
 
   /**
    * Is rowCount() execution allowed.
@@ -47,14 +55,37 @@ public function rowCount() {
   }
 
   /**
-   * {@inheritdoc}
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $a1
+   *   An option depending of the fetch mode specified by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   * @param $a2
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::setFetchMode()
+   * @see \Drupal\Core\Database\Php8StatementTrait::setFetchMode()
    */
-  public function setFetchMode($mode, $a1 = NULL, $a2 = []) {}
+  public function doSetFetchMode($mode, $a1 = NULL, $a2 = []) {}
 
   /**
-   * {@inheritdoc}
+   * Fetches the next row from a result set.
+   *
+   * @return
+   *   A result.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetch()
+   * @see \Drupal\Core\Database\Php8StatementTrait::fetch()
    */
-  public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {
+  public function doFetch() {
     return NULL;
   }
 
@@ -80,9 +111,22 @@ public function fetchAssoc() {
   }
 
   /**
-   * {@inheritdoc}
+   * Returns an array containing all of the result set rows.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $column_index
+   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
+   * @param $constructor_arguments
+   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *
+   * @return
+   *   An array of results.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetchAll()
+   * @see \Drupal\Core\Database\Php8StatementTrait::fetchAll()
    */
-  public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
+  public function doFetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
     return [];
   }
 
diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php
index 4f4248df04..c16a933ed6 100644
--- a/core/lib/Drupal/Core/Database/StatementInterface.php
+++ b/core/lib/Drupal/Core/Database/StatementInterface.php
@@ -2,6 +2,13 @@
 
 namespace Drupal\Core\Database;
 
+if (PHP_VERSION_ID >= 80000) {
+  class_alias('\Drupal\Core\Database\Php8StatementInterface', '\Drupal\Core\Database\StatementInterfaceBase');
+}
+else {
+  class_alias('\Drupal\Core\Database\Php7StatementInterface', '\Drupal\Core\Database\StatementInterfaceBase');
+}
+
 /**
  * Represents a prepared statement.
  *
@@ -18,7 +25,7 @@
  *
  * @ingroup database
  */
-interface StatementInterface extends \Traversable {
+interface StatementInterface extends StatementInterfaceBase, \Traversable {
 
   /**
    * Constructs a new PDOStatement object.
@@ -73,44 +80,6 @@ public function getQueryString();
    */
   public function rowCount();
 
-  /**
-   * Sets the default fetch mode for this statement.
-   *
-   * See http://php.net/manual/pdo.constants.php for the definition of the
-   * constants used.
-   *
-   * @param $mode
-   *   One of the PDO::FETCH_* constants.
-   * @param $a1
-   *   An option depending of the fetch mode specified by $mode:
-   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
-   *   - for PDO::FETCH_CLASS, the name of the class to create
-   *   - for PDO::FETCH_INTO, the object to add the data to
-   * @param $a2
-   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
-   *   constructor.
-   */
-  public function setFetchMode($mode, $a1 = NULL, $a2 = []);
-
-  /**
-   * Fetches the next row from a result set.
-   *
-   * See http://php.net/manual/pdo.constants.php for the definition of the
-   * constants used.
-   *
-   * @param $mode
-   *   One of the PDO::FETCH_* constants.
-   *   Default to what was specified by setFetchMode().
-   * @param $cursor_orientation
-   *   Not implemented in all database drivers, don't use.
-   * @param $cursor_offset
-   *   Not implemented in all database drivers, don't use.
-   *
-   * @return
-   *   A result, formatted according to $mode.
-   */
-  public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL);
-
   /**
    * Returns a single field from the next record of a result set.
    *
@@ -142,21 +111,6 @@ public function fetchObject();
    */
   public function fetchAssoc();
 
-  /**
-   * Returns an array containing all of the result set rows.
-   *
-   * @param $mode
-   *   One of the PDO::FETCH_* constants.
-   * @param $column_index
-   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
-   * @param $constructor_arguments
-   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
-   *
-   * @return
-   *   An array of results.
-   */
-  public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL);
-
   /**
    * Returns an entire single column of a result set as an indexed array.
    *
diff --git a/core/lib/Drupal/Core/Database/StatementPrefetch.php b/core/lib/Drupal/Core/Database/StatementPrefetch.php
index 3f6efdf364..cbee4ee91a 100644
--- a/core/lib/Drupal/Core/Database/StatementPrefetch.php
+++ b/core/lib/Drupal/Core/Database/StatementPrefetch.php
@@ -2,6 +2,13 @@
 
 namespace Drupal\Core\Database;
 
+if (PHP_VERSION_ID >= 80000) {
+  class_alias('\Drupal\Core\Database\Php8StatementTrait', '\Drupal\Core\Database\StatementPrefetchTrait');
+}
+else {
+  class_alias('\Drupal\Core\Database\Php7StatementTrait', '\Drupal\Core\Database\StatementPrefetchTrait');
+}
+
 /**
  * An implementation of StatementInterface that prefetches all data.
  *
@@ -9,6 +16,7 @@
  * every row it is possible to manipulate those results.
  */
 class StatementPrefetch implements \Iterator, StatementInterface {
+  use StatementPrefetchTrait;
 
   /**
    * The query string.
@@ -232,9 +240,26 @@ public function getQueryString() {
   }
 
   /**
-   * {@inheritdoc}
+   * Sets the default fetch mode for this statement.
+   *
+   * See http://php.net/manual/pdo.constants.php for the definition of the
+   * constants used.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $a1
+   *   An option depending of the fetch mode specified by $mode:
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
+   * @param $a2
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::setFetchMode()
+   * @see \Drupal\Core\Database\Php8StatementTrait::setFetchMode()
    */
-  public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
+  public function doSetFetchMode($mode, $a1 = NULL, $a2 = []) {
     $this->defaultFetchStyle = $mode;
     switch ($mode) {
       case \PDO::FETCH_CLASS:
@@ -373,12 +398,22 @@ public function rowCount() {
   }
 
   /**
-   * {@inheritdoc}
+   * Fetches the next row from a result set.
+   *
+   * @param int $mode
+   *   One of the PDO::FETCH_* constants. Default to what was specified by
+   *   setFetchMode().
+   *
+   * @return
+   *   A result.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetch()
+   * @see \Drupal\Core\Database\Php8StatementTrait::fetch()
    */
-  public function fetch($fetch_style = NULL, $cursor_orientation = \PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
+  public function doFetch($mode) {
     if (isset($this->currentRow)) {
       // Set the fetch parameter.
-      $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+      $this->fetchStyle = !empty($mode) ? $mode : $this->defaultFetchStyle;
       $this->fetchOptions = $this->defaultFetchOptions;
 
       // Grab the row in the format specified above.
@@ -461,9 +496,22 @@ public function fetchAssoc() {
   }
 
   /**
-   * {@inheritdoc}
+   * Returns an array containing all of the result set rows.
+   *
+   * @param $mode
+   *   One of the PDO::FETCH_* constants.
+   * @param $column_index
+   *   If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
+   * @param $constructor_arguments
+   *   If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *
+   * @return
+   *   An array of results.
+   *
+   * @see \Drupal\Core\Database\Php7StatementTrait::fetchAll()
+   * @see \Drupal\Core\Database\Php8StatementTrait::fetchAll()
    */
-  public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
+  public function doFetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
     $this->fetchStyle = isset($mode) ? $mode : $this->defaultFetchStyle;
     $this->fetchOptions = $this->defaultFetchOptions;
     if (isset($column_index)) {
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
index 35b5beaaa3..f3b7a8225f 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\DependencyInjection\Compiler;
 
+use Drupal\Component\Utility\Reflection;
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Exception\LogicException;
@@ -130,8 +131,9 @@ protected function processServiceCollectorPass(array $pass, $consumer_id, Contai
     $priority_pos = NULL;
     $extra_params = [];
     foreach ($params as $pos => $param) {
-      if ($param->getClass()) {
-        $interface = $param->getClass();
+      $class = Reflection::getParameterClassName($param);
+      if ($class !== NULL) {
+        $interface = $class;
       }
       elseif ($param->getName() === 'id') {
         $id_pos = $pos;
@@ -152,7 +154,6 @@ protected function processServiceCollectorPass(array $pass, $consumer_id, Contai
         $method_name,
       ]));
     }
-    $interface = $interface->getName();
 
     // Find all tagged handlers.
     $handlers = [];
diff --git a/core/lib/Drupal/Core/Entity/EntityResolverManager.php b/core/lib/Drupal/Core/Entity/EntityResolverManager.php
index c10d72f551..25cb70c335 100644
--- a/core/lib/Drupal/Core/Entity/EntityResolverManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityResolverManager.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Entity;
 
+use Drupal\Component\Utility\Reflection;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Symfony\Component\Routing\Route;
 
@@ -74,6 +75,10 @@ protected function getControllerClass(array $defaults) {
       }
     }
 
+    if ($controller === NULL) {
+      return NULL;
+    }
+
     if (strpos($controller, ':') === FALSE) {
       if (method_exists($controller, '__invoke')) {
         return [$controller, '__invoke'];
@@ -135,7 +140,8 @@ protected function setParametersFromReflection($controller, Route $route) {
       if (isset($entity_types[$parameter_name])) {
         $entity_type = $entity_types[$parameter_name];
         $entity_class = $entity_type->getClass();
-        if (($reflection_class = $parameter->getClass()) && (is_subclass_of($entity_class, $reflection_class->name) || $entity_class == $reflection_class->name)) {
+        $reflection_class = Reflection::getParameterClassName($parameter);
+        if ($reflection_class && (is_subclass_of($entity_class, $reflection_class) || $entity_class == $reflection_class)) {
           $parameter_definitions += [$parameter_name => []];
           $parameter_definitions[$parameter_name] += [
             'type' => 'entity:' . $parameter_name,
diff --git a/core/lib/Drupal/Core/Php8/Behat/MinkSelenium2Driver/Selenium2Driver.php b/core/lib/Drupal/Core/Php8/Behat/MinkSelenium2Driver/Selenium2Driver.php
new file mode 100644
index 0000000000..6e21d72d8b
--- /dev/null
+++ b/core/lib/Drupal/Core/Php8/Behat/MinkSelenium2Driver/Selenium2Driver.php
@@ -0,0 +1,1243 @@
+<?php
+// @codingStandardsIgnoreFile
+
+namespace Drupal\Core\Php8\Behat\MinkSelenium2Driver;
+/*
+ * This file is part of the Behat\Mink.
+ * (c) Konstantin Kudryashov <ever.zet@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Behat\Mink\Driver\CoreDriver;
+use Behat\Mink\Exception\DriverException;
+use Behat\Mink\Selector\Xpath\Escaper;
+use WebDriver\Element;
+use WebDriver\Exception\NoSuchElement;
+use WebDriver\Exception\UnknownCommand;
+use WebDriver\Exception\UnknownError;
+use WebDriver\Exception;
+use WebDriver\Key;
+use WebDriver\WebDriver;
+
+/**
+ * Selenium2 driver.
+ *
+ * @author Pete Otaqui <pete@otaqui.com>
+ */
+class Selenium2Driver extends CoreDriver
+{
+    /**
+     * Whether the browser has been started
+     * @var boolean
+     */
+    private $started = false;
+
+    /**
+     * The WebDriver instance
+     * @var WebDriver
+     */
+    private $webDriver;
+
+    /**
+     * @var string
+     */
+    private $browserName;
+
+    /**
+     * @var array
+     */
+    private $desiredCapabilities;
+
+    /**
+     * The WebDriverSession instance
+     * @var \WebDriver\Session
+     */
+    private $wdSession;
+
+    /**
+     * The timeout configuration
+     * @var array
+     */
+    private $timeouts = array();
+
+    /**
+     * @var Escaper
+     */
+    private $xpathEscaper;
+
+    /**
+     * Instantiates the driver.
+     *
+     * @param string $browserName         Browser name
+     * @param array  $desiredCapabilities The desired capabilities
+     * @param string $wdHost              The WebDriver host
+     */
+    public function __construct($browserName = 'firefox', $desiredCapabilities = null, $wdHost = 'http://localhost:4444/wd/hub')
+    {
+        $this->setBrowserName($browserName);
+        $this->setDesiredCapabilities($desiredCapabilities);
+        $this->setWebDriver(new WebDriver($wdHost));
+        $this->xpathEscaper = new Escaper();
+    }
+
+    /**
+     * Sets the browser name
+     *
+     * @param string $browserName the name of the browser to start, default is 'firefox'
+     */
+    protected function setBrowserName($browserName = 'firefox')
+    {
+        $this->browserName = $browserName;
+    }
+
+    /**
+     * Sets the desired capabilities - called on construction.  If null is provided, will set the
+     * defaults as desired.
+     *
+     * See http://code.google.com/p/selenium/wiki/DesiredCapabilities
+     *
+     * @param array $desiredCapabilities an array of capabilities to pass on to the WebDriver server
+     *
+     * @throws DriverException
+     */
+    public function setDesiredCapabilities($desiredCapabilities = null)
+    {
+        if ($this->started) {
+            throw new DriverException("Unable to set desiredCapabilities, the session has already started");
+        }
+
+        if (null === $desiredCapabilities) {
+            $desiredCapabilities = array();
+        }
+
+        // Join $desiredCapabilities with defaultCapabilities
+        $desiredCapabilities = array_replace(self::getDefaultCapabilities(), $desiredCapabilities);
+
+        if (isset($desiredCapabilities['firefox'])) {
+            foreach ($desiredCapabilities['firefox'] as $capability => $value) {
+                switch ($capability) {
+                    case 'profile':
+                        $desiredCapabilities['firefox_'.$capability] = base64_encode(file_get_contents($value));
+                        break;
+                    default:
+                        $desiredCapabilities['firefox_'.$capability] = $value;
+                }
+            }
+
+            unset($desiredCapabilities['firefox']);
+        }
+
+        // See https://sites.google.com/a/chromium.org/chromedriver/capabilities
+        if (isset($desiredCapabilities['chrome'])) {
+
+            $chromeOptions = (isset($desiredCapabilities['goog:chromeOptions']) && is_array($desiredCapabilities['goog:chromeOptions']))? $desiredCapabilities['goog:chromeOptions']:array();
+
+            foreach ($desiredCapabilities['chrome'] as $capability => $value) {
+                if ($capability == 'switches') {
+                    $chromeOptions['args'] = $value;
+                } else {
+                    $chromeOptions[$capability] = $value;
+                }
+                $desiredCapabilities['chrome.'.$capability] = $value;
+            }
+
+            $desiredCapabilities['goog:chromeOptions'] = $chromeOptions;
+
+            unset($desiredCapabilities['chrome']);
+        }
+
+        $this->desiredCapabilities = $desiredCapabilities;
+    }
+
+    /**
+     * Gets the desiredCapabilities
+     *
+     * @return array $desiredCapabilities
+     */
+    public function getDesiredCapabilities()
+    {
+        return $this->desiredCapabilities;
+    }
+
+    /**
+     * Sets the WebDriver instance
+     *
+     * @param WebDriver $webDriver An instance of the WebDriver class
+     */
+    public function setWebDriver(WebDriver $webDriver)
+    {
+        $this->webDriver = $webDriver;
+    }
+
+    /**
+     * Gets the WebDriverSession instance
+     *
+     * @return \WebDriver\Session
+     */
+    public function getWebDriverSession()
+    {
+        return $this->wdSession;
+    }
+
+    /**
+     * Returns the default capabilities
+     *
+     * @return array
+     */
+    public static function getDefaultCapabilities()
+    {
+        return array(
+            'browserName'       => 'firefox',
+            'name'              => 'Behat Test',
+        );
+    }
+
+    /**
+     * Makes sure that the Syn event library has been injected into the current page,
+     * and return $this for a fluid interface,
+     *
+     *     $this->withSyn()->executeJsOnXpath($xpath, $script);
+     *
+     * @return Selenium2Driver
+     */
+    protected function withSyn()
+    {
+        $hasSyn = $this->wdSession->execute(array(
+            'script' => 'return typeof window["Syn"]!=="undefined" && typeof window["Syn"].trigger!=="undefined"',
+            'args'   => array()
+        ));
+
+        if (!$hasSyn) {
+            // @todo this is an ugly trick to work out the location of the
+            //   original class so we don't need to copy the syn.js file.
+            //   This code is completely unnecessary. It's an artifact of the
+            //   hack to make this class work for Drupal on PHP 8. The vendor
+            //   update does not need this.
+            $dir = dirname(\Drupal::service('class_loader')->findFile('Behat\Mink\Driver\Selenium2Driver'));
+            $synJs = file_get_contents($dir.'/Resources/syn.js');
+            $this->wdSession->execute(array(
+                'script' => $synJs,
+                'args'   => array()
+            ));
+        }
+
+        return $this;
+    }
+
+    /**
+     * Creates some options for key events
+     *
+     * @param string $char     the character or code
+     * @param string $modifier one of 'shift', 'alt', 'ctrl' or 'meta'
+     *
+     * @return string a json encoded options array for Syn
+     */
+    protected static function charToOptions($char, $modifier = null)
+    {
+        $ord = ord($char);
+        if (is_numeric($char)) {
+            $ord = $char;
+        }
+
+        $options = array(
+            'keyCode'  => $ord,
+            'charCode' => $ord
+        );
+
+        if ($modifier) {
+            $options[$modifier.'Key'] = 1;
+        }
+
+        return json_encode($options);
+    }
+
+    /**
+     * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will
+     * be replaced with a reference to the result of the $xpath query
+     *
+     * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length');
+     *
+     * @param string  $xpath  the xpath to search with
+     * @param string  $script the script to execute
+     * @param boolean $sync   whether to run the script synchronously (default is TRUE)
+     *
+     * @return mixed
+     */
+    protected function executeJsOnXpath($xpath, $script, $sync = true)
+    {
+        return $this->executeJsOnElement($this->findElement($xpath), $script, $sync);
+    }
+
+    /**
+     * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will
+     * be replaced with a reference to the element
+     *
+     * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length');
+     *
+     * @param Element $element the webdriver element
+     * @param string  $script  the script to execute
+     * @param boolean $sync    whether to run the script synchronously (default is TRUE)
+     *
+     * @return mixed
+     */
+    private function executeJsOnElement(Element $element, $script, $sync = true)
+    {
+        $script  = str_replace('{{ELEMENT}}', 'arguments[0]', $script);
+
+        $options = array(
+            'script' => $script,
+            'args'   => array(array('ELEMENT' => $element->getID())),
+        );
+
+        if ($sync) {
+            return $this->wdSession->execute($options);
+        }
+
+        return $this->wdSession->execute_async($options);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function start()
+    {
+        try {
+            $this->wdSession = $this->webDriver->session($this->browserName, $this->desiredCapabilities);
+            $this->applyTimeouts();
+        } catch (\Exception $e) {
+            throw new DriverException('Could not open connection: '.$e->getMessage(), 0, $e);
+        }
+
+        if (!$this->wdSession) {
+            throw new DriverException('Could not connect to a Selenium 2 / WebDriver server');
+        }
+        $this->started = true;
+    }
+
+    /**
+     * Sets the timeouts to apply to the webdriver session
+     *
+     * @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds
+     *
+     * @throws DriverException
+     */
+    public function setTimeouts($timeouts)
+    {
+        $this->timeouts = $timeouts;
+
+        if ($this->isStarted()) {
+            $this->applyTimeouts();
+        }
+    }
+
+    /**
+     * Applies timeouts to the current session
+     */
+    private function applyTimeouts()
+    {
+        try {
+            foreach ($this->timeouts as $type => $param) {
+                $this->wdSession->timeouts($type, $param);
+            }
+        } catch (UnknownError $e) {
+            throw new DriverException('Error setting timeout: ' . $e->getMessage(), 0, $e);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isStarted()
+    {
+        return $this->started;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function stop()
+    {
+        if (!$this->wdSession) {
+            throw new DriverException('Could not connect to a Selenium 2 / WebDriver server');
+        }
+
+        $this->started = false;
+        try {
+            $this->wdSession->close();
+        } catch (\Exception $e) {
+            throw new DriverException('Could not close connection', 0, $e);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function reset()
+    {
+        $this->wdSession->deleteAllCookies();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function visit($url)
+    {
+        $this->wdSession->open($url);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCurrentUrl()
+    {
+        return $this->wdSession->url();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function reload()
+    {
+        $this->wdSession->refresh();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function forward()
+    {
+        $this->wdSession->forward();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function back()
+    {
+        $this->wdSession->back();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function switchToWindow($name = null)
+    {
+        $this->wdSession->focusWindow($name ? $name : '');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function switchToIFrame($name = null)
+    {
+        $this->wdSession->frame(array('id' => $name));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setCookie($name, $value = null)
+    {
+        if (null === $value) {
+            $this->wdSession->deleteCookie($name);
+
+            return;
+        }
+
+        // PHP 7.4 changed the way it encodes cookies to better respect the spec.
+        // This assumes that the server and the Mink client run on the same version (or
+        // at least the same side of the behavior change), so that the server and Mink
+        // consider the same value.
+        if (\PHP_VERSION_ID >= 70400) {
+            $encodedValue = rawurlencode($value);
+        } else {
+            $encodedValue = urlencode($value);
+        }
+
+        $cookieArray = array(
+            'name'   => $name,
+            'value'  => $encodedValue,
+            'secure' => false, // thanks, chibimagic!
+        );
+
+        $this->wdSession->setCookie($cookieArray);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCookie($name)
+    {
+        $cookies = $this->wdSession->getAllCookies();
+        foreach ($cookies as $cookie) {
+            if ($cookie['name'] === $name) {
+                // PHP 7.4 changed the way it encodes cookies to better respect the spec.
+                // This assumes that the server and the Mink client run on the same version (or
+                // at least the same side of the behavior change), so that the server and Mink
+                // consider the same value.
+                if (\PHP_VERSION_ID >= 70400) {
+                    return rawurldecode($cookie['value']);
+                }
+
+                return urldecode($cookie['value']);
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getContent()
+    {
+        return $this->wdSession->source();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getScreenshot()
+    {
+        return base64_decode($this->wdSession->screenshot());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getWindowNames()
+    {
+        return $this->wdSession->window_handles();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getWindowName()
+    {
+        return $this->wdSession->window_handle();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function findElementXpaths($xpath)
+    {
+        $nodes = $this->wdSession->elements('xpath', $xpath);
+
+        $elements = array();
+        foreach ($nodes as $i => $node) {
+            $elements[] = sprintf('(%s)[%d]', $xpath, $i+1);
+        }
+
+        return $elements;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTagName($xpath)
+    {
+        return $this->findElement($xpath)->name();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getText($xpath)
+    {
+        $node = $this->findElement($xpath);
+        $text = $node->text();
+        $text = (string) str_replace(array("\r", "\r\n", "\n"), ' ', $text);
+
+        return $text;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getHtml($xpath)
+    {
+        return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerHTML;');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getOuterHtml($xpath)
+    {
+        return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.outerHTML;');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttribute($xpath, $name)
+    {
+        $script = 'return {{ELEMENT}}.getAttribute(' . json_encode((string) $name) . ')';
+
+        return $this->executeJsOnXpath($xpath, $script);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getValue($xpath)
+    {
+        $element = $this->findElement($xpath);
+        $elementName = strtolower($element->name());
+        $elementType = strtolower($element->attribute('type'));
+
+        // Getting the value of a checkbox returns its value if selected.
+        if ('input' === $elementName && 'checkbox' === $elementType) {
+            return $element->selected() ? $element->attribute('value') : null;
+        }
+
+        if ('input' === $elementName && 'radio' === $elementType) {
+            $script = <<<JS
+var node = {{ELEMENT}},
+    value = null;
+
+var name = node.getAttribute('name');
+if (name) {
+    var fields = window.document.getElementsByName(name),
+        i, l = fields.length;
+    for (i = 0; i < l; i++) {
+        var field = fields.item(i);
+        if (field.form === node.form && field.checked) {
+            value = field.value;
+            break;
+        }
+    }
+}
+
+return value;
+JS;
+
+            return $this->executeJsOnElement($element, $script);
+        }
+
+        // Using $element->attribute('value') on a select only returns the first selected option
+        // even when it is a multiple select, so a custom retrieval is needed.
+        if ('select' === $elementName && $element->attribute('multiple')) {
+            $script = <<<JS
+var node = {{ELEMENT}},
+    value = [];
+
+for (var i = 0; i < node.options.length; i++) {
+    if (node.options[i].selected) {
+        value.push(node.options[i].value);
+    }
+}
+
+return value;
+JS;
+
+            return $this->executeJsOnElement($element, $script);
+        }
+
+        return $element->attribute('value');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setValue($xpath, $value)
+    {
+        $element = $this->findElement($xpath);
+        $elementName = strtolower($element->name());
+
+        if ('select' === $elementName) {
+            if (is_array($value)) {
+                $this->deselectAllOptions($element);
+
+                foreach ($value as $option) {
+                    $this->selectOptionOnElement($element, $option, true);
+                }
+
+                return;
+            }
+
+            $this->selectOptionOnElement($element, $value);
+
+            return;
+        }
+
+        if ('input' === $elementName) {
+            $elementType = strtolower($element->attribute('type'));
+
+            if (in_array($elementType, array('submit', 'image', 'button', 'reset'))) {
+                throw new DriverException(sprintf('Impossible to set value an element with XPath "%s" as it is not a select, textarea or textbox', $xpath));
+            }
+
+            if ('checkbox' === $elementType) {
+                if ($element->selected() xor (bool) $value) {
+                    $this->clickOnElement($element);
+                }
+
+                return;
+            }
+
+            if ('radio' === $elementType) {
+                $this->selectRadioValue($element, $value);
+
+                return;
+            }
+
+            if ('file' === $elementType) {
+                $element->postValue(array('value' => array(strval($value))));
+
+                return;
+            }
+        }
+
+        $value = strval($value);
+
+        if (in_array($elementName, array('input', 'textarea'))) {
+            $existingValueLength = strlen($element->attribute('value'));
+            // Add the TAB key to ensure we unfocus the field as browsers are triggering the change event only
+            // after leaving the field.
+            $value = str_repeat(Key::BACKSPACE . Key::DELETE, $existingValueLength) . $value;
+        }
+
+        $element->postValue(array('value' => array($value)));
+        // Remove the focus from the element if the field still has focus in
+        // order to trigger the change event. By doing this instead of simply
+        // triggering the change event for the given xpath we ensure that the
+        // change event will not be triggered twice for the same element if it
+        // has lost focus in the meanwhile. If the element has lost focus
+        // already then there is nothing to do as this will already have caused
+        // the triggering of the change event for that element.
+        $script = <<<JS
+var node = {{ELEMENT}};
+if (document.activeElement === node) {
+  document.activeElement.blur();
+}
+JS;
+        $this->executeJsOnElement($element, $script);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function check($xpath)
+    {
+        $element = $this->findElement($xpath);
+        $this->ensureInputType($element, $xpath, 'checkbox', 'check');
+
+        if ($element->selected()) {
+            return;
+        }
+
+        $this->clickOnElement($element);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function uncheck($xpath)
+    {
+        $element = $this->findElement($xpath);
+        $this->ensureInputType($element, $xpath, 'checkbox', 'uncheck');
+
+        if (!$element->selected()) {
+            return;
+        }
+
+        $this->clickOnElement($element);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isChecked($xpath)
+    {
+        return $this->findElement($xpath)->selected();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function selectOption($xpath, $value, $multiple = false)
+    {
+        $element = $this->findElement($xpath);
+        $tagName = strtolower($element->name());
+
+        if ('input' === $tagName && 'radio' === strtolower($element->attribute('type'))) {
+            $this->selectRadioValue($element, $value);
+
+            return;
+        }
+
+        if ('select' === $tagName) {
+            $this->selectOptionOnElement($element, $value, $multiple);
+
+            return;
+        }
+
+        throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isSelected($xpath)
+    {
+        return $this->findElement($xpath)->selected();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function click($xpath)
+    {
+        $this->clickOnElement($this->findElement($xpath));
+    }
+
+    private function clickOnElement(Element $element)
+    {
+        try {
+            // Move the mouse to the element as Selenium does not allow clicking on an element which is outside the viewport
+            $this->wdSession->moveto(array('element' => $element->getID()));
+        } catch (UnknownCommand $e) {
+            // If the Webdriver implementation does not support moveto (which is not part of the W3C WebDriver spec), proceed to the click
+        }
+
+        $element->click();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function doubleClick($xpath)
+    {
+        $this->mouseOver($xpath);
+        $this->wdSession->doubleclick();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rightClick($xpath)
+    {
+        $this->mouseOver($xpath);
+        $this->wdSession->click(array('button' => 2));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attachFile($xpath, $path)
+    {
+        $element = $this->findElement($xpath);
+        $this->ensureInputType($element, $xpath, 'file', 'attach a file on');
+
+        // Upload the file to Selenium and use the remote path. This will
+        // ensure that Selenium always has access to the file, even if it runs
+        // as a remote instance.
+        try {
+          $remotePath = $this->uploadFile($path);
+        } catch (\Exception $e) {
+          // File could not be uploaded to remote instance. Use the local path.
+          $remotePath = $path;
+        }
+
+        $element->postValue(array('value' => array($remotePath)));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isVisible($xpath)
+    {
+        return $this->findElement($xpath)->displayed();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function mouseOver($xpath)
+    {
+        $this->wdSession->moveto(array(
+            'element' => $this->findElement($xpath)->getID()
+        ));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function focus($xpath)
+    {
+        $this->trigger($xpath, 'focus');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function blur($xpath)
+    {
+        $this->trigger($xpath, 'blur');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function keyPress($xpath, $char, $modifier = null)
+    {
+        $options = self::charToOptions($char, $modifier);
+        $this->trigger($xpath, 'keypress', $options);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function keyDown($xpath, $char, $modifier = null)
+    {
+        $options = self::charToOptions($char, $modifier);
+        $this->trigger($xpath, 'keydown', $options);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function keyUp($xpath, $char, $modifier = null)
+    {
+        $options = self::charToOptions($char, $modifier);
+        $this->trigger($xpath, 'keyup', $options);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function dragTo($sourceXpath, $destinationXpath)
+    {
+        $source      = $this->findElement($sourceXpath);
+        $destination = $this->findElement($destinationXpath);
+
+        $this->wdSession->moveto(array(
+            'element' => $source->getID()
+        ));
+
+        $script = <<<JS
+(function (element) {
+    var event = document.createEvent("HTMLEvents");
+
+    event.initEvent("dragstart", true, true);
+    event.dataTransfer = {};
+
+    element.dispatchEvent(event);
+}({{ELEMENT}}));
+JS;
+        $this->withSyn()->executeJsOnElement($source, $script);
+
+        $this->wdSession->buttondown();
+        $this->wdSession->moveto(array(
+            'element' => $destination->getID()
+        ));
+        $this->wdSession->buttonup();
+
+        $script = <<<JS
+(function (element) {
+    var event = document.createEvent("HTMLEvents");
+
+    event.initEvent("drop", true, true);
+    event.dataTransfer = {};
+
+    element.dispatchEvent(event);
+}({{ELEMENT}}));
+JS;
+        $this->withSyn()->executeJsOnElement($destination, $script);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function executeScript($script)
+    {
+        if (preg_match('/^function[\s\(]/', $script)) {
+            $script = preg_replace('/;$/', '', $script);
+            $script = '(' . $script . ')';
+        }
+
+        $this->wdSession->execute(array('script' => $script, 'args' => array()));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function evaluateScript($script)
+    {
+        if (0 !== strpos(trim($script), 'return ')) {
+            $script = 'return ' . $script;
+        }
+
+        return $this->wdSession->execute(array('script' => $script, 'args' => array()));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function wait($timeout, $condition)
+    {
+        $script = "return $condition;";
+        $start = microtime(true);
+        $end = $start + $timeout / 1000.0;
+
+        do {
+            $result = $this->wdSession->execute(array('script' => $script, 'args' => array()));
+            usleep(100000);
+        } while (microtime(true) < $end && !$result);
+
+        return (bool) $result;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function resizeWindow($width, $height, $name = null)
+    {
+        $this->wdSession->window($name ? $name : 'current')->postSize(
+            array('width' => $width, 'height' => $height)
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function submitForm($xpath)
+    {
+        $this->findElement($xpath)->submit();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function maximizeWindow($name = null)
+    {
+        $this->wdSession->window($name ? $name : 'current')->maximize();
+    }
+
+    /**
+     * Returns Session ID of WebDriver or `null`, when session not started yet.
+     *
+     * @return string|null
+     */
+    public function getWebDriverSessionId()
+    {
+        return $this->isStarted() ? basename($this->wdSession->getUrl()) : null;
+    }
+
+    /**
+     * @param string $xpath
+     *
+     * @return Element
+     */
+    private function findElement($xpath)
+    {
+        return $this->wdSession->element('xpath', $xpath);
+    }
+
+    /**
+     * Selects a value in a radio button group
+     *
+     * @param Element $element An element referencing one of the radio buttons of the group
+     * @param string  $value   The value to select
+     *
+     * @throws DriverException when the value cannot be found
+     */
+    private function selectRadioValue(Element $element, $value)
+    {
+        // short-circuit when we already have the right button of the group to avoid XPath queries
+        if ($element->attribute('value') === $value) {
+            $element->click();
+
+            return;
+        }
+
+        $name = $element->attribute('name');
+
+        if (!$name) {
+            throw new DriverException(sprintf('The radio button does not have the value "%s"', $value));
+        }
+
+        $formId = $element->attribute('form');
+
+        try {
+            if (null !== $formId) {
+                $xpath = <<<'XPATH'
+//form[@id=%1$s]//input[@type="radio" and not(@form) and @name=%2$s and @value = %3$s]
+|
+//input[@type="radio" and @form=%1$s and @name=%2$s and @value = %3$s]
+XPATH;
+
+                $xpath = sprintf(
+                    $xpath,
+                    $this->xpathEscaper->escapeLiteral($formId),
+                    $this->xpathEscaper->escapeLiteral($name),
+                    $this->xpathEscaper->escapeLiteral($value)
+                );
+                $input = $this->wdSession->element('xpath', $xpath);
+            } else {
+                $xpath = sprintf(
+                    './ancestor::form//input[@type="radio" and not(@form) and @name=%s and @value = %s]',
+                    $this->xpathEscaper->escapeLiteral($name),
+                    $this->xpathEscaper->escapeLiteral($value)
+                );
+                $input = $element->element('xpath', $xpath);
+            }
+        } catch (NoSuchElement $e) {
+            $message = sprintf('The radio group "%s" does not have an option "%s"', $name, $value);
+
+            throw new DriverException($message, 0, $e);
+        }
+
+        $input->click();
+    }
+
+    /**
+     * @param Element $element
+     * @param string  $value
+     * @param bool    $multiple
+     */
+    private function selectOptionOnElement(Element $element, $value, $multiple = false)
+    {
+        $escapedValue = $this->xpathEscaper->escapeLiteral($value);
+        // The value of an option is the normalized version of its text when it has no value attribute
+        $optionQuery = sprintf('.//option[@value = %s or (not(@value) and normalize-space(.) = %s)]', $escapedValue, $escapedValue);
+        $option = $element->element('xpath', $optionQuery);
+
+        if ($multiple || !$element->attribute('multiple')) {
+            if (!$option->selected()) {
+                $option->click();
+            }
+
+            return;
+        }
+
+        // Deselect all options before selecting the new one
+        $this->deselectAllOptions($element);
+        $option->click();
+    }
+
+    /**
+     * Deselects all options of a multiple select
+     *
+     * Note: this implementation does not trigger a change event after deselecting the elements.
+     *
+     * @param Element $element
+     */
+    private function deselectAllOptions(Element $element)
+    {
+        $script = <<<JS
+var node = {{ELEMENT}};
+var i, l = node.options.length;
+for (i = 0; i < l; i++) {
+    node.options[i].selected = false;
+}
+JS;
+
+        $this->executeJsOnElement($element, $script);
+    }
+
+    /**
+     * Ensures the element is a checkbox
+     *
+     * @param Element $element
+     * @param string  $xpath
+     * @param string  $type
+     * @param string  $action
+     *
+     * @throws DriverException
+     */
+    private function ensureInputType(Element $element, $xpath, $type, $action)
+    {
+        if ('input' !== strtolower($element->name()) || $type !== strtolower($element->attribute('type'))) {
+            $message = 'Impossible to %s the element with XPath "%s" as it is not a %s input';
+
+            throw new DriverException(sprintf($message, $action, $xpath, $type));
+        }
+    }
+
+    /**
+     * @param $xpath
+     * @param $event
+     * @param string $options
+     */
+    private function trigger($xpath, $event, $options = '{}')
+    {
+        $script = 'Syn.trigger("' . $event . '", ' . $options . ', {{ELEMENT}})';
+        $this->withSyn()->executeJsOnXpath($xpath, $script);
+    }
+
+    /**
+     * Uploads a file to the Selenium instance.
+     *
+     * Note that uploading files is not part of the official WebDriver
+     * specification, but it is supported by Selenium.
+     *
+     * @param string $path     The path to the file to upload.
+     *
+     * @return string          The remote path.
+     *
+     * @throws DriverException When PHP is compiled without zip support, or the file doesn't exist.
+     * @throws UnknownError    When an unknown error occurred during file upload.
+     * @throws \Exception      When a known error occurred during file upload.
+     *
+     * @see https://github.com/SeleniumHQ/selenium/blob/master/py/selenium/webdriver/remote/webelement.py#L533
+     */
+    private function uploadFile($path)
+    {
+        if (!is_file($path)) {
+          throw new DriverException('File does not exist locally and cannot be uploaded to the remote instance.');
+        }
+
+        if (!class_exists('ZipArchive')) {
+          throw new DriverException('Could not compress file, PHP is compiled without zip support.');
+        }
+
+        // Selenium only accepts uploads that are compressed as a Zip archive.
+        // @todo work out a better way to create an empty zip archive. PHP has
+        //   deprecated using ZipArchive with an empty file but there is no obvious
+        //   replacement. Deleting the file could lead to problems during
+        //   concurrent testing so add .zip so the file does not exist.
+        $tempFilename = tempnam('', 'WebDriverZip') . '.zip';
+
+        $archive = new \ZipArchive();
+        $result = $archive->open($tempFilename, \ZipArchive::CREATE);
+        if (!$result) {
+          throw new DriverException('Zip archive could not be created. Error ' . $result);
+        }
+        $result = $archive->addFile($path, basename($path));
+        if (!$result) {
+          throw new DriverException('File could not be added to zip archive.');
+        }
+        $result = $archive->close();
+        if (!$result) {
+          throw new DriverException('Zip archive could not be closed.');
+        }
+
+        try {
+          $remotePath = $this->wdSession->file(array('file' => base64_encode(file_get_contents($tempFilename))));
+
+          // If no path is returned the file upload failed silently. In this
+          // case it is possible Selenium was not used but another web driver
+          // such as PhantomJS.
+          // @todo Support other drivers when (if) they get remote file transfer
+          // capability.
+          if (empty($remotePath)) {
+            throw new UnknownError();
+          }
+        } catch (\Exception $e) {
+          // Catch any error so we can still clean up the temporary archive.
+        }
+
+        unlink($tempFilename);
+
+        if (isset($e)) {
+          throw $e;
+        }
+
+        return $remotePath;
+    }
+
+}
diff --git a/core/lib/Drupal/Core/Php8/Doctrine/Reflection/StaticReflectionClass.php b/core/lib/Drupal/Core/Php8/Doctrine/Reflection/StaticReflectionClass.php
new file mode 100644
index 0000000000..e24798e47b
--- /dev/null
+++ b/core/lib/Drupal/Core/Php8/Doctrine/Reflection/StaticReflectionClass.php
@@ -0,0 +1,415 @@
+<?php
+// @codingStandardsIgnoreFile
+
+namespace Drupal\Core\Php8\Doctrine\Reflection;
+
+use Doctrine\Common\Reflection\StaticReflectionParser;
+use ReflectionClass;
+use ReflectionException;
+
+class StaticReflectionClass extends ReflectionClass
+{
+    /**
+     * The static reflection parser object.
+     *
+     * @var StaticReflectionParser
+     */
+    private $staticReflectionParser;
+
+    public function __construct(StaticReflectionParser $staticReflectionParser)
+    {
+        $this->staticReflectionParser = $staticReflectionParser;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getName()
+    {
+        return $this->staticReflectionParser->getClassName();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getDocComment()
+    {
+        return $this->staticReflectionParser->getDocComment();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getNamespaceName()
+    {
+        return $this->staticReflectionParser->getNamespaceName();
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getUseStatements()
+    {
+        return $this->staticReflectionParser->getUseStatements();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getMethod($name)
+    {
+        return $this->staticReflectionParser->getReflectionMethod($name);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getProperty($name)
+    {
+        return $this->staticReflectionParser->getReflectionProperty($name);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public static function export($argument, $return = false)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getConstant($name)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getConstants(int $filter = \ReflectionClassConstant::IS_PUBLIC | \ReflectionClassConstant::IS_PROTECTED | \ReflectionClassConstant::IS_PRIVATE)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getConstructor()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getDefaultProperties()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getEndLine()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getExtension()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getExtensionName()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getFileName()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getInterfaceNames()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getInterfaces()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getMethods($filter = null)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getModifiers()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getParentClass()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getProperties($filter = null)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getShortName()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getStartLine()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getStaticProperties()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getStaticPropertyValue($name, $default = '')
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getTraitAliases()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getTraitNames()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getTraits()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function hasConstant($name)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function hasMethod($name)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function hasProperty($name)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function implementsInterface($interface)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function inNamespace()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isAbstract()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isCloneable()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isFinal()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isInstance($object)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isInstantiable()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isInterface()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isInternal()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isIterateable()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isSubclassOf($class)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isTrait()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function isUserDefined()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function newInstance(...$args)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function newInstanceArgs(array $args = [])
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function newInstanceWithoutConstructor()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function setStaticPropertyValue($name, $value)
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function __toString()
+    {
+        throw new ReflectionException('Method not implemented');
+    }
+}
diff --git a/core/lib/Drupal/Core/Php8/Phpdocumentor/ReflectionDocBlock/StandardTagFactory.php b/core/lib/Drupal/Core/Php8/Phpdocumentor/ReflectionDocBlock/StandardTagFactory.php
new file mode 100644
index 0000000000..f9f4311071
--- /dev/null
+++ b/core/lib/Drupal/Core/Php8/Phpdocumentor/ReflectionDocBlock/StandardTagFactory.php
@@ -0,0 +1,341 @@
+<?php
+
+declare(strict_types=1);
+
+// @codingStandardsIgnoreFile
+
+namespace Drupal\Core\Php8\Phpdocumentor\ReflectionDocBlock;
+/**
+ * This file is part of phpDocumentor.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @link http://phpdoc.org
+ */
+
+use InvalidArgumentException;
+use phpDocumentor\Reflection\DocBlock\Tag;
+use phpDocumentor\Reflection\DocBlock\TagFactory;
+use phpDocumentor\Reflection\DocBlock\Tags\Author;
+use phpDocumentor\Reflection\DocBlock\Tags\Covers;
+use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
+use phpDocumentor\Reflection\DocBlock\Tags\Factory\StaticMethod;
+use phpDocumentor\Reflection\DocBlock\Tags\Generic;
+use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
+use phpDocumentor\Reflection\DocBlock\Tags\Link as LinkTag;
+use phpDocumentor\Reflection\DocBlock\Tags\Method;
+use phpDocumentor\Reflection\DocBlock\Tags\Param;
+use phpDocumentor\Reflection\DocBlock\Tags\Property;
+use phpDocumentor\Reflection\DocBlock\Tags\PropertyRead;
+use phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite;
+use phpDocumentor\Reflection\DocBlock\Tags\Return_;
+use phpDocumentor\Reflection\DocBlock\Tags\See as SeeTag;
+use phpDocumentor\Reflection\DocBlock\Tags\Since;
+use phpDocumentor\Reflection\DocBlock\Tags\Source;
+use phpDocumentor\Reflection\DocBlock\Tags\Throws;
+use phpDocumentor\Reflection\DocBlock\Tags\Uses;
+use phpDocumentor\Reflection\DocBlock\Tags\Var_;
+use phpDocumentor\Reflection\DocBlock\Tags\Version;
+use phpDocumentor\Reflection\FqsenResolver;
+use phpDocumentor\Reflection\Types\Context as TypeContext;
+use ReflectionMethod;
+use ReflectionParameter;
+use Webmozart\Assert\Assert;
+use function array_merge;
+use function array_slice;
+use function call_user_func_array;
+use function count;
+use function get_class;
+use function preg_match;
+use function strpos;
+use function trim;
+
+/**
+ * Creates a Tag object given the contents of a tag.
+ *
+ * This Factory is capable of determining the appropriate class for a tag and instantiate it using its `create`
+ * factory method. The `create` factory method of a Tag can have a variable number of arguments; this way you can
+ * pass the dependencies that you need to construct a tag object.
+ *
+ * > Important: each parameter in addition to the body variable for the `create` method must default to null, otherwise
+ * > it violates the constraint with the interface; it is recommended to use the {@see Assert::notNull()} method to
+ * > verify that a dependency is actually passed.
+ *
+ * This Factory also features a Service Locator component that is used to pass the right dependencies to the
+ * `create` method of a tag; each dependency should be registered as a service or as a parameter.
+ *
+ * When you want to use a Tag of your own with custom handling you need to call the `registerTagHandler` method, pass
+ * the name of the tag and a Fully Qualified Class Name pointing to a class that implements the Tag interface.
+ */
+final class StandardTagFactory implements TagFactory
+{
+    /** PCRE regular expression matching a tag name. */
+    public const REGEX_TAGNAME = '[\w\-\_\\\\:]+';
+
+    /**
+     * @var array<class-string<Tag>> An array with a tag as a key, and an
+     *                               FQCN to a class that handles it as an array value.
+     */
+    private $tagHandlerMappings = [
+        'author' => Author::class,
+        'covers' => Covers::class,
+        'deprecated' => Deprecated::class,
+        // 'example'        => '\phpDocumentor\Reflection\DocBlock\Tags\Example',
+        'link' => LinkTag::class,
+        'method' => Method::class,
+        'param' => Param::class,
+        'property-read' => PropertyRead::class,
+        'property' => Property::class,
+        'property-write' => PropertyWrite::class,
+        'return' => Return_::class,
+        'see' => SeeTag::class,
+        'since' => Since::class,
+        'source' => Source::class,
+        'throw' => Throws::class,
+        'throws' => Throws::class,
+        'uses' => Uses::class,
+        'var' => Var_::class,
+        'version' => Version::class,
+    ];
+
+    /**
+     * @var array<class-string<Tag>> An array with a anotation s a key, and an
+     *      FQCN to a class that handles it as an array value.
+     */
+    private $annotationMappings = [];
+
+    /**
+     * @var ReflectionParameter[][] a lazy-loading cache containing parameters
+     *      for each tagHandler that has been used.
+     */
+    private $tagHandlerParameterCache = [];
+
+    /** @var FqsenResolver */
+    private $fqsenResolver;
+
+    /**
+     * @var mixed[] an array representing a simple Service Locator where we can store parameters and
+     *     services that can be inserted into the Factory Methods of Tag Handlers.
+     */
+    private $serviceLocator = [];
+
+    /**
+     * Initialize this tag factory with the means to resolve an FQSEN and optionally a list of tag handlers.
+     *
+     * If no tag handlers are provided than the default list in the {@see self::$tagHandlerMappings} property
+     * is used.
+     *
+     * @see self::registerTagHandler() to add a new tag handler to the existing default list.
+     *
+     * @param array<class-string<Tag>> $tagHandlers
+     */
+    public function __construct(FqsenResolver $fqsenResolver, ?array $tagHandlers = null)
+    {
+        $this->fqsenResolver = $fqsenResolver;
+        if ($tagHandlers !== null) {
+            $this->tagHandlerMappings = $tagHandlers;
+        }
+
+        $this->addService($fqsenResolver, FqsenResolver::class);
+    }
+
+    public function create(string $tagLine, ?TypeContext $context = null) : Tag
+    {
+        if (!$context) {
+            $context = new TypeContext('');
+        }
+
+        [$tagName, $tagBody] = $this->extractTagParts($tagLine);
+
+        return $this->createTag(trim($tagBody), $tagName, $context);
+    }
+
+    /**
+     * @param mixed $value
+     */
+    public function addParameter(string $name, $value) : void
+    {
+        $this->serviceLocator[$name] = $value;
+    }
+
+    public function addService(object $service, ?string $alias = null) : void
+    {
+        $this->serviceLocator[$alias ?: get_class($service)] = $service;
+    }
+
+    public function registerTagHandler(string $tagName, string $handler) : void
+    {
+        Assert::stringNotEmpty($tagName);
+        Assert::classExists($handler);
+        Assert::implementsInterface($handler, StaticMethod::class);
+
+        if (strpos($tagName, '\\') && $tagName[0] !== '\\') {
+            throw new InvalidArgumentException(
+                'A namespaced tag must have a leading backslash as it must be fully qualified'
+            );
+        }
+
+        $this->tagHandlerMappings[$tagName] = $handler;
+    }
+
+    /**
+     * Extracts all components for a tag.
+     *
+     * @return string[]
+     */
+    private function extractTagParts(string $tagLine) : array
+    {
+        $matches = [];
+        if (!preg_match('/^@(' . self::REGEX_TAGNAME . ')((?:[\s\(\{])\s*([^\s].*)|$)/us', $tagLine, $matches)) {
+            throw new InvalidArgumentException(
+                'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors'
+            );
+        }
+
+        if (count($matches) < 3) {
+            $matches[] = '';
+        }
+
+        return array_slice($matches, 1);
+    }
+
+    /**
+     * Creates a new tag object with the given name and body or returns null if the tag name was recognized but the
+     * body was invalid.
+     */
+    private function createTag(string $body, string $name, TypeContext $context) : Tag
+    {
+        $handlerClassName = $this->findHandlerClassName($name, $context);
+        $arguments        = $this->getArgumentsForParametersFromWiring(
+            $this->fetchParametersForHandlerFactoryMethod($handlerClassName),
+            $this->getServiceLocatorWithDynamicParameters($context, $name, $body)
+        );
+
+        try {
+            $callable = [$handlerClassName, 'create'];
+            Assert::isCallable($callable);
+            /** @phpstan-var callable(string): ?Tag $callable */
+            $tag = call_user_func_array($callable, $arguments);
+
+            return $tag ?? InvalidTag::create($body, $name);
+        } catch (InvalidArgumentException $e) {
+            return InvalidTag::create($body, $name)->withError($e);
+        }
+    }
+
+    /**
+     * Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`).
+     *
+     * @return class-string<Tag>
+     */
+    private function findHandlerClassName(string $tagName, TypeContext $context) : string
+    {
+        $handlerClassName = Generic::class;
+        if (isset($this->tagHandlerMappings[$tagName])) {
+            $handlerClassName = $this->tagHandlerMappings[$tagName];
+        } elseif ($this->isAnnotation($tagName)) {
+            // TODO: Annotation support is planned for a later stage and as such is disabled for now
+            $tagName = (string) $this->fqsenResolver->resolve($tagName, $context);
+            if (isset($this->annotationMappings[$tagName])) {
+                $handlerClassName = $this->annotationMappings[$tagName];
+            }
+        }
+
+        return $handlerClassName;
+    }
+
+    /**
+     * Retrieves the arguments that need to be passed to the Factory Method with the given Parameters.
+     *
+     * @param ReflectionParameter[] $parameters
+     * @param mixed[]               $locator
+     *
+     * @return mixed[] A series of values that can be passed to the Factory Method of the tag whose parameters
+     *     is provided with this method.
+     */
+    private function getArgumentsForParametersFromWiring(array $parameters, array $locator) : array
+    {
+        $arguments = [];
+        foreach ($parameters as $parameter) {
+            $typeHint = null;
+            if ($parameter->hasType() && !$parameter->getType()->isBuiltin()) {
+                $typeHint = $parameter->getType()->getName();
+            }
+
+            if (isset($locator[$typeHint])) {
+                $arguments[] = $locator[$typeHint];
+                continue;
+            }
+
+            $parameterName = $parameter->getName();
+            if (isset($locator[$parameterName])) {
+                $arguments[] = $locator[$parameterName];
+                continue;
+            }
+
+            $arguments[] = null;
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Retrieves a series of ReflectionParameter objects for the static 'create' method of the given
+     * tag handler class name.
+     *
+     * @return ReflectionParameter[]
+     */
+    private function fetchParametersForHandlerFactoryMethod(string $handlerClassName) : array
+    {
+        if (!isset($this->tagHandlerParameterCache[$handlerClassName])) {
+            $methodReflection                                  = new ReflectionMethod($handlerClassName, 'create');
+            $this->tagHandlerParameterCache[$handlerClassName] = $methodReflection->getParameters();
+        }
+
+        return $this->tagHandlerParameterCache[$handlerClassName];
+    }
+
+    /**
+     * Returns a copy of this class' Service Locator with added dynamic parameters,
+     * such as the tag's name, body and Context.
+     *
+     * @param TypeContext $context The Context (namespace and aliasses) that may be
+     *  passed and is used to resolve FQSENs.
+     * @param string      $tagName The name of the tag that may be
+     *  passed onto the factory method of the Tag class.
+     * @param string      $tagBody The body of the tag that may be
+     *  passed onto the factory method of the Tag class.
+     *
+     * @return mixed[]
+     */
+    private function getServiceLocatorWithDynamicParameters(
+        TypeContext $context,
+        string $tagName,
+        string $tagBody
+    ) : array {
+        return array_merge(
+            $this->serviceLocator,
+            [
+                'name' => $tagName,
+                'body' => $tagBody,
+                TypeContext::class => $context,
+            ]
+        );
+    }
+
+    /**
+     * Returns whether the given tag belongs to an annotation.
+     *
+     * @todo this method should be populated once we implement Annotation notation support.
+     */
+    private function isAnnotation(string $tagContent) : bool
+    {
+        // 1. Contains a namespace separator
+        // 2. Contains parenthesis
+        // 3. Is present in a list of known annotations (make the algorithm smart by first checking is the last part
+        //    of the annotation class name matches the found tag name
+
+        return false;
+    }
+}
diff --git a/core/lib/Drupal/Core/Php8/Phpspec/Prophecy/ClassMirror.php b/core/lib/Drupal/Core/Php8/Phpspec/Prophecy/ClassMirror.php
new file mode 100644
index 0000000000..eb039d97c2
--- /dev/null
+++ b/core/lib/Drupal/Core/Php8/Phpspec/Prophecy/ClassMirror.php
@@ -0,0 +1,247 @@
+<?php
+// @codingStandardsIgnoreFile
+
+namespace Drupal\Core\Php8\Phpspec\Prophecy;
+/*
+ * This file is part of the Prophecy.
+ * (c) Konstantin Kudryashov <ever.zet@gmail.com>
+ *     Marcello Duarte <marcello.duarte@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Prophecy\Doubler\Generator\Node;
+use Prophecy\Exception\InvalidArgumentException;
+use Prophecy\Exception\Doubler\ClassMirrorException;
+use ReflectionClass;
+use ReflectionMethod;
+use ReflectionParameter;
+
+/**
+ * Class mirror.
+ * Core doubler class. Mirrors specific class and/or interfaces into class node tree.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class ClassMirror
+{
+    private static $reflectableMethods = array(
+        '__construct',
+        '__destruct',
+        '__sleep',
+        '__wakeup',
+        '__toString',
+        '__call',
+        '__invoke'
+    );
+
+    /**
+     * Reflects provided arguments into class node.
+     *
+     * @param ReflectionClass   $class
+     * @param ReflectionClass[] $interfaces
+     *
+     * @return Node\ClassNode
+     *
+     * @throws \Prophecy\Exception\InvalidArgumentException
+     */
+    public function reflect(ReflectionClass $class = null, array $interfaces)
+    {
+        $node = new Node\ClassNode;
+
+        if (null !== $class) {
+            if (true === $class->isInterface()) {
+                throw new InvalidArgumentException(sprintf(
+                    "Could not reflect %s as a class, because it\n".
+                    "is interface - use the second argument instead.",
+                    $class->getName()
+                ));
+            }
+
+            $this->reflectClassToNode($class, $node);
+        }
+
+        foreach ($interfaces as $interface) {
+            if (!$interface instanceof ReflectionClass) {
+                throw new InvalidArgumentException(sprintf(
+                    "[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n".
+                    "a second argument to `ClassMirror::reflect(...)`, but got %s.",
+                    is_object($interface) ? get_class($interface).' class' : gettype($interface)
+                ));
+            }
+            if (false === $interface->isInterface()) {
+                throw new InvalidArgumentException(sprintf(
+                    "Could not reflect %s as an interface, because it\n".
+                    "is class - use the first argument instead.",
+                    $interface->getName()
+                ));
+            }
+
+            $this->reflectInterfaceToNode($interface, $node);
+        }
+
+        $node->addInterface('Prophecy\Doubler\Generator\ReflectionInterface');
+
+        return $node;
+    }
+
+    private function reflectClassToNode(ReflectionClass $class, Node\ClassNode $node)
+    {
+        if (true === $class->isFinal()) {
+            throw new ClassMirrorException(sprintf(
+                'Could not reflect class %s as it is marked final.', $class->getName()
+            ), $class);
+        }
+
+        $node->setParentClass($class->getName());
+
+        foreach ($class->getMethods(ReflectionMethod::IS_ABSTRACT) as $method) {
+            if (false === $method->isProtected()) {
+                continue;
+            }
+
+            $this->reflectMethodToNode($method, $node);
+        }
+
+        foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+            if (0 === strpos($method->getName(), '_')
+                && !in_array($method->getName(), self::$reflectableMethods)) {
+                continue;
+            }
+
+            if (true === $method->isFinal()) {
+                $node->addUnextendableMethod($method->getName());
+                continue;
+            }
+
+            $this->reflectMethodToNode($method, $node);
+        }
+    }
+
+    private function reflectInterfaceToNode(ReflectionClass $interface, Node\ClassNode $node)
+    {
+        $node->addInterface($interface->getName());
+
+        foreach ($interface->getMethods() as $method) {
+            $this->reflectMethodToNode($method, $node);
+        }
+    }
+
+    private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $classNode)
+    {
+        $node = new Node\MethodNode($method->getName());
+
+        if (true === $method->isProtected()) {
+            $node->setVisibility('protected');
+        }
+
+        if (true === $method->isStatic()) {
+            $node->setStatic();
+        }
+
+        if (true === $method->returnsReference()) {
+            $node->setReturnsReference();
+        }
+
+        if (version_compare(PHP_VERSION, '7.0', '>=') && $method->hasReturnType()) {
+            $returnType = PHP_VERSION_ID >= 70100 ? $method->getReturnType()->getName() : (string) $method->getReturnType();
+            $returnTypeLower = strtolower($returnType);
+
+            if ('self' === $returnTypeLower) {
+                $returnType = $method->getDeclaringClass()->getName();
+            }
+            if ('parent' === $returnTypeLower) {
+                $returnType = $method->getDeclaringClass()->getParentClass()->getName();
+            }
+
+            $node->setReturnType($returnType);
+
+            if (version_compare(PHP_VERSION, '7.1', '>=') && $method->getReturnType()->allowsNull()) {
+                $node->setNullableReturnType(true);
+            }
+        }
+
+        if (is_array($params = $method->getParameters()) && count($params)) {
+            foreach ($params as $param) {
+                $this->reflectArgumentToNode($param, $node);
+            }
+        }
+
+        $classNode->addMethod($node);
+    }
+
+    private function reflectArgumentToNode(ReflectionParameter $parameter, Node\MethodNode $methodNode)
+    {
+        $name = $parameter->getName() == '...' ? '__dot_dot_dot__' : $parameter->getName();
+        $node = new Node\ArgumentNode($name);
+
+        $node->setTypeHint($this->getTypeHint($parameter));
+
+        if ($this->isVariadic($parameter)) {
+            $node->setAsVariadic();
+        }
+
+        if ($this->hasDefaultValue($parameter)) {
+            $node->setDefault($this->getDefaultValue($parameter));
+        }
+
+        if ($parameter->isPassedByReference()) {
+            $node->setAsPassedByReference();
+        }
+
+        $node->setAsNullable($this->isNullable($parameter));
+
+        $methodNode->addArgument($node);
+    }
+
+    private function hasDefaultValue(ReflectionParameter $parameter)
+    {
+        if ($this->isVariadic($parameter)) {
+            return false;
+        }
+
+        if ($parameter->isDefaultValueAvailable()) {
+            return true;
+        }
+
+        return $parameter->isOptional() || $this->isNullable($parameter);
+    }
+
+    private function getDefaultValue(ReflectionParameter $parameter)
+    {
+        if (!$parameter->isDefaultValueAvailable()) {
+            return null;
+        }
+
+        return $parameter->getDefaultValue();
+    }
+
+    private function getTypeHint(ReflectionParameter $parameter)
+    {
+        if (version_compare(PHP_VERSION, '7.0', '>=') && true === $parameter->hasType()) {
+            $type = $parameter->getType();
+            if ($type instanceof \ReflectionUnionType) {
+               return implode('|', $type->getTypes());
+            }
+            $typehint = $type->getName();
+            if ($typehint === 'self') {
+              $typehint = $parameter->getDeclaringClass()->getName();
+            }
+            return $typehint;
+        }
+
+        return null;
+    }
+
+    private function isVariadic(ReflectionParameter $parameter)
+    {
+        return PHP_VERSION_ID >= 50600 && $parameter->isVariadic();
+    }
+
+    private function isNullable(ReflectionParameter $parameter)
+    {
+        return $parameter->allowsNull() && null !== $this->getTypeHint($parameter);
+    }
+
+}
diff --git a/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php b/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
index bd31f529cc..af1f10a22c 100644
--- a/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
@@ -70,8 +70,8 @@ public function testNodeTranslation() {
 
     $normalized = $this->serializer->normalize($node, $this->format);
 
-    $this->assertContains(['lang' => 'en', 'value' => $node->getTitle()], $normalized['title'], 'Original language title has been normalized.');
-    $this->assertContains(['lang' => 'de', 'value' => $translation->getTitle()], $normalized['title'], 'Translation language title has been normalized.');
+    $this->assertContainsEquals(['lang' => 'en', 'value' => $node->getTitle()], $normalized['title'], 'Original language title has been normalized.');
+    $this->assertContainsEquals(['lang' => 'de', 'value' => $translation->getTitle()], $normalized['title'], 'Translation language title has been normalized.');
 
     /** @var \Drupal\node\NodeInterface $denormalized_node */
     $denormalized_node = $this->serializer->denormalize($normalized, 'Drupal\node\Entity\Node', $this->format);
diff --git a/core/modules/migrate/tests/src/Unit/MigrateStubTest.php b/core/modules/migrate/tests/src/Unit/MigrateStubTest.php
index fcf171b17c..79df417040 100644
--- a/core/modules/migrate/tests/src/Unit/MigrateStubTest.php
+++ b/core/modules/migrate/tests/src/Unit/MigrateStubTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\migrate\Unit;
 
 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use Drupal\migrate\MigrateStub;
 use Drupal\migrate\Plugin\MigrateDestinationInterface;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
@@ -22,6 +23,8 @@
  */
 class MigrateStubTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   /**
    * The plugin manager prophecy.
    *
diff --git a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
index 2d5e1678ac..1975274326 100644
--- a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
+++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
@@ -28,7 +28,7 @@ class GDToolkit extends ImageToolkitBase {
   /**
    * A GD image resource.
    *
-   * @var resource|null
+   * @var resource|\GdImage|null
    */
   protected $resource = NULL;
 
@@ -98,6 +98,8 @@ public function __construct(array $configuration, $plugin_id, array $plugin_defi
    * Destructs a GDToolkit object.
    *
    * Frees memory associated with a GD image resource.
+   *
+   * @todo Remove the method for PHP 8.0+ https://www.drupal.org/node/3173031
    */
   public function __destruct() {
     if (is_resource($this->resource)) {
@@ -124,15 +126,20 @@ public static function create(ContainerInterface $container, array $configuratio
   /**
    * Sets the GD image resource.
    *
-   * @param resource $resource
+   * @param resource|\GdImage $resource
    *   The GD image resource.
    *
    * @return $this
    *   An instance of the current toolkit object.
    */
   public function setResource($resource) {
-    if (!is_resource($resource) || get_resource_type($resource) != 'gd') {
-      throw new \InvalidArgumentException('Invalid resource argument');
+    if (!(is_object($resource) && $resource instanceof \GdImage)) {
+      // Since PHP 8.0 resource should be \GdImage, for previous versions it
+      // should be resource.
+      // @TODO clean-up for PHP 8.0+ https://www.drupal.org/node/3173031
+      if (!is_resource($resource) || get_resource_type($resource) != 'gd') {
+        throw new \InvalidArgumentException('Invalid resource argument');
+      }
     }
     $this->preLoadInfo = NULL;
     $this->resource = $resource;
@@ -142,11 +149,12 @@ public function setResource($resource) {
   /**
    * Retrieves the GD image resource.
    *
-   * @return resource|null
+   * @return resource|\GdImage|null
    *   The GD image resource, or NULL if not available.
    */
   public function getResource() {
-    if (!is_resource($this->resource)) {
+    // @TODO clean-up for PHP 8.0+ https://www.drupal.org/node/3173031
+    if (!(is_resource($this->resource) || (is_object($this->resource) && $this->resource instanceof \GdImage))) {
       $this->load();
     }
     return $this->resource;
diff --git a/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php b/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php
index 16b9810535..a4bea10bdf 100644
--- a/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php
+++ b/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php
@@ -126,7 +126,7 @@ public function testReferenceablesWithNoLabelKey($match, $match_operator, $limit
       // entity labels.
       // @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface::getReferenceableEntities()
       $item = is_string($item) ? Html::escape($item) : $item;
-      $this->assertContains($item, $referenceables[$this->bundle]);
+      $this->assertContainsEquals($item, $referenceables[$this->bundle]);
     }
 
     // Test ::countReferenceableEntities().
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 955eb5bf55..91b4778261 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -149,7 +149,7 @@
 }
 
 if (!Composer::upgradePHPUnitCheck(Version::id())) {
-  simpletest_script_print_error("PHPUnit testing framework version 7 or greater is required when running on PHP 7.3 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this.");
+  simpletest_script_print_error("PHPUnit testing framework version 9 or greater is required when running on PHP 7.4 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this.");
   exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
 }
 
diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
index 0e8fee1021..7bd4f70347 100644
--- a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -7,7 +7,7 @@
 use Behat\Mink\Mink;
 use Behat\Mink\Session;
 use Drupal\Component\FileSystem\FileSystem as DrupalFilesystem;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
 use Symfony\Component\Finder\Finder;
@@ -52,7 +52,7 @@
 abstract class BuildTestBase extends TestCase {
 
   use ExternalCommandRequirementsTrait;
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * The working directory where this test will manipulate files.
diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
index c96a0f1947..5693f7859d 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
@@ -21,7 +21,7 @@ public function testHtRouter() {
     }
 
     $this->copyCodebase();
-    $this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
+    $this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction --ignore-platform-reqs');
     $this->assertErrorOutputContains('Generating autoload files');
     $this->installQuickStart('minimal');
     $this->formLogin($this->adminUsername, $this->adminPassword);
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
index 3719bcc64e..8082b5161e 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
@@ -74,7 +74,11 @@ public function uploadFileAndGetRemoteFilePath($path) {
     }
 
     // Selenium only accepts uploads that are compressed as a Zip archive.
-    $tempFilename = tempnam('', 'WebDriverZip');
+    // @todo work out a better way to create an empty zip archive. PHP has
+    //   deprecated using ZipArchive with an empty file but there is no obvious
+    //   replacement. Deleting the file could lead to problems during
+    //   concurrent testing so add .zip so the file does not exist.
+    $tempFilename = tempnam('', 'WebDriverZip') . '.zip';
 
     $archive = new \ZipArchive();
     $result = $archive->open($tempFilename, \ZipArchive::CREATE);
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
index 1e6c1b3ead..e5925e8100 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
@@ -612,7 +612,7 @@ public function testUnmetDependency() {
         'Configuration <em class="placeholder">unknown.config</em> depends on the <em class="placeholder">unknown</em> extension that will not be installed after import.',
       ];
       foreach ($expected as $expected_message) {
-        $this->assertContains($expected_message, $error_log, $expected_message);
+        $this->assertContainsEquals($expected_message, $error_log, $expected_message);
       }
     }
 
@@ -658,7 +658,7 @@ public function testUnmetDependency() {
         'Configuration <em class="placeholder">config_test.dynamic.dotted.theme</em> depends on themes (<em class="placeholder">unknown, Seven</em>) that will not be installed after import.',
       ];
       foreach ($expected as $expected_message) {
-        $this->assertContains($expected_message, $error_log, $expected_message);
+        $this->assertContainsEquals($expected_message, $error_log, $expected_message);
       }
     }
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
index f568f9d1b4..e1d7c2739f 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
@@ -67,14 +67,14 @@ public function testArrayArgumentsSQLInjection() {
   public function testConditionOperatorArgumentsSQLInjection() {
     $injection = "IS NOT NULL) ;INSERT INTO {test} (name) VALUES ('test12345678'); -- ";
 
-    $previous_error_handler = set_error_handler(function ($severity, $message, $filename, $lineno, $context) use (&$previous_error_handler) {
+    $previous_error_handler = set_error_handler(function ($severity, $message, $filename, $lineno) use (&$previous_error_handler) {
       // Normalize the filename to use UNIX directory separators.
       if (preg_match('@core/lib/Drupal/Core/Database/Query/Condition.php$@', str_replace(DIRECTORY_SEPARATOR, '/', $filename))) {
         // Convert errors to exceptions for testing purposes below.
         throw new \ErrorException($message, 0, $severity, $filename, $lineno);
       }
       if ($previous_error_handler) {
-        return $previous_error_handler($severity, $message, $filename, $lineno, $context);
+        return $previous_error_handler($severity, $message, $filename, $lineno);
       }
     });
     try {
diff --git a/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php b/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php
index 1e97b7a805..991f9263c9 100644
--- a/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php
@@ -42,7 +42,7 @@ public function testMunging() {
     $munged_name = file_munge_filename($this->name, '', TRUE);
     $messages = \Drupal::messenger()->all();
     \Drupal::messenger()->deleteAll();
-    $this->assertContains(strtr('For security reasons, your upload has been renamed to <em class="placeholder">%filename</em>.', ['%filename' => $munged_name]), $messages['status'], 'Alert properly set when a file is renamed.');
+    $this->assertContainsEquals(strtr('For security reasons, your upload has been renamed to <em class="placeholder">%filename</em>.', ['%filename' => $munged_name]), $messages['status'], 'Alert properly set when a file is renamed.');
     $this->assertNotEqual($munged_name, $this->name, new FormattableMarkup('The new filename (%munged) has been modified from the original (%original)', ['%munged' => $munged_name, '%original' => $this->name]));
   }
 
diff --git a/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php b/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php
index cbc62397b5..bc9dda8360 100644
--- a/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\KernelTests\Core\Image;
 
+use Drupal\Component\Utility\Bytes;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Image\ImageInterface;
 use Drupal\Component\Render\FormattableMarkup;
@@ -427,31 +428,59 @@ public function testManipulations() {
   }
 
   /**
-   * Tests that GD resources are freed from memory.
+   * Tests that GD images are freed from memory.
    */
-  public function testResourceDestruction() {
-    // Test that an Image object going out of scope releases its GD resource.
-    $image = $this->imageFactory->get('core/tests/fixtures/files/image-test.png');
+  public function testImageDestruction() {
+    // Build and save to disk a large image. When in memory, this image will
+    // require in excess of 150 MB allocated.
+    $directory = Settings::get('file_public_path') . '/imagetest';
+    \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
+    $test_image_path = $directory . '/' . 'large-image.png';
+    $image = $this->imageFactory->get('core/tests/fixtures/files/image-1.png');
+    $image->apply('resize', ['width' => 7200, 'height' => 4800]);
+    $image->save($test_image_path);
+    $image = NULL;
+
+    // Test that an Image object going out of scope releases its memory
+    // occupation.
+    $preload_memory_usage = memory_get_usage(TRUE);
+    $image = $this->imageFactory->get($test_image_path);
     $res = $image->getToolkit()->getResource();
-    $this->assertIsResource($res);
+    $postload_memory_usage = memory_get_usage(TRUE);
+    $this->assertGreaterThan($preload_memory_usage + Bytes::toNumber('100 MB'), $postload_memory_usage);
+    if (PHP_VERSION_ID < 80000) {
+      $this->assertIsResource($res);
+    }
     $image = NULL;
-    // @todo In https://www.drupal.org/node/3133236 convert this to
-    //   $this->assertIsNotResource($res).
-    $this->assertFalse(is_resource($res), 'Image resource was destroyed after losing scope.');
+    $postdestroy_memory_usage = memory_get_usage(TRUE);
+    $this->assertLessThan($preload_memory_usage + Bytes::toNumber('1 MB'), $postdestroy_memory_usage);
+    if (PHP_VERSION_ID < 80000) {
+      // @todo In https://www.drupal.org/node/3133236 convert this to
+      //   $this->assertIsNotResource($res).
+      $this->assertFalse(is_resource($res), 'Image resource was destroyed after losing scope.');
+    }
 
-    // Test that 'create_new' operation does not leave orphaned GD resources.
-    $image = $this->imageFactory->get('core/tests/fixtures/files/image-test.png');
+    // Test that 'create_new' operation does not leave orphaned memory
+    // allocation.
+    $image = $this->imageFactory->get($test_image_path);
     $old_res = $image->getToolkit()->getResource();
-    // Check if resource has been created successfully.
-    $this->assertIsResource($old_res);
+    $postload_memory_usage = memory_get_usage(TRUE);
+    if (PHP_VERSION_ID < 80000) {
+      // Check if resource has been created successfully.
+      $this->assertIsResource($old_res);
+    }
     $image->createNew(20, 20);
     $new_res = $image->getToolkit()->getResource();
-    // Check if the original resource has been destroyed.
-    // @todo In https://www.drupal.org/node/3133236 convert this to
-    //   $this->assertIsNotResource($old_res).
-    $this->assertFalse(is_resource($old_res));
-    // Check if a new resource has been created successfully.
-    $this->assertIsResource($new_res);
+    $postcreatenew_memory_usage = memory_get_usage(TRUE);
+    $this->assertLessThan($postload_memory_usage - Bytes::toNumber('100 MB'), $postcreatenew_memory_usage);
+    if (PHP_VERSION_ID < 80000) {
+      // Check if the original resource has been destroyed.
+      // @todo In https://www.drupal.org/node/3133236 convert this to
+      //   $this->assertIsNotResource($old_res).
+      $this->assertFalse(is_resource($old_res));
+      // Check if a new resource has been created successfully.
+      $this->assertIsResource($new_res);
+    }
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php
index ac305022ff..e244e05e04 100644
--- a/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php
@@ -49,7 +49,7 @@ public function testRemoveSingleMessage() {
 
     // Check we only have the second one.
     $this->assertCount(1, $this->messenger->messagesByType(MessengerInterface::TYPE_STATUS));
-    $this->assertContains('Second message with <em>markup!</em> (not removed).', $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS));
+    $this->assertContainsEquals('Second message with <em>markup!</em> (not removed).', $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS));
 
   }
 
diff --git a/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php b/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php
index 4a72a3dc42..bc8685db01 100644
--- a/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php
@@ -7,6 +7,7 @@
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
 use PHPUnit\Framework\Error\Notice;
+use PHPUnit\Framework\Error\Warning;
 use SebastianBergmann\Comparator\Factory;
 use SebastianBergmann\Comparator\ComparisonFailure;
 
@@ -108,7 +109,7 @@ public function dataSetProvider() {
         new FormattableMarkup('goldfinger', []),
         ['goldfinger'],
         FALSE,
-        Notice::class,
+        PHP_VERSION_ID >= 80000 ? Warning::class : Notice::class,
       ],
       'stdClass vs TranslatableMarkup' => [
         (object) ['goldfinger'],
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index ae72732df2..ba7c5712fa 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -19,7 +19,7 @@
 use Drupal\Tests\ConfigTestTrait;
 use Drupal\Tests\RandomGeneratorTrait;
 use Drupal\Tests\TestRequirementsTrait;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
 use PHPUnit\Framework\Exception;
 use PHPUnit\Framework\TestCase;
@@ -78,7 +78,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
   use RandomGeneratorTrait;
   use ConfigTestTrait;
   use TestRequirementsTrait;
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * {@inheritdoc}
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 0aac9ce747..448bb6a22b 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -16,7 +16,7 @@
 use Drupal\Tests\block\Traits\BlockCreationTrait;
 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 use Drupal\Tests\node\Traits\NodeCreationTrait;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
 use GuzzleHttp\Cookie\CookieJar;
@@ -64,7 +64,7 @@ abstract class BrowserTestBase extends TestCase {
     createUser as drupalCreateUser;
   }
   use XdebugRequestTrait;
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * The database prefix of this test run.
diff --git a/core/tests/Drupal/Tests/Component/Annotation/Plugin/Discovery/AnnotationBridgeDecoratorTest.php b/core/tests/Drupal/Tests/Component/Annotation/Plugin/Discovery/AnnotationBridgeDecoratorTest.php
index 12732e110a..288b85992b 100644
--- a/core/tests/Drupal/Tests/Component/Annotation/Plugin/Discovery/AnnotationBridgeDecoratorTest.php
+++ b/core/tests/Drupal/Tests/Component/Annotation/Plugin/Discovery/AnnotationBridgeDecoratorTest.php
@@ -6,6 +6,7 @@
 use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
 use Drupal\Component\Plugin\Definition\PluginDefinition;
 use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -14,6 +15,8 @@
  */
 class AnnotationBridgeDecoratorTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   /**
    * @covers ::getDefinitions
    */
diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
index f348b0705b..71facd301d 100644
--- a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
+++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Component\DependencyInjection;
 
 use Drupal\Component\Utility\Crypt;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -24,6 +25,8 @@
  */
 class ContainerTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   /**
    * The tested container.
    *
diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
index 92ee5490ac..c50b117407 100644
--- a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
+++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Component\DependencyInjection\Dumper {
 
   use Drupal\Component\Utility\Crypt;
+  use Drupal\Tests\Traits\PhpUnitWarnings;
   use PHPUnit\Framework\TestCase;
   use Symfony\Component\DependencyInjection\Definition;
   use Symfony\Component\DependencyInjection\Reference;
@@ -24,6 +25,8 @@
    */
   class OptimizedPhpArrayDumperTest extends TestCase {
 
+    use PhpUnitWarnings;
+
     /**
      * The container builder instance.
      *
diff --git a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php
index be2054811e..76f0064e7d 100644
--- a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php
+++ b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php
@@ -4,7 +4,6 @@
 
 use Drupal\Component\Diff\Engine\DiffOp;
 use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\Error\Error;
 
 /**
  * Test DiffOp base class.
@@ -25,7 +24,7 @@ class DiffOpTest extends TestCase {
    * @covers ::reverse
    */
   public function testReverse() {
-    $this->expectException(Error::class);
+    $this->expectError();
     $op = new DiffOp();
     $result = $op->reverse();
   }
diff --git a/core/tests/Drupal/Tests/Component/Gettext/PoStreamWriterTest.php b/core/tests/Drupal/Tests/Component/Gettext/PoStreamWriterTest.php
index 39cf95d18a..76be270813 100644
--- a/core/tests/Drupal/Tests/Component/Gettext/PoStreamWriterTest.php
+++ b/core/tests/Drupal/Tests/Component/Gettext/PoStreamWriterTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Gettext\PoItem;
 use Drupal\Component\Gettext\PoStreamWriter;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use org\bovigo\vfs\vfsStream;
 use org\bovigo\vfs\vfsStreamFile;
 use PHPUnit\Framework\TestCase;
@@ -14,6 +15,8 @@
  */
 class PoStreamWriterTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   /**
    * The PO writer object under test.
    *
diff --git a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php
index 6230bdbd7b..c17b1bb516 100644
--- a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php
+++ b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php
@@ -4,8 +4,8 @@
 
 use Drupal\Component\PhpStorage\FileStorage;
 use Drupal\Component\Utility\Random;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use org\bovigo\vfs\vfsStreamDirectory;
-use PHPUnit\Framework\Error\Warning;
 
 /**
  * @coversDefaultClass \Drupal\Component\PhpStorage\FileStorage
@@ -14,6 +14,8 @@
  */
 class FileStorageTest extends PhpStorageTestBase {
 
+  use PhpUnitWarnings;
+
   /**
    * Standard test settings to pass to storage instances.
    *
@@ -99,8 +101,8 @@ public function testCreateDirectoryFailWarning() {
       'bin' => 'test',
     ]);
     $code = "<?php\n echo 'here';";
-    $this->expectException(Warning::class);
-    $this->expectExceptionMessage('mkdir(): Permission Denied');
+    $this->expectWarning();
+    $this->expectWarningMessage('mkdir(): Permission Denied');
     $storage->save('subdirectory/foo.php', $code);
   }
 
diff --git a/core/tests/Drupal/Tests/Component/Plugin/PluginManagerBaseTest.php b/core/tests/Drupal/Tests/Component/Plugin/PluginManagerBaseTest.php
index 954ff70110..c305687825 100644
--- a/core/tests/Drupal/Tests/Component/Plugin/PluginManagerBaseTest.php
+++ b/core/tests/Drupal/Tests/Component/Plugin/PluginManagerBaseTest.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Component\Plugin\Mapper\MapperInterface;
 use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -13,6 +14,8 @@
  */
 class PluginManagerBaseTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   /**
    * A callback method for mocking FactoryInterface objects.
    */
diff --git a/core/tests/Drupal/Tests/Component/Utility/ReflectionTest.php b/core/tests/Drupal/Tests/Component/Utility/ReflectionTest.php
new file mode 100644
index 0000000000..7deb10328e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Utility/ReflectionTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\Component\Utility;
+
+use Drupal\Component\Utility\Reflection;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Utility\Reflection
+ * @group Utility
+ */
+class ReflectionTest extends TestCase {
+
+  /**
+   * @param string|null $expected
+   * @param \ReflectionParameter $parameter
+   *
+   * @covers ::getParameterClassName
+   * @dataProvider providerGetParameterClassName
+   */
+  public function testGetParameterClassName(?string $expected, \ReflectionParameter $parameter) {
+    $this->assertEquals($expected, Reflection::getParameterClassName($parameter));
+  }
+
+  /**
+   * Data provider for ::testGetParameterClassName().
+   *
+   * @return array[]
+   */
+  public function providerGetParameterClassName() {
+    $reflection_method = new \ReflectionMethod(static::class, 'existsForTesting');
+    $parameters = $reflection_method->getParameters();
+    return [
+      'string' => [NULL, $parameters[0]],
+      'array' => [NULL, $parameters[1]],
+      'same class' => ['Drupal\Tests\Component\Utility\ReflectionTest', $parameters[2]],
+      'class' => ['Drupal\Component\Utility\Reflection', $parameters[3]],
+      'parent' => ['PHPUnit\Framework\TestCase', $parameters[4]],
+      'self' => ['Drupal\Tests\Component\Utility\ReflectionTest', $parameters[5]],
+    ];
+  }
+
+  /**
+   * This method exists for reflection testing only.
+   *
+   * Note the capital P in Parent is intentional and for testing purposes.
+   */
+  // phpcs:disable Generic.PHP.LowerCaseKeyword.Found
+  protected function existsForTesting(string $string, array $array, ReflectionTest $test, Reflection $reflection, Parent $parent, self $self) {
+  }
+
+  // phpcs:enable
+
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/AssertUtilsTrait.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/AssertUtilsTrait.php
index 96dab84912..ecbd4f83c1 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/AssertUtilsTrait.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/AssertUtilsTrait.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Tests\Composer\Plugin\Scaffold;
 
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 
 /**
  * Convenience class for creating fixtures.
  */
 trait AssertUtilsTrait {
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * Asserts that a given file exists and is/is not a symlink.
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php
index e37d06f1f5..ff189a3788 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/AppendOpTest.php
@@ -5,7 +5,7 @@
 use Drupal\Composer\Plugin\Scaffold\Operations\AppendOp;
 use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
 use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -14,7 +14,7 @@
  * @group Scaffold
  */
 class AppendOpTest extends TestCase {
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * @covers ::process
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/ReplaceOpTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/ReplaceOpTest.php
index cb5d6c7c4b..65de3c77c0 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/ReplaceOpTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/ReplaceOpTest.php
@@ -5,7 +5,7 @@
 use Drupal\Composer\Plugin\Scaffold\Operations\ReplaceOp;
 use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
 use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -14,7 +14,7 @@
  * @group Scaffold
  */
 class ReplaceOpTest extends TestCase {
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * @covers ::process
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/SkipOpTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/SkipOpTest.php
index ba8313bbce..f2cb6cbd6e 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/SkipOpTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Integration/SkipOpTest.php
@@ -5,7 +5,7 @@
 use Drupal\Composer\Plugin\Scaffold\Operations\SkipOp;
 use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
 use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -14,7 +14,7 @@
  * @group Scaffold
  */
 class SkipOpTest extends TestCase {
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * @covers ::process
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/ConfigTest.php b/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/ConfigTest.php
index 9755278b51..f800cdd526 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/ConfigTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/ConfigTest.php
@@ -4,6 +4,7 @@
 
 use Composer\Package\RootPackageInterface;
 use Drupal\Composer\Plugin\VendorHardening\Config;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -12,6 +13,8 @@
  */
 class ConfigTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   /**
    * @covers ::getPathsForPackage
    */
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php b/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php
index 845356f887..79e136d8c3 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/VendorHardening/VendorHardeningPluginTest.php
@@ -8,6 +8,7 @@
 use Composer\Package\RootPackageInterface;
 use Drupal\Composer\Plugin\VendorHardening\Config;
 use Drupal\Composer\Plugin\VendorHardening\VendorHardeningPlugin;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use org\bovigo\vfs\vfsStream;
 use PHPUnit\Framework\TestCase;
 
@@ -17,6 +18,8 @@
  */
 class VendorHardeningPluginTest extends TestCase {
 
+  use PhpUnitWarnings;
+
   public function setUp(): void {
     parent::setUp();
     vfsStream::setup('vendor', NULL, [
diff --git a/core/tests/Drupal/Tests/ComposerIntegrationTest.php b/core/tests/Drupal/Tests/ComposerIntegrationTest.php
index 5cd2719beb..d74cf42027 100644
--- a/core/tests/Drupal/Tests/ComposerIntegrationTest.php
+++ b/core/tests/Drupal/Tests/ComposerIntegrationTest.php
@@ -5,6 +5,7 @@
 use Drupal\Composer\Plugin\VendorHardening\Config;
 use Drupal\Core\Composer\Composer;
 use Drupal\Tests\Composer\ComposerIntegrationTrait;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 use Symfony\Component\Yaml\Yaml;
 
 /**
@@ -260,6 +261,11 @@ public function testVendorCleanup($class, $property) {
     $reflection = new \ReflectionProperty($class, $property);
     $reflection->setAccessible(TRUE);
     $config = $reflection->getValue();
+    // PHPUnit 9.5.3 removes 'phpunit/php-token-stream' from its dependencies.
+    // @todo remove the check below when PHPUnit 9 is the minimum.
+    if (RunnerVersion::getMajor() >= 9) {
+      unset($config['phpunit/php-token-stream']);
+    }
     foreach (array_keys($config) as $package) {
       $this->assertContains(strtolower($package), $packages);
     }
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php
index 7e5fe84836..40efadf4ea 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php
@@ -113,8 +113,8 @@ public function testGrouper() {
     $this->assertSame('all', $group['media']);
     $this->assertTrue($group['preprocess']);
     $this->assertCount(3, $group['items']);
-    $this->assertContains($css_assets['system.base.css'], $group['items']);
-    $this->assertContains($css_assets['js.module.css'], $group['items']);
+    $this->assertContainsEquals($css_assets['system.base.css'], $group['items']);
+    $this->assertContainsEquals($css_assets['js.module.css'], $group['items']);
 
     // Check group 2.
     $group = $groups[1];
@@ -123,7 +123,7 @@ public function testGrouper() {
     $this->assertSame('all', $group['media']);
     $this->assertTrue($group['preprocess']);
     $this->assertCount(1, $group['items']);
-    $this->assertContains($css_assets['field.css'], $group['items']);
+    $this->assertContainsEquals($css_assets['field.css'], $group['items']);
 
     // Check group 3.
     $group = $groups[2];
@@ -132,7 +132,7 @@ public function testGrouper() {
     $this->assertSame('all', $group['media']);
     $this->assertTrue($group['preprocess']);
     $this->assertCount(1, $group['items']);
-    $this->assertContains($css_assets['external.css'], $group['items']);
+    $this->assertContainsEquals($css_assets['external.css'], $group['items']);
 
     // Check group 4.
     $group = $groups[3];
@@ -141,7 +141,7 @@ public function testGrouper() {
     $this->assertSame('all', $group['media']);
     $this->assertTrue($group['preprocess']);
     $this->assertCount(1, $group['items']);
-    $this->assertContains($css_assets['elements.css'], $group['items']);
+    $this->assertContainsEquals($css_assets['elements.css'], $group['items']);
 
     // Check group 5.
     $group = $groups[4];
@@ -150,7 +150,7 @@ public function testGrouper() {
     $this->assertSame('print', $group['media']);
     $this->assertTrue($group['preprocess']);
     $this->assertCount(1, $group['items']);
-    $this->assertContains($css_assets['print.css'], $group['items']);
+    $this->assertContainsEquals($css_assets['print.css'], $group['items']);
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
index e4bed4e3a4..dcb3b0f196 100644
--- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
@@ -103,13 +103,13 @@ public function testGetLibraryByName() {
    * Tests getting a deprecated library.
    */
   public function testAssetLibraryDeprecation() {
-    $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line, $context) use (&$previous_error_handler) {
+    $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
       // Convert deprecation error into a catchable exception.
       if ($severity === E_USER_DEPRECATED) {
         throw new \ErrorException($message, 0, $severity, $file, $line);
       }
       if ($previous_error_handler) {
-        return $previous_error_handler($severity, $message, $file, $line, $context);
+        return $previous_error_handler($severity, $message, $file, $line);
       }
     });
 
diff --git a/core/tests/Drupal/Tests/Core/Config/ConfigTest.php b/core/tests/Drupal/Tests/Core/Config/ConfigTest.php
index ac09beb7a6..0ab90fe518 100644
--- a/core/tests/Drupal/Tests/Core/Config/ConfigTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/ConfigTest.php
@@ -270,7 +270,7 @@ public function testSetIllegalOffsetValue() {
     $this->config->set('testData', 1);
 
     // Attempt to treat the single value as a nested item.
-    $this->expectException(Warning::class);
+    $this->expectException(PHP_VERSION_ID >= 80000 ? \Error::class : Warning::class);
     $this->config->set('testData.illegalOffset', 1);
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
index c5cdabdf12..70a1cfb913 100644
--- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\Tests\Core\Database;
 
 use Composer\Autoload\ClassLoader;
-use Drupal\Core\Database\Statement;
+use Drupal\Core\Database\StatementEmpty;
 use Drupal\Tests\Core\Database\Stub\StubConnection;
 use Drupal\Tests\Core\Database\Stub\StubPDO;
 use Drupal\Tests\Core\Database\Stub\Driver;
@@ -613,16 +613,13 @@ public function testQueryTrim($expected, $query, $options) {
     $mock_pdo = $this->getMockBuilder(StubPdo::class)
       ->setMethods(['execute', 'prepare', 'setAttribute'])
       ->getMock();
-    $mock_statement = $this->getMockBuilder(Statement::class)
-      ->disableOriginalConstructor()
-      ->getMock();
 
     // Ensure that PDO::prepare() is called only once, and with the
     // correctly trimmed query string.
     $mock_pdo->expects($this->once())
       ->method('prepare')
       ->with($expected)
-      ->willReturn($mock_statement);
+      ->willReturn(new StatementEmpty());
     $connection = new StubConnection($mock_pdo, []);
     $connection->query($query, [], $options);
   }
diff --git a/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php b/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php
index 297628262c..02ad9061b3 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Enhancer/EntityRouteEnhancerTest.php
@@ -29,7 +29,9 @@ public function testEnhancer() {
     $defaults['_entity_form'] = 'entity_test.default';
     $defaults['_route_object'] = (new Route('/test', $defaults));
     $new_defaults = $route_enhancer->enhance($defaults, $request);
-    $this->assertIsCallable($new_defaults['_controller']);
+    // @todo I don't think this assertion is important. Also it breaks in PHP 8
+    //    due to https://3v4l.org/21CAr.
+    // $this->assertIsCallable($new_defaults['_controller']);
     $this->assertEquals($defaults['_controller'], $new_defaults['_controller'], '_controller did not get overridden.');
 
     // Set _entity_form and ensure that the form is set.
diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
index 250b184df6..50df652c04 100644
--- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
+++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
@@ -133,6 +133,18 @@ public static function getSkippedDeprecations() {
       'AssertLegacyTrait::assertFieldByXPath() is deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use $this->xpath() instead and check the values directly in the test. See https://www.drupal.org/node/3129738',
       'AssertLegacyTrait::assertNoFieldByXPath() is deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use $this->xpath() instead and assert that the result is empty. See https://www.drupal.org/node/3129738',
       'AssertLegacyTrait::assertFieldsByValue() is deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use iteration over the fields yourself instead and directly check the values in the test. See https://www.drupal.org/node/3129738',
+      // PHPUnit 9.
+      "The \"PHPUnit\TextUI\DefaultResultPrinter\" class is considered internal This class is not covered by the backward compatibility promise for PHPUnit. It may change without further notice. You should not use it from \"Drupal\Tests\Listeners\HtmlOutputPrinter\".",
+      'assertFileNotExists() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertFileDoesNotExist() instead.',
+      'PHPUnit\\Framework\\TestCase::prophesize() is deprecated and will be removed in PHPUnit 10. Please use the trait provided by phpspec/prophecy-phpunit.',
+      'assertRegExp() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertMatchesRegularExpression() instead.',
+      'assertNotRegExp() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDoesNotMatchRegularExpression() instead.',
+      'assertDirectoryNotExists() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDirectoryDoesNotExist() instead.',
+      'Support for using expectException() with PHPUnit\\Framework\\Error\\Warning is deprecated and will be removed in PHPUnit 10. Use expectWarning() instead.',
+      'Support for using expectException() with PHPUnit\\Framework\\Error\\Error is deprecated and will be removed in PHPUnit 10. Use expectError() instead.',
+      'assertDirectoryNotIsWritable() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDirectoryIsNotWritable() instead.',
+      'assertFileNotIsWritable() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertFileIsNotWritable() instead.',
+      'The at() matcher has been deprecated. It will be removed in PHPUnit 10. Please refactor your test to not rely on the order in which methods are invoked.',
     ];
   }
 
diff --git a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
index a3f7916e98..a85c320499 100644
--- a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
+++ b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
@@ -2,15 +2,27 @@
 
 namespace Drupal\Tests\Listeners;
 
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 use PHPUnit\Framework\TestResult;
-use PHPUnit\TextUI\ResultPrinter;
+
+// In order to manage different implementations across PHPUnit versions, we
+// dynamically load the base ResultPrinter class dependent on the PHPUnit runner
+// version.
+if (!class_exists(ResultPrinterBase::class, FALSE)) {
+  if (RunnerVersion::getMajor() < 9) {
+    class_alias('PHPUnit\TextUI\ResultPrinter', ResultPrinterBase::class);
+  }
+  else {
+    class_alias('PHPUnit\TextUI\DefaultResultPrinter', ResultPrinterBase::class);
+  }
+}
 
 /**
  * Defines a class for providing html output results for functional tests.
  *
  * @internal
  */
-class HtmlOutputPrinter extends ResultPrinter {
+class HtmlOutputPrinter extends ResultPrinterBase {
 
   use HtmlOutputPrinterTrait;
 
diff --git a/core/tests/Drupal/Tests/PhpUnitWarningsTest.php b/core/tests/Drupal/Tests/PhpUnitWarningsTest.php
new file mode 100644
index 0000000000..0cf183fb98
--- /dev/null
+++ b/core/tests/Drupal/Tests/PhpUnitWarningsTest.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\Tests;
+
+/**
+ * @coversDefaultClass \Drupal\Tests\Traits\PhpUnitWarnings
+ * @group legacy
+ */
+class PhpUnitWarningsTest extends UnitTestCase {
+
+  /**
+   * @expectedDeprecation Test warning for \Drupal\Tests\PhpUnitWarningsTest::testAddWarning()
+   */
+  public function testAddWarning() {
+    $this->addWarning('Test warning for \Drupal\Tests\PhpUnitWarningsTest::testAddWarning()');
+  }
+
+  /**
+   * @expectedDeprecation Using assertContains() with string haystacks is deprecated and will not be supported in PHPUnit 9. Refactor your test to use assertStringContainsString() or assertStringContainsStringIgnoringCase() instead.
+   * @expectedDeprecation The optional $ignoreCase parameter of assertContains() is deprecated and will be removed in PHPUnit 9.
+   */
+  public function testAssertContains() {
+    $this->assertContains('string', 'aaaastringaaa');
+    $this->assertContains('STRING', 'aaaastringaaa', '', TRUE);
+  }
+
+  /**
+   * @expectedDeprecation Using assertNotContains() with string haystacks is deprecated and will not be supported in PHPUnit 9. Refactor your test to use assertStringNotContainsString() or assertStringNotContainsStringIgnoringCase() instead.
+   * @expectedDeprecation The optional $ignoreCase parameter of assertNotContains() is deprecated and will be removed in PHPUnit 9.
+   */
+  public function testAssertNotContains() {
+    $this->assertNotContains('foo', 'bar');
+    $this->assertNotContains('FOO', 'bar', '', TRUE);
+  }
+
+  /**
+   * @expectedDeprecation assertArraySubset() is deprecated and will be removed in PHPUnit 9.
+   */
+  public function testAssertArraySubset() {
+    $this->assertArraySubset(['a'], ['a', 'b']);
+  }
+
+  /**
+   * @expectedDeprecation assertInternalType() is deprecated and will be removed in PHPUnit 9. Refactor your test to use assertIsArray(), assertIsBool(), assertIsFloat(), assertIsInt(), assertIsNumeric(), assertIsObject(), assertIsResource(), assertIsString(), assertIsScalar(), assertIsCallable(), or assertIsIterable() instead.
+   */
+  public function testAssertInternalType() {
+    $this->assertInternalType('string', 'string');
+  }
+
+  /**
+   * @expectedDeprecation assertAttributeEquals() is deprecated and will be removed in PHPUnit 9.
+   * @expectedDeprecation readAttribute() is deprecated and will be removed in PHPUnit 9.
+   * @expectedDeprecation getObjectAttribute() is deprecated and will be removed in PHPUnit 9.
+   * @expectedDeprecation assertAttributeSame() is deprecated and will be removed in PHPUnit 9.
+   * @expectedDeprecation assertAttributeInstanceOf() is deprecated and will be removed in PHPUnit 9.
+   * @expectedDeprecation assertAttributeEmpty() is deprecated and will be removed in PHPUnit 9.
+   */
+  public function testAssertAttribute() {
+    $obj = new class() {
+      protected $attribute = 'value';
+      protected $class;
+      protected $empty;
+
+      public function __construct() {
+        $this->class = new \stdClass();
+      }
+
+    };
+    $this->assertAttributeEquals('value', 'attribute', $obj);
+    $this->assertAttributeSame('value', 'attribute', $obj);
+    $this->assertAttributeInstanceOf(\stdClass::class, 'class', $obj);
+    $this->assertAttributeEmpty('empty', $obj);
+  }
+
+  /**
+   * @expectedDeprecation The optional $canonicalize parameter of assertEquals() is deprecated and will be removed in PHPUnit 9. Refactor your test to use assertEqualsCanonicalizing() instead.
+   */
+  public function testAssertEquals() {
+    $this->assertEquals(['a', 'b'], ['b', 'a'], '', 0.0, 10, TRUE);
+  }
+
+  /**
+   * @expectedDeprecation expectExceptionMessageRegExp() is deprecated in PHPUnit 8 and will be removed in PHPUnit 9.
+   */
+  public function testExpectExceptionMessageRegExp() {
+    $this->expectException(\Exception::class);
+    $this->expectExceptionMessageRegExp('/An exception .*/');
+    throw new \Exception('An exception has been triggered');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Traits/PHPUnit8Warnings.php b/core/tests/Drupal/Tests/Traits/PHPUnit8Warnings.php
deleted file mode 100644
index 5b80264681..0000000000
--- a/core/tests/Drupal/Tests/Traits/PHPUnit8Warnings.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-namespace Drupal\Tests\Traits;
-
-/**
- * Used to ignore warnings being added by PHPUnit 8.
- *
- * This trait exists to allow Drupal 8 tests using PHPUnit 7 and Drupal 9 tests
- * using PHPUnit 8 to happily co-exist. Once Drupal 8 and Drupal 9 are not so
- * closely aligned these will be fixed in core and the warnings will be emitted
- * from the test runner.
- *
- * @todo https://www.drupal.org/project/drupal/issues/3110543 Remove the ignored
- *   warnings to support PHPUnit 9.
- *
- * @internal
- */
-trait PHPUnit8Warnings {
-
-  /**
-   * The list of warnings to ignore.
-   *
-   * @var string[]
-   */
-  private static $ignoredWarnings = [
-    'expectExceptionMessageRegExp() is deprecated in PHPUnit 8 and will be removed in PHPUnit 9.',
-  ];
-
-  /**
-   * Ignores specific PHPUnit 8 warnings.
-   *
-   * @see \PHPUnit\Framework\TestCase::addWarning()
-   *
-   * @internal
-   */
-  public function addWarning(string $warning): void {
-    if (in_array($warning, self::$ignoredWarnings, TRUE)) {
-      return;
-    }
-    parent::addWarning($warning);
-  }
-
-}
diff --git a/core/tests/Drupal/Tests/Traits/PhpUnitWarnings.php b/core/tests/Drupal/Tests/Traits/PhpUnitWarnings.php
new file mode 100644
index 0000000000..c24f1210ae
--- /dev/null
+++ b/core/tests/Drupal/Tests/Traits/PhpUnitWarnings.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\Tests\Traits;
+
+/**
+ * Converts deprecation warnings added by PHPUnit to silenced deprecations.
+ *
+ * This trait exists to allow Drupal to run tests with multiple versions of
+ * PHPUnit without failing due to PHPUnit's deprecation warnings.
+ *
+ * @internal
+ */
+trait PhpUnitWarnings {
+
+  /**
+   * Deprecation warnings from PHPUnit to raise with @trigger_error().
+   *
+   * Add any PHPUnit deprecations that should be handled as deprecation warnings
+   * (rather than unconditional failures) for core and contrib.
+   *
+   * @var string[]
+   */
+  private static $deprecationWarnings = [
+    'Using assertContains() with string haystacks is deprecated and will not be supported in PHPUnit 9. Refactor your test to use assertStringContainsString() or assertStringContainsStringIgnoringCase() instead.',
+    'Using assertNotContains() with string haystacks is deprecated and will not be supported in PHPUnit 9. Refactor your test to use assertStringNotContainsString() or assertStringNotContainsStringIgnoringCase() instead.',
+    'assertArraySubset() is deprecated and will be removed in PHPUnit 9.',
+    'assertInternalType() is deprecated and will be removed in PHPUnit 9. Refactor your test to use assertIsArray(), assertIsBool(), assertIsFloat(), assertIsInt(), assertIsNumeric(), assertIsObject(), assertIsResource(), assertIsString(), assertIsScalar(), assertIsCallable(), or assertIsIterable() instead.',
+    'readAttribute() is deprecated and will be removed in PHPUnit 9.',
+    'getObjectAttribute() is deprecated and will be removed in PHPUnit 9.',
+    'The optional $canonicalize parameter of assertEquals() is deprecated and will be removed in PHPUnit 9. Refactor your test to use assertEqualsCanonicalizing() instead.',
+    'assertAttributeEquals() is deprecated and will be removed in PHPUnit 9.',
+    'assertAttributeSame() is deprecated and will be removed in PHPUnit 9.',
+    'assertAttributeInstanceOf() is deprecated and will be removed in PHPUnit 9.',
+    'assertAttributeEmpty() is deprecated and will be removed in PHPUnit 9.',
+    'The optional $ignoreCase parameter of assertContains() is deprecated and will be removed in PHPUnit 9.',
+    'The optional $ignoreCase parameter of assertNotContains() is deprecated and will be removed in PHPUnit 9.',
+    'expectExceptionMessageRegExp() is deprecated in PHPUnit 8 and will be removed in PHPUnit 9.',
+    // Warning for testing.
+    'Test warning for \Drupal\Tests\PhpUnitWarningsTest::testAddWarning()',
+    // PHPUnit 9.
+    'assertFileNotExists() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertFileDoesNotExist() instead.',
+    'PHPUnit\\Framework\\TestCase::prophesize() is deprecated and will be removed in PHPUnit 10. Please use the trait provided by phpspec/prophecy-phpunit.',
+    'assertRegExp() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertMatchesRegularExpression() instead.',
+    'assertNotRegExp() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDoesNotMatchRegularExpression() instead.',
+    'assertDirectoryNotExists() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDirectoryDoesNotExist() instead.',
+    'Support for using expectException() with PHPUnit\\Framework\\Error\\Warning is deprecated and will be removed in PHPUnit 10. Use expectWarning() instead.',
+    'Support for using expectException() with PHPUnit\\Framework\\Error\\Error is deprecated and will be removed in PHPUnit 10. Use expectError() instead.',
+    'assertDirectoryNotIsWritable() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDirectoryIsNotWritable() instead.',
+    'assertFileNotIsWritable() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertFileIsNotWritable() instead.',
+    'The at() matcher has been deprecated. It will be removed in PHPUnit 10. Please refactor your test to not rely on the order in which methods are invoked.',
+  ];
+
+  /**
+   * Converts PHPUnit deprecation warnings to E_USER_DEPRECATED.
+   *
+   * @param string $warning
+   *   The warning message raised in tests.
+   *
+   * @see \PHPUnit\Framework\TestCase::addWarning()
+   *
+   * @internal
+   */
+  public function addWarning(string $warning): void {
+    if (in_array($warning, self::$deprecationWarnings, TRUE)) {
+      // Convert listed PHPUnit deprecations into E_USER_DEPRECATED and prevent
+      // each from being raised as a test warning.
+      @trigger_error($warning, E_USER_DEPRECATED);
+      return;
+    }
+
+    // Otherwise, let the parent raise any warning not specifically listed.
+    parent::addWarning($warning);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index 2c2257b2da..55978ba413 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -9,7 +9,7 @@
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
-use Drupal\Tests\Traits\PHPUnit8Warnings;
+use Drupal\Tests\Traits\PhpUnitWarnings;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -19,7 +19,7 @@
  */
 abstract class UnitTestCase extends TestCase {
 
-  use PHPUnit8Warnings;
+  use PhpUnitWarnings;
 
   /**
    * The random generator.
