Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\DeprecationBridge;
use PHPUnit\Framework\TestCase;
/**
* Drupal's PHPUnit extension to manage code deprecation.
*
* This class is a replacement for symfony/phpunit-bridge that does not
* support PHPUnit 10. In the future this extension might be dropped if
* PHPUnit adds support for all deprecation management needs.
*
* @internal
*/
final class DeprecationHandler {
/**
* Indicates if the extension is enabled.
*/
private static bool $enabled = FALSE;
/**
* A list of deprecation messages that should be ignored if detected.
*
* @var list<string>
*/
private static array $deprecationIgnorePatterns = [];
/**
* A list of expected deprecation messages.
*
* @var list<string>
*/
private static array $expectedDeprecations = [];
/**
* A list of deprecation messages collected during test run.
*
* @var list<string>
*/
private static array $collectedDeprecations = [];
/**
* This class should not be instantiated.
*/
private function __construct() {
throw new \LogicException(__CLASS__ . ' should not be instantiated');
}
/**
* Returns the extension configuration.
*
* For historical reasons, the configuration is stored in the
* SYMFONY_DEPRECATIONS_HELPER environment variable.
*
* @return array|false
* An array of configuration variables, of FALSE if the extension is
* disabled.
*/
public static function getConfiguration(): array|FALSE {
$environmentVariable = getenv('SYMFONY_DEPRECATIONS_HELPER');
if ($environmentVariable === 'disabled') {
return FALSE;
}
if ($environmentVariable === FALSE) {
// Ensure ignored deprecation patterns listed in .deprecation-ignore.txt
// are considered in testing.
$relativeFilePath = __DIR__ . "/../../../../../.deprecation-ignore.txt";
$deprecationIgnoreFilename = realpath($relativeFilePath);
if (empty($deprecationIgnoreFilename)) {
throw new \InvalidArgumentException(sprintf('The ignoreFile "%s" does not exist.', $relativeFilePath));
}
$environmentVariable = "ignoreFile=$deprecationIgnoreFilename";
}
parse_str($environmentVariable, $configuration);
$environmentVariable = getenv('PHPUNIT_FAIL_ON_PHPUNIT_DEPRECATION');
$phpUnitDeprecationVariable = $environmentVariable !== FALSE ? $environmentVariable : TRUE;
$configuration['failOnPhpunitDeprecation'] = filter_var($phpUnitDeprecationVariable, \FILTER_VALIDATE_BOOLEAN);
return $configuration;
}
/**
* Determines if the extension is enabled.
*
* @return bool
* TRUE if enabled, FALSE if disabled.
*/
public static function isEnabled(): bool {
return self::$enabled;
}
/**
* Initializes the extension.
*
* @param string|null $ignoreFile
* The path to a file containing ignore patterns for deprecations.
*/
public static function init(?string $ignoreFile = NULL): void {
if (self::isEnabled()) {
throw new \LogicException(__CLASS__ . ' is already initialized');
}
// Load the deprecation ignore patterns from the specified file.
if ($ignoreFile && !self::$deprecationIgnorePatterns) {
if (!is_file($ignoreFile)) {
throw new \InvalidArgumentException(sprintf('The ignoreFile "%s" does not exist.', $ignoreFile));
}
set_error_handler(static function ($t, $m) use ($ignoreFile, &$line) {
throw new \RuntimeException(sprintf('Invalid pattern found in "%s" on line "%d"', $ignoreFile, 1 + $line) . substr($m, 12));
});
try {
foreach (file($ignoreFile) as $line => $pattern) {
if ((trim($pattern)[0] ?? '#') !== '#') {
preg_match($pattern, '');
self::$deprecationIgnorePatterns[] = $pattern;
}
}
}
finally {
restore_error_handler();
}
}
// Mark the extension as enabled.
self::$enabled = TRUE;
}
/**
* Resets the extension.
*
* The extension should be reset at the beginning of each test run to ensure
* matching of expected and actual deprecations.
*/
public static function reset(): void {
if (!self::isEnabled()) {
return;
}
self::$expectedDeprecations = [];
self::$collectedDeprecations = [];
}
/**
* Adds an expected deprecation.
*
* Tests will expect deprecations during the test execution; at the end of
* each test run, collected deprecations are checked against the expected
* ones.
*
* @param string $message
* The expected deprecation message.
*/
public static function expectDeprecation(string $message): void {
if (!self::isEnabled()) {
return;
}
self::$expectedDeprecations[] = $message;
}
/**
* Returns all expected deprecations.
*
* @return list<string>
* The expected deprecation messages.
*/
public static function getExpectedDeprecations(): array {
if (!self::isEnabled()) {
throw new \LogicException(__CLASS__ . ' is not initialized');
}
return self::$expectedDeprecations;
}
/**
* Collects an actual deprecation.
*
* Tests will expect deprecations during the test execution; at the end of
* each test run, collected deprecations are checked against the expected
* ones.
*
* @param string $message
* The actual deprecation message triggered via trigger_error().
*/
public static function collectActualDeprecation(string $message): void {
if (!self::isEnabled()) {
return;
}
self::$collectedDeprecations[] = $message;
}
/**
* Returns all collected deprecations.
*
* @return list<string>
* The collected deprecation messages.
*/
public static function getCollectedDeprecations(): array {
if (!self::isEnabled()) {
throw new \LogicException(__CLASS__ . ' is not initialized');
}
return self::$collectedDeprecations;
}
/**
* Determines if an actual deprecation should be ignored.
*
* Deprecations that match the patterns included in the ignore file should
* be ignored.
*
* @param string $deprecationMessage
* The actual deprecation message triggered via trigger_error().
*/
public static function isIgnoredDeprecation(string $deprecationMessage): bool {
if (!self::$deprecationIgnorePatterns) {
return FALSE;
}
$result = @preg_filter(self::$deprecationIgnorePatterns, '$0', $deprecationMessage);
if (preg_last_error() !== \PREG_NO_ERROR) {
throw new \RuntimeException(preg_last_error_msg());
}
return (bool) $result;
}
/**
* Determines if a test case is a deprecation test.
*
* Deprecation tests are those that are annotated with '@group legacy' or
* that have a '#[IgnoreDeprecations]' attribute.
*
* @param \PHPUnit\Framework\TestCase $testCase
* The test case being executed.
*/
public static function isDeprecationTest(TestCase $testCase): bool {
return $testCase->valueObjectForEvents()->metadata()->isIgnoreDeprecations()->isNotEmpty() || self::isTestInLegacyGroup($testCase);
}
/**
* Determines if a test case is part of the 'legacy' group.
*
* @param \PHPUnit\Framework\TestCase $testCase
* The test case being executed.
*/
private static function isTestInLegacyGroup(TestCase $testCase): bool {
$groups = [];
foreach ($testCase->valueObjectForEvents()->metadata()->isGroup() as $metadata) {
$groups[] = $metadata->groupName();
}
return in_array('legacy', $groups, TRUE);
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\DeprecationBridge;
use Drupal\Core\Utility\Error;
use Drupal\TestTools\ErrorHandler\TestErrorHandler;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
/**
* A trait to include in Drupal tests to manage expected deprecations.
*
* This code works in coordination with DeprecationHandler.
*
* This trait is a replacement for symfony/phpunit-bridge that is not
* supporting PHPUnit 10. In the future this extension might be dropped if
* PHPUnit will support all deprecation management needs.
*
* @see \Drupal\TestTools\Extension\DeprecationBridge\DeprecationHandler
*
* @internal
*/
trait ExpectDeprecationTrait {
/**
* Sets up the test error handler.
*
* This method is run before each test's ::setUp() method, and when the
* DeprecationHandler is active, resets the extension to be able to collect
* the test's deprecations, and sets TestErrorHandler as the current error
* handler.
*
* @see \Drupal\TestTools\ErrorHandler\TestErrorHandler
*/
#[Before]
public function setUpErrorHandler(): void {
if (!DeprecationHandler::isEnabled()) {
return;
}
DeprecationHandler::reset();
set_error_handler(new TestErrorHandler(Error::currentErrorHandler(), $this));
}
/**
* Tears down the test error handler.
*
* This method is run after each test's ::tearDown() method, and checks if
* collected deprecations match the expectations; it also resets the error
* handler to the one set prior of the change made by ::setUpErrorHandler().
*/
#[After]
public function tearDownErrorHandler(): void {
if (!DeprecationHandler::isEnabled()) {
return;
}
// We expect that the current error handler is the one set by
// ::setUpErrorHandler() prior to the start of the test execution. If not,
// the error handler was changed during the test execution but not properly
// restored during ::tearDown().
$handler = Error::currentErrorHandler();
if (!$handler instanceof TestErrorHandler) {
throw new \RuntimeException(sprintf('%s registered its own error handler without restoring the previous one before or during tear down. This can cause unpredictable test results. Ensure the test cleans up after itself.', $this->name()));
}
restore_error_handler();
// Checks if collected deprecations match the expectations.
if (DeprecationHandler::getExpectedDeprecations()) {
$prefix = "@expectedDeprecation:\n";
$expDep = $prefix . '%A ' . implode("\n%A ", DeprecationHandler::getExpectedDeprecations()) . "\n%A";
$actDep = $prefix . ' ' . implode("\n ", DeprecationHandler::getCollectedDeprecations()) . "\n";
$this->assertStringMatchesFormat($expDep, $actDep);
}
}
/**
* Adds an expected deprecation.
*
* @param string $message
* The expected deprecation message.
*/
public function expectDeprecation(string $message): void {
if (!DeprecationHandler::isDeprecationTest($this)) {
throw new \RuntimeException('expectDeprecation() can only be called from tests marked with #[IgnoreDeprecations] or \'@group legacy\'');
}
if (!DeprecationHandler::isEnabled()) {
return;
}
DeprecationHandler::expectDeprecation($message);
}
}

View File

@ -0,0 +1,244 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\Dump;
use PHPUnit\Event\TestRunner\Finished as TestRunnerFinished;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
/**
* Drupal's extension for printing dump() output results.
*
* @internal
*/
final class DebugDump implements Extension {
/**
* The path to the dump staging file.
*/
private static string $stagingFilePath;
/**
* Whether colors should be used for printing.
*/
private static bool $colors = FALSE;
/**
* Whether the caller of dump should be included in the report.
*/
private static bool $printCaller = FALSE;
/**
* {@inheritdoc}
*/
public function bootstrap(
Configuration $configuration,
Facade $facade,
ParameterCollection $parameters,
): void {
// Determine staging file path.
self::$stagingFilePath = tempnam(sys_get_temp_dir(), 'dpd');
// Determine color output.
$colors = $parameters->has('colors') ? $parameters->get('colors') : FALSE;
self::$colors = filter_var($colors, \FILTER_VALIDATE_BOOLEAN);
// Print caller.
$printCaller = $parameters->has('printCaller') ? $parameters->get('printCaller') : FALSE;
self::$printCaller = filter_var($printCaller, \FILTER_VALIDATE_BOOLEAN);
// Set the environment variable with the configuration.
$config = json_encode([
'stagingFilePath' => self::$stagingFilePath,
'colors' => self::$colors,
'printCaller' => self::$printCaller,
]);
putenv('DRUPAL_PHPUNIT_DUMPER_CONFIG=' . $config);
$facade->registerSubscriber(new TestRunnerFinishedSubscriber($this));
}
/**
* Determines if the extension is enabled.
*
* @return bool
* TRUE if enabled, FALSE if disabled.
*/
public static function isEnabled(): bool {
return getenv('DRUPAL_PHPUNIT_DUMPER_CONFIG') !== FALSE;
}
/**
* A CLI handler for \Symfony\Component\VarDumper\VarDumper.
*
* @param mixed $var
* The variable to be dumped.
*/
public static function cliHandler(mixed $var): void {
if (!self::isEnabled()) {
return;
}
$config = (array) json_decode(getenv('DRUPAL_PHPUNIT_DUMPER_CONFIG'));
$caller = self::getCaller();
$cloner = new VarCloner();
$dumper = new CliDumper();
$dumper->setColors($config['colors']);
$dump = [];
$dumper->dump(
$cloner->cloneVar($var),
function ($line, $depth, $indent_pad) use (&$dump) {
// A negative depth means "end of dump".
if ($depth >= 0) {
// Adds a two spaces indentation to the line.
$dump[] = str_repeat($indent_pad, $depth) . $line;
}
}
);
file_put_contents(
$config['stagingFilePath'],
self::encodeDump($caller['test']->id(), $caller['file'], $caller['line'], $dump) . "\n",
FILE_APPEND,
);
}
/**
* Encodes the dump for storing.
*
* @param string $testId
* The id of the test from where the dump was called.
* @param string|null $file
* The path of the file from where the dump was called.
* @param int|null $line
* The line number from where the dump was called.
* @param array $dump
* The dump as an array of lines.
*
* @return string
* An encoded string.
*/
private static function encodeDump(string $testId, ?string $file, ?int $line, array $dump): string {
$data = [
'test' => $testId,
'file' => $file,
'line' => $line,
'dump' => $dump,
];
$jsonData = json_encode($data);
return base64_encode($jsonData);
}
/**
* Decodes a dump retrieved from storage.
*
* @param string $encodedData
* An encoded string.
*
* @return array{test: string, file: string|null, line: int|null, dump: string[]}
* An encoded string.
*/
private static function decodeDump(string $encodedData): array {
$jsonData = base64_decode($encodedData);
return (array) json_decode($jsonData);
}
/**
* Returns information about the caller of dump().
*
* @return array{test: \PHPUnit\Framework\Event\Code\TestMethod, file: string|null, line: int|null}
* Caller information.
*/
private static function getCaller(): array {
$backtrace = debug_backtrace();
while (!isset($backtrace[0]['function']) || $backtrace[0]['function'] !== 'dump') {
array_shift($backtrace);
}
$call['file'] = $backtrace[1]['file'] ?? NULL;
$call['line'] = $backtrace[1]['line'] ?? NULL;
while (!isset($backtrace[0]['object']) || !($backtrace[0]['object'] instanceof TestCase)) {
array_shift($backtrace);
}
$call['test'] = $backtrace[0]['object']->valueObjectForEvents();
return $call;
}
/**
* Retrieves dumps from storage.
*
* @return array{string, array{file: string|null, line: int|null, dump: string[]}}
* Caller information.
*/
public static function getDumps(): array {
if (!self::isEnabled()) {
return [];
}
$config = (array) json_decode(getenv('DRUPAL_PHPUNIT_DUMPER_CONFIG'));
$contents = rtrim(file_get_contents($config['stagingFilePath']));
if (empty($contents)) {
return [];
}
$encodedDumps = explode("\n", $contents);
$dumps = [];
foreach ($encodedDumps as $encodedDump) {
$dump = self::decodeDump($encodedDump);
$test = $dump['test'];
unset($dump['test']);
$dumps[$test][] = $dump;
}
return $dumps;
}
/**
* Prints the dumps generated during the test.
*/
public function testRunnerFinished(TestRunnerFinished $event): void {
$dumps = self::getDumps();
// Cleanup.
unlink(self::$stagingFilePath);
putenv('DRUPAL_PHPUNIT_DUMPER_CONFIG');
if ($dumps === []) {
return;
}
print "\n\n";
print "dump() output\n";
print "-------------\n\n";
foreach ($dumps as $testId => $testDumps) {
if (self::$printCaller) {
print $testId . "\n";
}
foreach ($testDumps as $dump) {
if (self::$printCaller) {
print "in " . $dump['file'] . ", line " . $dump['line'] . ":\n";
}
foreach ($dump['dump'] as $line) {
print $line . "\n";
}
if (self::$printCaller) {
print "\n";
}
}
if (self::$printCaller) {
print "\n";
}
}
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\Dump;
use PHPUnit\Event\TestRunner\Finished;
use PHPUnit\Event\TestRunner\FinishedSubscriber;
/**
* Event subscriber notifying end of test runner execution to HTML logging.
*
* @internal
*/
final class TestRunnerFinishedSubscriber implements FinishedSubscriber {
public function __construct(
private readonly DebugDump $dump,
) {
}
public function notify(Finished $event): void {
$this->dump->testRunnerFinished($event);
}
}

View File

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\HtmlLogging;
use PHPUnit\Event\TestRunner\Finished as TestRunnerFinished;
use PHPUnit\Event\TestRunner\Started as TestRunnerStarted;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
/**
* Drupal's extension for providing HTML output results for functional tests.
*
* @internal
*/
final class HtmlOutputLogger implements Extension {
/**
* The status of the extension.
*/
private bool $enabled = FALSE;
/**
* A file with list of links to HTML pages generated.
*/
private ?string $browserOutputFile = NULL;
/**
* A file with list of links to HTML pages generated.
*/
private string $outputDirectory;
/**
* Verbosity of the final report.
*
* If TRUE, a list of links generated will be output at the end of the test
* run; if FALSE, only a summary with the count of pages generated.
*/
private bool $outputVerbose;
/**
* {@inheritdoc}
*/
public function bootstrap(
Configuration $configuration,
Facade $facade,
ParameterCollection $parameters,
): void {
// Determine output directory.
$envDirectory = getenv('BROWSERTEST_OUTPUT_DIRECTORY');
if ($envDirectory === "") {
print "HTML output disabled by BROWSERTEST_OUTPUT_DIRECTORY = ''.\n\n";
return;
}
elseif ($envDirectory !== FALSE) {
$directory = $envDirectory;
}
elseif ($parameters->has('outputDirectory')) {
$directory = $parameters->get('outputDirectory');
}
else {
print "HTML output directory not specified.\n\n";
return;
}
$realDirectory = realpath($directory);
if ($realDirectory === FALSE || !is_dir($realDirectory) || !is_writable($realDirectory)) {
print "HTML output directory {$directory} is not a writable directory.\n\n";
return;
}
$this->outputDirectory = $realDirectory;
// Determine output verbosity.
$envVerbose = getenv('BROWSERTEST_OUTPUT_VERBOSE');
if ($envVerbose !== FALSE) {
$verbose = $envVerbose;
}
elseif ($parameters->has('verbose')) {
$verbose = $parameters->get('verbose');
}
else {
$verbose = FALSE;
}
$this->outputVerbose = filter_var($verbose, \FILTER_VALIDATE_BOOLEAN);
$facade->registerSubscriber(new TestRunnerStartedSubscriber($this));
$facade->registerSubscriber(new TestRunnerFinishedSubscriber($this));
$this->enabled = TRUE;
}
/**
* Logs a link to a generated HTML page.
*
* @param string $logEntry
* A link to a generated HTML page, should not contain a trailing newline.
*
* @throws \RuntimeException
*/
public static function log(string $logEntry): void {
$browserOutputFile = getenv('BROWSERTEST_OUTPUT_FILE');
if ($browserOutputFile === FALSE) {
throw new \RuntimeException("HTML output is not enabled");
}
file_put_contents($browserOutputFile, $logEntry . "\n", FILE_APPEND);
}
/**
* Empties the list of the HTML output created during the test run.
*/
public function testRunnerStarted(TestRunnerStarted $event): void {
if (!$this->enabled) {
throw new \RuntimeException("HTML output is not enabled");
}
// Convert to a canonicalized absolute pathname just in case the current
// working directory is changed.
$this->browserOutputFile = tempnam($this->outputDirectory, 'browser_output_');
if ($this->browserOutputFile) {
touch($this->browserOutputFile);
putenv('BROWSERTEST_OUTPUT_FILE=' . $this->browserOutputFile);
}
else {
// Remove any environment variable.
putenv('BROWSERTEST_OUTPUT_FILE');
throw new \RuntimeException("Unable to create a temporary file in {$this->outputDirectory}.");
}
}
/**
* Prints the list of HTML output generated during the test.
*/
public function testRunnerFinished(TestRunnerFinished $event): void {
if (!$this->enabled) {
throw new \RuntimeException("HTML output is not enabled");
}
$contents = file_get_contents($this->browserOutputFile);
if ($contents) {
print "\n\n";
if ($this->outputVerbose) {
print "HTML output was generated.\n";
print $contents;
}
else {
print "HTML output was generated, " . count(explode("\n", $contents)) . " page(s).\n";
}
}
// No need to keep the file around any more.
unlink($this->browserOutputFile);
putenv('BROWSERTEST_OUTPUT_FILE');
$this->browserOutputFile = NULL;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\HtmlLogging;
/**
* Base class for PHPUnit event subscribers related to HTML logging.
*
* @internal
*/
abstract class SubscriberBase {
public function __construct(
private readonly HtmlOutputLogger $logger,
) {
}
protected function logger(): HtmlOutputLogger {
return $this->logger;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\HtmlLogging;
use PHPUnit\Event\TestRunner\Finished;
use PHPUnit\Event\TestRunner\FinishedSubscriber;
/**
* Event subscriber notifying end of test runner execution to HTML logging.
*
* @internal
*/
final class TestRunnerFinishedSubscriber extends SubscriberBase implements FinishedSubscriber {
public function notify(Finished $event): void {
$this->logger()->testRunnerFinished($event);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension\HtmlLogging;
use PHPUnit\Event\TestRunner\Started;
use PHPUnit\Event\TestRunner\StartedSubscriber;
/**
* Event subscriber notifying beginning of test runner to HTML logging.
*
* @internal
*/
final class TestRunnerStartedSubscriber extends SubscriberBase implements StartedSubscriber {
public function notify(Started $event): void {
$this->logger()->testRunnerStarted($event);
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension;
use Drupal\Core\Serialization\Yaml;
/**
* Writes the info file and ensures the mtime changes.
*
* @see \Drupal\Component\FileCache\FileCache
* @see \Drupal\Core\Extension\InfoParser
*/
trait InfoWriterTrait {
/**
* Writes the info file and ensures the mtime changes.
*
* @param string $file_path
* The info file path.
* @param array $info
* The info array.
*
* @return void
* No return value.
*/
private function writeInfoFile(string $file_path, array $info): void {
$mtime = file_exists($file_path) ? filemtime($file_path) : FALSE;
file_put_contents($file_path, Yaml::encode($info));
// Ensure mtime changes.
if ($mtime === filemtime($file_path)) {
touch($file_path, max($mtime + 1, time()));
}
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension;
use PHPUnit\Framework\Attributes\BeforeClass;
use Symfony\Component\Process\ExecutableFinder;
/**
* Ensures Composer executable is available, skips test otherwise.
*/
trait RequiresComposerTrait {
#[BeforeClass]
public static function requiresComposer(): void {
if (!((new ExecutableFinder())->find('composer'))) {
static::markTestSkipped('This test requires the Composer executable to be accessible.');
}
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Provides methods to access modules' schema.
*/
class SchemaInspector {
/**
* Returns the module's schema specification.
*
* This function can be used to retrieve a schema specification provided by
* hook_schema(), so it allows you to derive your tables from existing
* specifications.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $handler
* The module handler to use for calling schema hook.
* @param string $module
* The module to which the table belongs.
*
* @return array
* An array of schema definition provided by hook_schema().
*
* @see \hook_schema()
*/
public static function getTablesSpecification(ModuleHandlerInterface $handler, string $module): array {
if ($handler->loadInclude($module, 'install')) {
return $handler->invoke($module, 'schema') ?? [];
}
return [];
}
}