diff options
Diffstat (limited to 'core/lib')
57 files changed, 1010 insertions, 549 deletions
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 63514b60eff8..9b6dc8765017 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -76,7 +76,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '11.2-dev'; + const VERSION = '11.3-dev'; /** * Core API compatibility. diff --git a/core/lib/Drupal/Component/Annotation/Doctrine/DocParser.php b/core/lib/Drupal/Component/Annotation/Doctrine/DocParser.php index 052542ecfca7..5fb3e2e3a75e 100644 --- a/core/lib/Drupal/Component/Annotation/Doctrine/DocParser.php +++ b/core/lib/Drupal/Component/Annotation/Doctrine/DocParser.php @@ -417,7 +417,7 @@ final class DocParser $message = sprintf('Expected %s, got ', $expected); $message .= ($this->lexer->lookahead === null) ? 'end of string' - : sprintf("'%s' at position %s", $token['value'], $token['position']); + : sprintf("'%s' at position %s", $token->value, $token->position); if (strlen($this->context)) { $message .= ' in ' . $this->context; @@ -616,13 +616,13 @@ final class DocParser $annotations = array(); while (null !== $this->lexer->lookahead) { - if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { + if (DocLexer::T_AT !== $this->lexer->lookahead->type) { $this->lexer->moveNext(); continue; } // make sure the @ is preceded by non-catchable pattern - if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { + if (null !== $this->lexer->token && $this->lexer->lookahead->position === $this->lexer->token->position + strlen($this->lexer->token->value)) { $this->lexer->moveNext(); continue; } @@ -630,8 +630,8 @@ final class DocParser // make sure the @ is followed by either a namespace separator, or // an identifier token if ((null === $peek = $this->lexer->glimpse()) - || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) - || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { + || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek->type && !in_array($peek->type, self::$classIdentifiers, true)) + || $peek->position !== $this->lexer->lookahead->position + 1) { $this->lexer->moveNext(); continue; } @@ -988,17 +988,17 @@ final class DocParser $this->lexer->moveNext(); - $className = $this->lexer->token['value']; + $className = $this->lexer->token->value; while ( null !== $this->lexer->lookahead && - $this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value'])) && + $this->lexer->lookahead->position === ($this->lexer->token->position + strlen($this->lexer->token->value)) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR) ) { $this->match(DocLexer::T_NAMESPACE_SEPARATOR); $this->matchAny(self::$classIdentifiers); - $className .= '\\' . $this->lexer->token['value']; + $className .= '\\' . $this->lexer->token->value; } return $className; @@ -1013,7 +1013,7 @@ final class DocParser { $peek = $this->lexer->glimpse(); - if (DocLexer::T_EQUALS === $peek['type']) { + if (DocLexer::T_EQUALS === $peek->type) { return $this->FieldAssignment(); } @@ -1039,18 +1039,18 @@ final class DocParser return $this->Constant(); } - switch ($this->lexer->lookahead['type']) { + switch ($this->lexer->lookahead->type) { case DocLexer::T_STRING: $this->match(DocLexer::T_STRING); - return $this->lexer->token['value']; + return $this->lexer->token->value; case DocLexer::T_INTEGER: $this->match(DocLexer::T_INTEGER); - return (int)$this->lexer->token['value']; + return (int)$this->lexer->token->value; case DocLexer::T_FLOAT: $this->match(DocLexer::T_FLOAT); - return (float)$this->lexer->token['value']; + return (float)$this->lexer->token->value; case DocLexer::T_TRUE: $this->match(DocLexer::T_TRUE); @@ -1078,7 +1078,7 @@ final class DocParser private function FieldAssignment() { $this->match(DocLexer::T_IDENTIFIER); - $fieldName = $this->lexer->token['value']; + $fieldName = $this->lexer->token->value; $this->match(DocLexer::T_EQUALS); @@ -1146,14 +1146,14 @@ final class DocParser { $peek = $this->lexer->glimpse(); - if (DocLexer::T_EQUALS === $peek['type'] - || DocLexer::T_COLON === $peek['type']) { + if (DocLexer::T_EQUALS === $peek->type + || DocLexer::T_COLON === $peek->type) { if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { $key = $this->Constant(); } else { $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING)); - $key = $this->lexer->token['value']; + $key = $this->lexer->token->value; } $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON)); diff --git a/core/lib/Drupal/Component/Annotation/composer.json b/core/lib/Drupal/Component/Annotation/composer.json index 3575798a7c15..24977f8da9f7 100644 --- a/core/lib/Drupal/Component/Annotation/composer.json +++ b/core/lib/Drupal/Component/Annotation/composer.json @@ -9,7 +9,7 @@ "require": { "php": ">=8.3.0", "doctrine/annotations": "^2.0", - "doctrine/lexer": "^2.0", + "doctrine/lexer": "^2 || ^3", "drupal/core-class-finder": "11.x-dev", "drupal/core-file-cache": "11.x-dev", "drupal/core-plugin": "11.x-dev", diff --git a/core/lib/Drupal/Component/DependencyInjection/composer.json b/core/lib/Drupal/Component/DependencyInjection/composer.json index ccf16002da9c..df9e64651815 100644 --- a/core/lib/Drupal/Component/DependencyInjection/composer.json +++ b/core/lib/Drupal/Component/DependencyInjection/composer.json @@ -14,8 +14,8 @@ }, "require": { "php": ">=8.3.0", - "symfony/dependency-injection": "^7.3@beta", - "symfony/service-contracts": "v3.5.1" + "symfony/dependency-injection": "^7.3", + "symfony/service-contracts": "v3.6.0" }, "suggest": { "symfony/expression-language": "For using expressions in service container configuration" diff --git a/core/lib/Drupal/Component/EventDispatcher/composer.json b/core/lib/Drupal/Component/EventDispatcher/composer.json index b832a6c45b1b..d78d1b980c0c 100644 --- a/core/lib/Drupal/Component/EventDispatcher/composer.json +++ b/core/lib/Drupal/Component/EventDispatcher/composer.json @@ -8,9 +8,9 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=8.3.0", - "symfony/dependency-injection": "^7.3@beta", - "symfony/event-dispatcher": "^7.3@beta", - "symfony/event-dispatcher-contracts": "v3.5.1" + "symfony/dependency-injection": "^7.3", + "symfony/event-dispatcher": "^7.3", + "symfony/event-dispatcher-contracts": "v3.6.0" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/HttpFoundation/composer.json b/core/lib/Drupal/Component/HttpFoundation/composer.json index 283e0713ca5c..85e62e4ef9f5 100644 --- a/core/lib/Drupal/Component/HttpFoundation/composer.json +++ b/core/lib/Drupal/Component/HttpFoundation/composer.json @@ -8,7 +8,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=8.3.0", - "symfony/http-foundation": "^7.3@beta" + "symfony/http-foundation": "^7.3" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/Plugin/LazyPluginCollection.php b/core/lib/Drupal/Component/Plugin/LazyPluginCollection.php index e11003efcb2d..86408c233900 100644 --- a/core/lib/Drupal/Component/Plugin/LazyPluginCollection.php +++ b/core/lib/Drupal/Component/Plugin/LazyPluginCollection.php @@ -142,7 +142,11 @@ abstract class LazyPluginCollection implements \IteratorAggregate, \Countable { $this->remove($instance_id); } - public function getIterator(): \ArrayIterator { + /** + * @return \Traversable<string, mixed> + * A traversable generator. + */ + public function getIterator(): \Traversable { $instances = []; foreach ($this->getInstanceIds() as $instance_id) { $instances[$instance_id] = $this->get($instance_id); diff --git a/core/lib/Drupal/Component/Plugin/composer.json b/core/lib/Drupal/Component/Plugin/composer.json index 6a4f1fc7e2cd..2c5a4864291a 100644 --- a/core/lib/Drupal/Component/Plugin/composer.json +++ b/core/lib/Drupal/Component/Plugin/composer.json @@ -10,7 +10,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=8.3.0", - "symfony/validator": "^7.3@beta" + "symfony/validator": "^7.3" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/Render/FormattableMarkup.php b/core/lib/Drupal/Component/Render/FormattableMarkup.php index 6db6288d47d5..c6e5ebcb9ddf 100644 --- a/core/lib/Drupal/Component/Render/FormattableMarkup.php +++ b/core/lib/Drupal/Component/Render/FormattableMarkup.php @@ -124,10 +124,10 @@ class FormattableMarkup implements MarkupInterface, \Countable { * Insecure examples. * @code * // The following are using the @ placeholder inside an HTML tag. - * $this->placeholderFormat('<@foo>text</@foo>, ['@foo' => $some_variable]); - * $this->placeholderFormat('<a @foo>link text</a>, ['@foo' => $some_variable]); - * $this->placeholderFormat('<a href="@foo">link text</a>, ['@foo' => $some_variable]); - * $this->placeholderFormat('<a title="@foo">link text</a>, ['@foo' => $some_variable]); + * $this->placeholderFormat('<@foo>text</@foo>', ['@foo' => $some_variable]); + * $this->placeholderFormat('<a @foo>link text</a>', ['@foo' => $some_variable]); + * $this->placeholderFormat('<a href="@foo">link text</a>', ['@foo' => $some_variable]); + * $this->placeholderFormat('<a title="@foo">link text</a>', ['@foo' => $some_variable]); * // Implicitly convert an object to a string, which is not sanitized. * $this->placeholderFormat('Non-sanitized replacement value: @foo', ['@foo' => $safe_string_interface_object]); * @endcode diff --git a/core/lib/Drupal/Component/Serialization/composer.json b/core/lib/Drupal/Component/Serialization/composer.json index 10068bba3ced..22325e148742 100644 --- a/core/lib/Drupal/Component/Serialization/composer.json +++ b/core/lib/Drupal/Component/Serialization/composer.json @@ -8,7 +8,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=8.3.0", - "symfony/yaml": "^7.3@beta" + "symfony/yaml": "^7.3" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Core/Access/AccessGroupAnd.php b/core/lib/Drupal/Core/Access/AccessGroupAnd.php new file mode 100644 index 000000000000..d4535ccac523 --- /dev/null +++ b/core/lib/Drupal/Core/Access/AccessGroupAnd.php @@ -0,0 +1,55 @@ +<?php + +namespace Drupal\Core\Access; + +use Drupal\Core\Session\AccountInterface; + +/** + * An access group where all the dependencies must be allowed. + * + * @internal + */ +class AccessGroupAnd implements AccessibleInterface { + + /** + * The access dependencies. + * + * @var \Drupal\Core\Access\AccessibleInterface[] + */ + protected $dependencies = []; + + /** + * Adds an access dependency. + * + * @param \Drupal\Core\Access\AccessibleInterface $dependency + * The access dependency to be added. + * + * @return $this + */ + public function addDependency(AccessibleInterface $dependency) { + $this->dependencies[] = $dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) { + $access_result = AccessResult::neutral(); + foreach (array_slice($this->dependencies, 1) as $dependency) { + $access_result = $access_result->andIf($dependency->access($operation, $account, TRUE)); + } + return $return_as_object ? $access_result : $access_result->isAllowed(); + } + + /** + * Gets all the access dependencies. + * + * @return list<\Drupal\Core\Access\AccessibleInterface> + * The list of access dependencies. + */ + public function getDependencies() { + return $this->dependencies; + } + +} diff --git a/core/lib/Drupal/Core/Access/DependentAccessInterface.php b/core/lib/Drupal/Core/Access/DependentAccessInterface.php new file mode 100644 index 000000000000..eee44102c953 --- /dev/null +++ b/core/lib/Drupal/Core/Access/DependentAccessInterface.php @@ -0,0 +1,35 @@ +<?php + +namespace Drupal\Core\Access; + +/** + * Interface for AccessibleInterface objects that have an access dependency. + * + * Objects should implement this interface when their access depends on access + * to another object that implements \Drupal\Core\Access\AccessibleInterface. + * This interface simply provides the getter method for the access + * dependency object. Objects that implement this interface are responsible for + * checking access of the access dependency because the dependency may not take + * effect in all cases. For instance an entity may only need the access + * dependency set when it is embedded within another entity and its access + * should be dependent on access to the entity in which it is embedded. + * + * To check the access to the dependency the object implementing this interface + * can use code like this: + * @code + * $accessible->getAccessDependency()->access($op, $account, TRUE); + * @endcode + * + * @internal + */ +interface DependentAccessInterface { + + /** + * Gets the access dependency. + * + * @return \Drupal\Core\Access\AccessibleInterface|null + * The access dependency or NULL if none has been set. + */ + public function getAccessDependency(); + +} diff --git a/core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php b/core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php new file mode 100644 index 000000000000..a6e5e671aaae --- /dev/null +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php @@ -0,0 +1,46 @@ +<?php + +namespace Drupal\Core\Access; + +/** + * An interface to allow adding an access dependency. + * + * @internal + */ +interface RefinableDependentAccessInterface extends DependentAccessInterface { + + /** + * Sets the access dependency. + * + * If an access dependency is already set this will replace the existing + * dependency. + * + * @param \Drupal\Core\Access\AccessibleInterface $access_dependency + * The object upon which access depends. + * + * @return $this + */ + public function setAccessDependency(AccessibleInterface $access_dependency); + + /** + * Adds an access dependency into the existing access dependency. + * + * If no existing dependency is currently set this will set the dependency + * will be set to the new value. + * + * If there is an existing dependency and it is not an instance of + * AccessGroupAnd the dependency will be set as a new AccessGroupAnd + * instance with the existing and new dependencies as the members of the + * group. + * + * If there is an existing dependency and it is an instance of AccessGroupAnd + * the dependency will be added to the existing access group. + * + * @param \Drupal\Core\Access\AccessibleInterface $access_dependency + * The access dependency to merge. + * + * @return $this + */ + public function addAccessDependency(AccessibleInterface $access_dependency); + +} diff --git a/core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php b/core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php new file mode 100644 index 000000000000..96b966e8d873 --- /dev/null +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\Core\Access; + +/** + * Trait for \Drupal\Core\Access\RefinableDependentAccessInterface. + * + * @internal + */ +trait RefinableDependentAccessTrait { + + /** + * The access dependency. + * + * @var \Drupal\Core\Access\AccessibleInterface + */ + protected $accessDependency; + + /** + * {@inheritdoc} + */ + public function setAccessDependency(AccessibleInterface $access_dependency) { + $this->accessDependency = $access_dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAccessDependency() { + return $this->accessDependency; + } + + /** + * {@inheritdoc} + */ + public function addAccessDependency(AccessibleInterface $access_dependency) { + if (empty($this->accessDependency)) { + $this->accessDependency = $access_dependency; + return $this; + } + if (!$this->accessDependency instanceof AccessGroupAnd) { + $accessGroup = new AccessGroupAnd(); + $this->accessDependency = $accessGroup->addDependency($this->accessDependency); + } + $this->accessDependency->addDependency($access_dependency); + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php index 6c02649d2705..dad8bc10a21e 100644 --- a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php +++ b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php @@ -7,7 +7,7 @@ use Drupal\Component\Assertion\Inspector; /** * Passes cache tag events to classes that wish to respond to them. */ -class CacheTagsInvalidator implements CacheTagsInvalidatorInterface { +class CacheTagsInvalidator implements CacheTagsInvalidatorInterface, CacheTagsPurgeInterface { /** * Holds an array of cache tags invalidators. @@ -54,6 +54,17 @@ class CacheTagsInvalidator implements CacheTagsInvalidatorInterface { } /** + * {@inheritdoc} + */ + public function purge(): void { + foreach ($this->invalidators as $invalidator) { + if ($invalidator instanceof CacheTagsPurgeInterface) { + $invalidator->purge(); + } + } + } + + /** * Adds a cache tags invalidator. * * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator diff --git a/core/lib/Drupal/Core/Cache/CacheTagsPurgeInterface.php b/core/lib/Drupal/Core/Cache/CacheTagsPurgeInterface.php new file mode 100644 index 000000000000..24c110372d12 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheTagsPurgeInterface.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Cache; + +/** + * Provides purging of cache tag invalidations. + * + * Backends that persistently store cache tag invalidations can use this + * interface to implement purging of cache tag invalidations. By default, cache + * tag purging will only be called during drupal_flush_all_caches(), after all + * other caches have been cleared. + * + * @ingroup cache + */ +interface CacheTagsPurgeInterface { + + /** + * Purge cache tag invalidations. + */ + public function purge(): void; + +} diff --git a/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php b/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php index cb88c69495a7..9602bc8ba5d5 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php +++ b/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php @@ -8,7 +8,7 @@ use Drupal\Core\Database\DatabaseException; /** * Cache tags invalidations checksum implementation that uses the database. */ -class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface, CacheTagsChecksumPreloadInterface { +class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface, CacheTagsChecksumPreloadInterface, CacheTagsPurgeInterface { use CacheTagsChecksumTrait; @@ -70,6 +70,22 @@ class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTags } /** + * {@inheritdoc} + */ + public function purge(): void { + try { + $this->connection->truncate('cachetags')->execute(); + } + catch (\Throwable $e) { + // If the table does not exist yet, there is nothing to purge. + if (!$this->ensureTableExists()) { + throw $e; + } + } + $this->reset(); + } + + /** * Check if the cache tags table exists and create it if not. */ protected function ensureTableExists() { diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php index fbc7542a7de9..9f6d1188dc88 100644 --- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php @@ -5,6 +5,7 @@ declare(strict_types = 1); namespace Drupal\Core\Config\Plugin\Validation\Constraint; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\Schema\TypeResolver; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; @@ -50,6 +51,8 @@ class ConfigExistsConstraintValidator extends ConstraintValidator implements Con return; } + $constraint->prefix = TypeResolver::resolveDynamicTypeName($constraint->prefix, $this->context->getObject()); + if (!in_array($constraint->prefix . $name, $this->configFactory->listAll($constraint->prefix), TRUE)) { $this->context->addViolation($constraint->message, ['@name' => $constraint->prefix . $name]); } diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 489b2f5f94d1..133dc99f182e 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -433,7 +433,7 @@ abstract class Connection { public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { assert(!isset($options['return']), 'Passing "return" option to prepareStatement() has no effect. See https://www.drupal.org/node/3185520'); if (isset($options['fetch']) && is_int($options['fetch'])) { - @trigger_error("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); } try { @@ -654,7 +654,7 @@ abstract class Connection { assert(!isset($options['return']), 'Passing "return" option to query() has no effect. See https://www.drupal.org/node/3185520'); assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033'); if (isset($options['fetch']) && is_int($options['fetch'])) { - @trigger_error("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); } // Use default values if not already set. diff --git a/core/lib/Drupal/Core/Database/Statement/PdoTrait.php b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php index 3e1f104c9f4f..f477b466a639 100644 --- a/core/lib/Drupal/Core/Database/Statement/PdoTrait.php +++ b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php @@ -12,7 +12,7 @@ trait PdoTrait { /** * Converts a FetchAs mode to a \PDO::FETCH_* constant value. * - * @param \Drupal\Core\Database\FetchAs $mode + * @param \Drupal\Core\Database\Statement\FetchAs $mode * The FetchAs mode. * * @return int @@ -34,7 +34,7 @@ trait PdoTrait { * @param int $mode * The \PDO::FETCH_* constant value. * - * @return \Drupal\Core\Database\FetchAs + * @return \Drupal\Core\Database\Statement\FetchAs * A FetchAs mode. */ protected function pdoToFetchAs(int $mode): FetchAs { @@ -70,7 +70,7 @@ trait PdoTrait { /** * Sets the default fetch mode for the PDO statement. * - * @param \Drupal\Core\Database\FetchAs $mode + * @param \Drupal\Core\Database\Statement\FetchAs $mode * One of the cases of the FetchAs enum. * @param int|class-string|null $columnOrClass * If $mode is FetchAs::Column, the index of the column to fetch. @@ -118,7 +118,7 @@ trait PdoTrait { /** * Fetches the next row from the PDO statement. * - * @param \Drupal\Core\Database\FetchAs|null $mode + * @param \Drupal\Core\Database\Statement\FetchAs|null $mode * (Optional) one of the cases of the FetchAs enum. If not specified, * defaults to what is specified by setFetchMode(). * @param int|null $cursorOrientation @@ -175,7 +175,7 @@ trait PdoTrait { /** * Returns an array containing all of the result set rows. * - * @param \Drupal\Core\Database\FetchAs|null $mode + * @param \Drupal\Core\Database\Statement\FetchAs|null $mode * (Optional) one of the cases of the FetchAs enum. If not specified, * defaults to what is specified by setFetchMode(). * @param int|class-string|null $columnOrClass diff --git a/core/lib/Drupal/Core/Database/Statement/ResultBase.php b/core/lib/Drupal/Core/Database/Statement/ResultBase.php index 6232581f906f..af1b12a56534 100644 --- a/core/lib/Drupal/Core/Database/Statement/ResultBase.php +++ b/core/lib/Drupal/Core/Database/Statement/ResultBase.php @@ -42,7 +42,7 @@ abstract class ResultBase { /** * Sets the default fetch mode for this result set. * - * @param \Drupal\Core\Database\FetchAs $mode + * @param \Drupal\Core\Database\Statement\FetchAs $mode * One of the cases of the FetchAs enum. * @param array{class: class-string, constructor_args: list<mixed>, column: int, cursor_orientation?: int, cursor_offset?: int} $fetchOptions * An array of fetch options. @@ -55,7 +55,7 @@ abstract class ResultBase { /** * Fetches the next row. * - * @param \Drupal\Core\Database\FetchAs $mode + * @param \Drupal\Core\Database\Statement\FetchAs $mode * One of the cases of the FetchAs enum. * @param array{class?: class-string, constructor_args?: list<mixed>, column?: int, cursor_orientation?: int, cursor_offset?: int} $fetchOptions * An array of fetch options. @@ -68,7 +68,7 @@ abstract class ResultBase { /** * Returns an array containing all of the result set rows. * - * @param \Drupal\Core\Database\FetchAs $mode + * @param \Drupal\Core\Database\Statement\FetchAs $mode * One of the cases of the FetchAs enum. * @param array{class?: class-string, constructor_args?: list<mixed>, column?: int, cursor_orientation?: int, cursor_offset?: int} $fetchOptions * An array of fetch options. @@ -120,7 +120,7 @@ abstract class ResultBase { * * @param string $column * The name of the field on which to index the array. - * @param \Drupal\Core\Database\FetchAs $mode + * @param \Drupal\Core\Database\Statement\FetchAs $mode * One of the cases of the FetchAs enum. If set to FetchAs::Associative * or FetchAs::List the returned value with be an array of arrays. For any * other value it will be an array of objects. If not specified, defaults to diff --git a/core/lib/Drupal/Core/Database/Statement/StatementBase.php b/core/lib/Drupal/Core/Database/Statement/StatementBase.php index c193c5d35020..98fa378d58f4 100644 --- a/core/lib/Drupal/Core/Database/Statement/StatementBase.php +++ b/core/lib/Drupal/Core/Database/Statement/StatementBase.php @@ -180,7 +180,7 @@ abstract class StatementBase implements \Iterator, StatementInterface { */ public function setFetchMode($mode, $a1 = NULL, $a2 = []) { if (is_int($mode)) { - @trigger_error("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); $mode = $this->pdoToFetchAs($mode); } assert($mode instanceof FetchAs); @@ -217,7 +217,7 @@ abstract class StatementBase implements \Iterator, StatementInterface { */ public function fetch($mode = NULL, $cursorOrientation = NULL, $cursorOffset = NULL) { if (is_int($mode)) { - @trigger_error("Passing the \$mode argument as an integer to fetch() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the \$mode argument as an integer to fetch() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); $mode = $this->pdoToFetchAs($mode); } assert($mode === NULL || $mode instanceof FetchAs); @@ -292,7 +292,7 @@ abstract class StatementBase implements \Iterator, StatementInterface { */ public function fetchAll($mode = NULL, $columnIndex = NULL, $constructorArguments = NULL) { if (is_int($mode)) { - @trigger_error("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); $mode = $this->pdoToFetchAs($mode); } @@ -325,7 +325,7 @@ abstract class StatementBase implements \Iterator, StatementInterface { */ public function fetchAllAssoc($key, $fetch = NULL) { if (is_int($fetch)) { - @trigger_error("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); $fetch = $this->pdoToFetchAs($fetch); } assert($fetch === NULL || $fetch instanceof FetchAs); diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php index c4cafb9d2892..7f906620674d 100644 --- a/core/lib/Drupal/Core/Database/StatementInterface.php +++ b/core/lib/Drupal/Core/Database/StatementInterface.php @@ -65,7 +65,7 @@ interface StatementInterface extends \Traversable { /** * Sets the default fetch mode for this statement. * - * @param \Drupal\Core\Database\FetchAs|int $mode + * @param \Drupal\Core\Database\Statement\FetchAs|int $mode * One of the cases of the FetchAs enum, or (deprecated) a \PDO::FETCH_* * constant. * @param string|int|null $a1 @@ -87,7 +87,7 @@ interface StatementInterface extends \Traversable { /** * Fetches the next row from a result set. * - * @param \Drupal\Core\Database\FetchAs|int|null $mode + * @param \Drupal\Core\Database\Statement\FetchAs|int|null $mode * (Optional) one of the cases of the FetchAs enum, or (deprecated) a * \PDO::FETCH_* constant. If not specified, defaults to what is specified * by setFetchMode(). @@ -147,7 +147,7 @@ interface StatementInterface extends \Traversable { /** * Returns an array containing all of the result set rows. * - * @param \Drupal\Core\Database\FetchAs|int|null $mode + * @param \Drupal\Core\Database\Statement\FetchAs|int|null $mode * (Optional) one of the cases of the FetchAs enum, or (deprecated) a * \PDO::FETCH_* constant. If not specified, defaults to what is specified * by setFetchMode(). @@ -206,7 +206,7 @@ interface StatementInterface extends \Traversable { * * @param string $key * The name of the field on which to index the array. - * @param \Drupal\Core\Database\FetchAs|int|string|null $fetch + * @param \Drupal\Core\Database\Statement\FetchAs|int|string|null $fetch * (Optional) the fetch mode to use. One of the cases of the FetchAs enum, * or (deprecated) a \PDO::FETCH_* constant. If set to FetchAs::Associative * or FetchAs::List the returned value with be an array of arrays. For any diff --git a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php index 96bc07e7f89e..8a2a73f1bf7c 100644 --- a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php +++ b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php @@ -101,7 +101,7 @@ class StatementPrefetchIterator extends StatementBase { */ public function execute($args = [], $options = []) { if (isset($options['fetch']) && is_int($options['fetch'])) { - @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); } $startEvent = $this->dispatchStatementExecutionStartEvent($args ?? []); diff --git a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php index 88dc007f5403..f580d645cad4 100644 --- a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php +++ b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php @@ -52,7 +52,7 @@ class StatementWrapperIterator extends StatementBase { */ public function execute($args = [], $options = []) { if (isset($options['fetch']) && is_int($options['fetch'])) { - @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); + @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED); } if (isset($options['fetch'])) { diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php index dcecc44e17c6..b8693e4bb761 100644 --- a/core/lib/Drupal/Core/Database/Transaction.php +++ b/core/lib/Drupal/Core/Database/Transaction.php @@ -30,12 +30,12 @@ class Transaction { /** * Destructs the object. * - * Depending on the nesting level of the object, this leads to a COMMIT (for - * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on - * the database. + * If the transaction is still active at this stage, and depending on the + * state of the transaction stack, this leads to a COMMIT (for a root item) + * or to a RELEASE SAVEPOINT (for a savepoint item) executed on the database. */ public function __destruct() { - $this->connection->transactionManager()->unpile($this->name, $this->id); + $this->connection->transactionManager()->purge($this->name, $this->id); } /** @@ -46,16 +46,22 @@ class Transaction { } /** - * Rolls back the current transaction. + * Returns the transaction to the parent nesting level. * - * This is just a wrapper method to rollback whatever transaction stack we are - * currently in, which is managed by the TransactionManager. Note that logging - * needs to happen after a transaction has been rolled back or the log - * messages will be rolled back too. + * Depending on the state of the transaction stack, this leads to a COMMIT + * operation (for a root item), or to a RELEASE SAVEPOINT operation (for a + * savepoint item) executed on the database. + */ + public function commitOrRelease(): void { + $this->connection->transactionManager()->unpile($this->name, $this->id); + } + + /** + * Rolls back the transaction. * - * Depending on the nesting level of the object, this leads to a ROLLBACK (for - * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed - * on the database. + * Depending on the state of the transaction stack, this leads to a ROLLBACK + * operation (for a root item), or to a ROLLBACK TO SAVEPOINT + a RELEASE + * SAVEPOINT operations (for a savepoint item) executed on the database. */ public function rollBack() { $this->connection->transactionManager()->rollback($this->name, $this->id); diff --git a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php index aa663d942265..fa1a309a7678 100644 --- a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php +++ b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php @@ -102,6 +102,16 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { private ClientConnectionTransactionState $connectionTransactionState; /** + * Whether to trigger warnings when unpiling a void transaction. + * + * Normally FALSE, is set to TRUE by specific tests checking the internal + * state of the transaction stack. + * + * @internal + */ + public bool $triggerWarningWhenUnpilingOnVoidTransaction = FALSE; + + /** * Constructor. * * @param \Drupal\Core\Database\Connection $connection @@ -202,7 +212,9 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { protected function voidStackItem(string $id): void { // The item should be removed from $stack and added to $voidedItems for // later processing. - $this->voidedItems[$id] = $this->stack[$id]; + if (isset($this->stack[$id])) { + $this->voidedItems[$id] = $this->stack[$id]; + } $this->removeStackItem($id); } @@ -285,14 +297,29 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { } /** - * {@inheritdoc} + * Purges a Drupal transaction from the manager. + * + * This is only called by a Transaction object's ::__destruct() method and + * should only be called internally by a database driver. + * + * @param string $name + * The name of the transaction. + * @param string $id + * The id of the transaction. + * + * @throws \Drupal\Core\Database\TransactionOutOfOrderException + * If a Drupal Transaction with the specified name does not exist. + * @throws \Drupal\Core\Database\TransactionCommitFailedException + * If the commit of the root transaction failed. + * + * @internal */ - public function unpile(string $name, string $id): void { + public function purge(string $name, string $id): void { // If this is a 'root' transaction, and it is voided (that is, no longer in // the stack), then the transaction on the database is no longer active. An - // action such as a rollback, or a DDL statement, was executed that - // terminated the database transaction. So, we can process the post - // transaction callbacks. + // action such as a commit, a release savepoint, a rollback, or a DDL + // statement, was executed that terminated the database transaction. So, we + // can process the post transaction callbacks. if (!isset($this->stack()[$id]) && isset($this->voidedItems[$id]) && $this->rootId === $id) { $this->processPostTransactionCallbacks(); $this->rootId = NULL; @@ -309,6 +336,62 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { return; } + // When we get here, the transaction (or savepoint) is still active on the + // database. We can unpile it, and if we are left with no more items in the + // stack, we can also process the post transaction callbacks. + $this->commit($name, $id); + $this->removeStackItem($id); + if ($this->rootId === $id) { + $this->processPostTransactionCallbacks(); + $this->rootId = NULL; + } + } + + /** + * {@inheritdoc} + */ + public function unpile(string $name, string $id): void { + // If the transaction was voided, we cannot unpile. Skip but trigger a user + // warning if requested. + if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) { + if ($this->triggerWarningWhenUnpilingOnVoidTransaction) { + trigger_error('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', E_USER_WARNING); + } + return; + } + + // If there is no $id to commit, or if $id does not correspond to the one + // in the stack for that $name, the commit is out of order. + if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) { + throw new TransactionOutOfOrderException("Error attempting commit of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString()); + } + + // Commit the transaction. + $this->commit($name, $id); + + // Void the transaction stack item. + $this->voidStackItem($id); + } + + /** + * Commits a Drupal transaction. + * + * @param string $name + * The name of the transaction. + * @param string $id + * The id of the transaction. + * + * @throws \Drupal\Core\Database\TransactionOutOfOrderException + * If a Drupal Transaction with the specified name does not exist. + * @throws \Drupal\Core\Database\TransactionCommitFailedException + * If the commit of the root transaction failed. + */ + protected function commit(string $name, string $id): void { + if ($this->getConnectionTransactionState() !== ClientConnectionTransactionState::Active) { + // The stack got corrupted. + throw new TransactionOutOfOrderException("Transaction {$id}\\{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); + } + // If we are not releasing the last savepoint but an earlier one, or // committing a root transaction while savepoints are active, all // subsequent savepoints will be released as well. The stack must be @@ -317,33 +400,20 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { $this->voidStackItem((string) $i); } - if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) { - if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) { - // Release the client transaction savepoint in case the Drupal - // transaction is not a root one. - $this->releaseClientSavepoint($name); - } - elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) { - // If this was the root Drupal transaction, we can commit the client - // transaction. - $this->processRootCommit(); - if ($this->rootId === $id) { - $this->processPostTransactionCallbacks(); - $this->rootId = NULL; - } - } - else { - // The stack got corrupted. - throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); - } - - // Remove the transaction from the stack. - $this->removeStackItem($id); - return; + if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) { + // Release the client transaction savepoint in case the Drupal + // transaction is not a root one. + $this->releaseClientSavepoint($name); + } + elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) { + // If this was the root Drupal transaction, we can commit the client + // transaction. + $this->processRootCommit(); + } + else { + // The stack got corrupted. + throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); } - - // The stack got corrupted. - throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString()); } /** diff --git a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php index 11af511f14bc..a9aa2c770525 100644 --- a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php +++ b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php @@ -53,8 +53,8 @@ interface TransactionManagerInterface { * Removes a Drupal transaction from the stack. * * The unpiled item does not necessarily need to be the last on the stack. - * This method should only be called by a Transaction object going out of - * scope. + * This method should only be called by a Transaction object's + * ::commitOrRelease() method. * * This method should only be called internally by a database driver. * diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php index 1cdeb7263cee..127318c1cd0c 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php @@ -37,7 +37,6 @@ class BackendCompilerPass implements CompilerPassInterface { * {@inheritdoc} */ public function process(ContainerBuilder $container): void { - $driver_backend = NULL; if ($container->hasParameter('default_backend')) { $default_backend = $container->getParameter('default_backend'); // Opt out from the default backend. @@ -64,10 +63,10 @@ class BackendCompilerPass implements CompilerPassInterface { if ($container->hasAlias($id)) { continue; } - if ($container->hasDefinition("$driver_backend.$id") || $container->hasAlias("$driver_backend.$id")) { + if (isset($driver_backend) && ($container->hasDefinition("$driver_backend.$id") || $container->hasAlias("$driver_backend.$id"))) { $container->setAlias($id, new Alias("$driver_backend.$id")); } - elseif ($container->hasDefinition("$default_backend.$id") || $container->hasAlias("$default_backend.$id")) { + elseif (!empty($default_backend) && ($container->hasDefinition("$default_backend.$id") || $container->hasAlias("$default_backend.$id"))) { $container->setAlias($id, new Alias("$default_backend.$id")); } } diff --git a/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php b/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php index f2e8d7137c82..c40c9b830faa 100644 --- a/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php +++ b/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php @@ -20,8 +20,8 @@ interface InstallRequirementsInterface { * hand. As a consequence, install-time requirements must be checked without * access to the full Drupal API, because it is not available during * install.php. - * If a requirement has a severity of REQUIREMENT_ERROR, install.php will - * abort or at least the module will not install. + * If a requirement has a severity of RequirementSeverity::Error, install.php + * will abort or at least the module will not install. * Other severity levels have no effect on the installation. * Module dependencies do not belong to these installation requirements, * but should be defined in the module's .info.yml file. @@ -37,12 +37,9 @@ interface InstallRequirementsInterface { * - value: This should only be used for version numbers, do not set it if * not applicable. * - description: The description of the requirement/status. - * - severity: (optional) The requirement's result/severity level, one of: - * - REQUIREMENT_INFO: For info only. - * - REQUIREMENT_OK: The requirement is satisfied. - * - REQUIREMENT_WARNING: The requirement failed with a warning. - * - REQUIREMENT_ERROR: The requirement failed with an error. - * Defaults to REQUIREMENT_OK when installing. + * - severity: (optional) An instance of + * \Drupal\Core\Extension\Requirement\RequirementSeverity enum. Defaults + * to RequirementSeverity::OK when installing. */ public static function getRequirements(): array; diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 53cf3c95aa5f..ad361d3fe669 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -333,6 +333,9 @@ class ModuleHandler implements ModuleHandlerInterface { */ public function resetImplementations() { $this->alterEventListeners = []; + $this->invokeMap = []; + $this->listenersByHook = []; + $this->modulesByHook = []; } /** @@ -730,6 +733,7 @@ class ModuleHandler implements ModuleHandlerInterface { */ protected function getFlatHookListeners(string $hook): array { if (!isset($this->listenersByHook[$hook])) { + $this->listenersByHook[$hook] = []; foreach ($this->eventDispatcher->getListeners("drupal_hook.$hook") as $listener) { if (is_array($listener) && is_object($listener[0])) { $module = $this->hookImplementationsMap[$hook][get_class($listener[0])][$listener[1]]; @@ -755,7 +759,7 @@ class ModuleHandler implements ModuleHandlerInterface { } } - return $this->listenersByHook[$hook] ?? []; + return $this->listenersByHook[$hook]; } } diff --git a/core/lib/Drupal/Core/Extension/Requirement/RequirementSeverity.php b/core/lib/Drupal/Core/Extension/Requirement/RequirementSeverity.php new file mode 100644 index 000000000000..ec085c0cb5b4 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Requirement/RequirementSeverity.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Extension\Requirement; + +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * The requirements severity enum. + */ +enum RequirementSeverity: int { + + /* + * Informational message only. + */ + case Info = -1; + + /* + * Requirement successfully met. + */ + case OK = 0; + + /* + * Warning condition; proceed but flag warning. + */ + case Warning = 1; + + /* + * Error condition; abort installation. + */ + case Error = 2; + + /** + * Returns the translated title of the severity. + */ + public function title(): TranslatableMarkup { + return match ($this) { + self::Info => new TranslatableMarkup('Checked'), + self::OK => new TranslatableMarkup('OK'), + self::Warning => new TranslatableMarkup('Warnings found'), + self::Error => new TranslatableMarkup('Errors found'), + }; + } + + /** + * Returns the status of the severity. + * + * This string representation can be used as an array key when grouping + * requirements checks by severity, or in other places where the int-backed + * value is not appropriate. + */ + public function status(): string { + return match ($this) { + self::Info => 'checked', + self::OK => 'ok', + self::Warning => 'warning', + self::Error => 'error', + }; + + } + + /** + * Determines the most severe requirement in a list of requirements. + * + * @param array<string, array{'title': \Drupal\Core\StringTranslation\TranslatableMarkup, 'value': mixed, description: \Drupal\Core\StringTranslation\TranslatableMarkup, 'severity': \Drupal\Core\Extension\Requirement\RequirementSeverity}> $requirements + * An array of requirements, in the same format as is returned by + * hook_requirements(), hook_runtime_requirements(), + * hook_update_requirements(), and + * \Drupal\Core\Extension\InstallRequirementsInterface. + * + * @return \Drupal\Core\Extension\Requirement\RequirementSeverity + * The most severe requirement. + * + * @see \Drupal\Core\Extension\InstallRequirementsInterface::getRequirements() + * @see \hook_requirements() + * @see \hook_runtime_requirements() + * @see \hook_update_requirements() + */ + public static function maxSeverityFromRequirements(array $requirements): RequirementSeverity { + RequirementSeverity::convertLegacyIntSeveritiesToEnums($requirements, __METHOD__); + return array_reduce( + $requirements, + function (RequirementSeverity $severity, $requirement) { + $requirementSeverity = $requirement['severity'] ?? RequirementSeverity::OK; + return RequirementSeverity::from(max($severity->value, $requirementSeverity->value)); + }, + RequirementSeverity::OK + ); + } + + /** + * Converts legacy int value severities to enums. + * + * @param array<string, array{'title': \Drupal\Core\StringTranslation\TranslatableMarkup, 'value': mixed, description: \Drupal\Core\StringTranslation\TranslatableMarkup, 'severity': \Drupal\Core\Extension\Requirement\RequirementSeverity}> $requirements + * An array of requirements, in the same format as is returned by + * hook_requirements(), hook_runtime_requirements(), + * hook_update_requirements(), and + * \Drupal\Core\Extension\InstallRequirementsInterface. + * @param string $deprecationMethod + * The method name to pass to the deprecation message. + * + * @see \Drupal\Core\Extension\InstallRequirementsInterface::getRequirements() + * @see \hook_requirements() + * @see \hook_runtime_requirements() + * @see \hook_update_requirements() + */ + public static function convertLegacyIntSeveritiesToEnums(array &$requirements, string $deprecationMethod): void { + foreach ($requirements as &$requirement) { + if (isset($requirement['severity'])) { + $severity = $requirement['severity']; + if (!$severity instanceof RequirementSeverity) { + @trigger_error("Calling {$deprecationMethod}() with an array of \$requirements with 'severity' with values not of type " . RequirementSeverity::class . " enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939", \E_USER_DEPRECATED); + $requirement['severity'] = RequirementSeverity::from($requirement['severity']); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php index 364d672c12fc..172193ca8554 100644 --- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php +++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Extension; +use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface; use Drupal\Component\Utility\Html; use Drupal\Core\Asset\AssetCollectionOptimizerInterface; use Drupal\Core\Config\ConfigFactoryInterface; @@ -22,100 +23,25 @@ class ThemeInstaller implements ThemeInstallerInterface { use ModuleDependencyMessageTrait; use StringTranslationTrait; - /** - * @var \Drupal\Core\Extension\ThemeHandlerInterface - */ - protected $themeHandler; - - /** - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * @var \Drupal\Core\Config\ConfigInstallerInterface - */ - protected $configInstaller; - - /** - * @var \Drupal\Core\Extension\ModuleHandlerInterface - */ - protected $moduleHandler; - - /** - * @var \Drupal\Core\State\StateInterface - */ - protected $state; - - /** - * @var \Drupal\Core\Config\ConfigManagerInterface - */ - protected $configManager; - - /** - * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface - */ - protected $cssCollectionOptimizer; - - /** - * @var \Drupal\Core\Routing\RouteBuilderInterface - */ - protected $routeBuilder; - - /** - * @var \Psr\Log\LoggerInterface - */ - protected $logger; - - /** - * The module extension list. - * - * @var \Drupal\Core\Extension\ModuleExtensionList - */ - protected $moduleExtensionList; - - /** - * Constructs a new ThemeInstaller. - * - * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler - * The theme handler. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory to get the installed themes. - * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer - * (optional) The config installer to install configuration. This optional - * to allow the theme handler to work before Drupal is installed and has a - * database. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler to fire themes_installed/themes_uninstalled hooks. - * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager - * The config manager used to uninstall a theme. - * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer - * The CSS asset collection optimizer service. - * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder - * (optional) The route builder service to rebuild the routes if a theme is - * installed. - * @param \Psr\Log\LoggerInterface $logger - * A logger instance. - * @param \Drupal\Core\State\StateInterface $state - * The state store. - * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list - * The module extension list. - * @param \Drupal\Core\Theme\Registry|null $themeRegistry - * The theme registry. - * @param \Drupal\Core\Extension\ThemeExtensionList|null $themeExtensionList - * The theme extension list. - */ - public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state, ModuleExtensionList $module_extension_list, protected Registry $themeRegistry, protected ThemeExtensionList $themeExtensionList) { - $this->themeHandler = $theme_handler; - $this->configFactory = $config_factory; - $this->configInstaller = $config_installer; - $this->moduleHandler = $module_handler; - $this->configManager = $config_manager; - $this->cssCollectionOptimizer = $css_collection_optimizer; - $this->routeBuilder = $route_builder; - $this->logger = $logger; - $this->state = $state; - $this->moduleExtensionList = $module_extension_list; + public function __construct( + protected readonly ThemeHandlerInterface $themeHandler, + protected readonly ConfigFactoryInterface $configFactory, + protected readonly ConfigInstallerInterface $configInstaller, + protected readonly ModuleHandlerInterface $moduleHandler, + protected readonly ConfigManagerInterface $configManager, + protected readonly AssetCollectionOptimizerInterface $cssCollectionOptimizer, + protected readonly RouteBuilderInterface $routeBuilder, + protected readonly LoggerInterface $logger, + protected readonly StateInterface $state, + protected readonly ModuleExtensionList $moduleExtensionList, + protected readonly Registry $themeRegistry, + protected readonly ThemeExtensionList $themeExtensionList, + protected ?CachedDiscoveryInterface $componentPluginManager = NULL, + ) { + if ($this->componentPluginManager === NULL) { + @trigger_error('Calling ' . __METHOD__ . ' without the $componentPluginManager argument is deprecated in drupal:11.2.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3525649', E_USER_DEPRECATED); + $this->componentPluginManager = \Drupal::service('plugin.manager.sdc'); + } } /** @@ -311,11 +237,9 @@ class ThemeInstaller implements ThemeInstallerInterface { * Resets some other systems like rebuilding the route information or caches. */ protected function resetSystem() { - if ($this->routeBuilder) { - $this->routeBuilder->setRebuildNeeded(); - } - + $this->routeBuilder->setRebuildNeeded(); $this->themeRegistry->reset(); + $this->componentPluginManager->clearCachedDefinitions(); } } diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 40cd1824106d..4d8d0a863a34 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -6,6 +6,7 @@ */ use Drupal\Core\Database\Database; +use Drupal\Core\Extension\Requirement\RequirementSeverity; use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\Core\Utility\UpdateException; @@ -1097,8 +1098,9 @@ function hook_updater_info_alter(&$updaters) { * Drupal itself (by install.php) with an installation profile or later by hand. * As a consequence, install-time requirements must be checked without access * to the full Drupal API, because it is not available during install.php. - * If a requirement has a severity of REQUIREMENT_ERROR, install.php will abort - * or at least the module will not install. + * If a requirement has a severity of + * \Drupal\Core\Extension\Requirement\RequirementSeverity::Error, install.php + * will abort or at least the module will not install. * Other severity levels have no effect on the installation. * Module dependencies do not belong to these installation requirements, * but should be defined in the module's .info.yml file. @@ -1111,8 +1113,9 @@ function hook_updater_info_alter(&$updaters) { * tasks and security issues. * The returned 'requirements' will be listed on the status report in the * administration section, with indication of the severity level. - * Moreover, any requirement with a severity of REQUIREMENT_ERROR severity will - * result in a notice on the administration configuration page. + * Moreover, any requirement with a severity of + * \Drupal\Core\Extension\Requirement\RequirementSeverity::Error will result in + * a notice on the administration configuration page. * * @param string $phase * The phase in which requirements are checked: @@ -1121,7 +1124,7 @@ function hook_updater_info_alter(&$updaters) { * - runtime: The runtime requirements are being checked and shown on the * status report page. * - * @return array + * @return array<string, array{'title': \Drupal\Core\StringTranslation\TranslatableMarkup, 'value': mixed, description: \Drupal\Core\StringTranslation\TranslatableMarkup, 'severity': \Drupal\Core\Extension\Requirement\RequirementSeverity}> * An associative array where the keys are arbitrary but must be unique (it * is suggested to use the module short name as a prefix) and the values are * themselves associative arrays with the following elements: @@ -1130,12 +1133,9 @@ function hook_updater_info_alter(&$updaters) { * install phase, this should only be used for version numbers, do not set * it if not applicable. * - description: The description of the requirement/status. - * - severity: (optional) The requirement's result/severity level, one of: - * - REQUIREMENT_INFO: For info only. - * - REQUIREMENT_OK: The requirement is satisfied. - * - REQUIREMENT_WARNING: The requirement failed with a warning. * - REQUIREMENT_ERROR: The requirement failed with an error. - * Defaults to REQUIREMENT_OK when installing, REQUIREMENT_INFO otherwise. + * - severity: The requirement's severity. Defaults to RequirementSeverity::OK + * when installing, or RequirementSeverity::Info otherwise. */ function hook_requirements($phase): array { $requirements = []; @@ -1145,7 +1145,7 @@ function hook_requirements($phase): array { $requirements['drupal'] = [ 'title' => t('Drupal'), 'value' => \Drupal::VERSION, - 'severity' => REQUIREMENT_INFO, + 'severity' => RequirementSeverity::Info, ]; } @@ -1156,7 +1156,7 @@ function hook_requirements($phase): array { ]; if (version_compare(phpversion(), \Drupal::MINIMUM_PHP) < 0) { $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version.', ['%version' => \Drupal::MINIMUM_PHP]); - $requirements['php']['severity'] = REQUIREMENT_ERROR; + $requirements['php']['severity'] = RequirementSeverity::Error; } // Report cron status @@ -1169,7 +1169,7 @@ function hook_requirements($phase): array { else { $requirements['cron'] = [ 'description' => t('Cron has not run. It appears cron jobs have not been setup on your system. Check the help pages for <a href=":url">configuring cron jobs</a>.', [':url' => 'https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview']), - 'severity' => REQUIREMENT_ERROR, + 'severity' => RequirementSeverity::Error, 'value' => t('Never run'), ]; } @@ -1199,7 +1199,7 @@ function hook_requirements_alter(array &$requirements): void { $requirements['php']['title'] = t('PHP version'); // Decrease the 'update status' requirement severity from warning to info. - $requirements['update status']['severity'] = REQUIREMENT_INFO; + $requirements['update status']['severity'] = RequirementSeverity::Info; // Remove a requirements entry. unset($requirements['foo']); @@ -1216,8 +1216,9 @@ function hook_requirements_alter(array &$requirements): void { * general status information like maintenance tasks and security issues. * The returned requirements will be listed on the status report in the * administration section, with an indication of the severity level. - * Moreover, any requirement with a severity of REQUIREMENT_ERROR will result in - * a notice on the 'Configuration' administration page (/admin/config). + * Moreover, any requirement with severity of RequirementSeverity::Error will + * result in a notice on the 'Configuration' administration page + * (/admin/config). * * @return array * An associative array where the keys are arbitrary but must be unique (it @@ -1226,12 +1227,9 @@ function hook_requirements_alter(array &$requirements): void { * - title: The name of the requirement. * - value: The current value (e.g., version, time, level, etc). * - description: The description of the requirement/status. - * - severity: (optional) The requirement's severity level, one of: - * - REQUIREMENT_INFO: For info only. - * - REQUIREMENT_OK: The requirement is satisfied. - * - REQUIREMENT_WARNING: The requirement failed with a warning. - * - REQUIREMENT_ERROR: The requirement failed with an error. - * Defaults to REQUIREMENT_OK. + * - severity: (optional) An instance of + * \Drupal\Core\Extension\Requirement\RequirementSeverity enum. Defaults to + * RequirementSeverity::OK. */ function hook_runtime_requirements(): array { $requirements = []; @@ -1240,7 +1238,7 @@ function hook_runtime_requirements(): array { $requirements['drupal'] = [ 'title' => t('Drupal'), 'value' => \Drupal::VERSION, - 'severity' => REQUIREMENT_INFO, + 'severity' => RequirementSeverity::Info, ]; // Test PHP version @@ -1250,7 +1248,7 @@ function hook_runtime_requirements(): array { ]; if (version_compare(phpversion(), \Drupal::MINIMUM_PHP) < 0) { $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version.', ['%version' => \Drupal::MINIMUM_PHP]); - $requirements['php']['severity'] = REQUIREMENT_ERROR; + $requirements['php']['severity'] = RequirementSeverity::Error; } // Report cron status @@ -1263,7 +1261,7 @@ function hook_runtime_requirements(): array { else { $requirements['cron']['description'] = t('Cron has not run. It appears cron jobs have not been setup on your system. Check the help pages for <a href=":url">configuring cron jobs</a>.', [':url' => 'https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview']); $requirements['cron']['value'] = t('Never run'); - $requirements['cron']['severity'] = REQUIREMENT_ERROR; + $requirements['cron']['severity'] = RequirementSeverity::Error; } $requirements['cron']['description'] .= ' ' . t('You can <a href=":cron">run cron manually</a>.', [':cron' => Url::fromRoute('system.run_cron')->toString()]); @@ -1287,7 +1285,7 @@ function hook_runtime_requirements_alter(array &$requirements): void { $requirements['php']['title'] = t('PHP version'); // Decrease the 'update status' requirement severity from warning to info. - $requirements['update status']['severity'] = REQUIREMENT_INFO; + $requirements['update status']['severity'] = RequirementSeverity::Info; // Remove a requirements entry. unset($requirements['foo']); @@ -1306,13 +1304,9 @@ function hook_runtime_requirements_alter(array &$requirements): void { * - title: The name of the requirement. * - value: The current value (e.g., version, time, level, etc). * - description: The description of the requirement/status. - * - severity: (optional) The requirement's result/severity level, one of: - * - REQUIREMENT_INFO: Has no effect during updates. - * - REQUIREMENT_OK: Has no effect during updates. - * - REQUIREMENT_WARNING: Displays a warning, user can choose to continue. - * - REQUIREMENT_ERROR: Displays an error message, user cannot continue - * until the problem is resolved. - * Defaults to REQUIREMENT_OK. + * - severity: (optional) An instance of + * \Drupal\Core\Extension\Requirement\RequirementSeverity enum. Defaults to + * RequirementSeverity::OK. */ function hook_update_requirements() { $requirements = []; @@ -1324,7 +1318,7 @@ function hook_update_requirements() { ]; if (version_compare(phpversion(), \Drupal::MINIMUM_PHP) < 0) { $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version.', ['%version' => \Drupal::MINIMUM_PHP]); - $requirements['php']['severity'] = REQUIREMENT_ERROR; + $requirements['php']['severity'] = RequirementSeverity::Error; } return $requirements; @@ -1347,7 +1341,7 @@ function hook_update_requirements_alter(array &$requirements): void { $requirements['php']['title'] = t('PHP version'); // Decrease the 'update status' requirement severity from warning to info. - $requirements['update status']['severity'] = REQUIREMENT_INFO; + $requirements['update status']['severity'] = RequirementSeverity::Info; // Remove a requirements entry. unset($requirements['foo']); diff --git a/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php b/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php deleted file mode 100644 index 158010463d2a..000000000000 --- a/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Core\Hook\Attribute; - -use Drupal\Core\Hook\Order\OrderInterface; - -/** - * Hook attribute for FormAlter. - * - * @see hook_form_alter(). - */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class FormAlter extends Hook { - - /** - * {@inheritdoc} - */ - public const string PREFIX = 'form'; - - /** - * {@inheritdoc} - */ - public const string SUFFIX = 'alter'; - - /** - * Constructs a FormAlter attribute object. - * - * @param string $form_id - * (optional) The ID of the form that this implementation alters. - * If this is left blank then `form_alter` is the hook that is registered. - * @param string $method - * (optional) The method name. If this attribute is on a method, this - * parameter is not required. If this attribute is on a class and this - * parameter is omitted, the class must have an __invoke() method, which is - * taken as the hook implementation. - * @param string|null $module - * (optional) The module this implementation is for. This allows one module - * to implement a hook on behalf of another module. Defaults to the module - * the implementation is in. - * @param \Drupal\Core\Hook\Order\OrderInterface|null $order - * (optional) Set the order of the implementation. - */ - public function __construct( - string $form_id = '', - public string $method = '', - public ?string $module = NULL, - public ?OrderInterface $order = NULL, - ) { - parent::__construct($form_id, $method, $module, $order); - } - -} diff --git a/core/lib/Drupal/Core/Hook/Attribute/Hook.php b/core/lib/Drupal/Core/Hook/Attribute/Hook.php index 34dbc8ebf916..0084e651180d 100644 --- a/core/lib/Drupal/Core/Hook/Attribute/Hook.php +++ b/core/lib/Drupal/Core/Hook/Attribute/Hook.php @@ -98,27 +98,10 @@ use Drupal\Core\Hook\Order\OrderInterface; class Hook implements HookAttributeInterface { /** - * The hook prefix such as `form`. - * - * @var string - */ - public const string PREFIX = ''; - - /** - * The hook suffix such as `alter`. - * - * @var string - */ - public const string SUFFIX = ''; - - /** * Constructs a Hook attribute object. * * @param string $hook * The short hook name, without the 'hook_' prefix. - * $hook is only optional when Hook is extended and a PREFIX or SUFFIX is - * defined. When using the [#Hook] attribute directly $hook is required. - * See Drupal\Core\Hook\Attribute\Preprocess. * @param string $method * (optional) The method name. If this attribute is on a method, this * parameter is not required. If this attribute is on a class and this @@ -132,15 +115,10 @@ class Hook implements HookAttributeInterface { * (optional) Set the order of the implementation. */ public function __construct( - public string $hook = '', + public string $hook, public string $method = '', public ?string $module = NULL, public ?OrderInterface $order = NULL, - ) { - $this->hook = implode('_', array_filter([static::PREFIX, $hook, static::SUFFIX])); - if ($this->hook === '') { - throw new \LogicException('The Hook attribute or an attribute extending the Hook attribute must provide the $hook parameter, a PREFIX or a SUFFIX.'); - } - } + ) {} } diff --git a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php b/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php deleted file mode 100644 index 47642859a20b..000000000000 --- a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Core\Hook\Attribute; - -/** - * Attribute for defining a class method as a preprocess function. - * - * Pass no arguments for hook_preprocess `#[Preprocess]`. - * For `hook_preprocess_HOOK` pass the `HOOK` without the `hook_preprocess` - * portion `#[Preprocess('HOOK')]`. - * - * See \Drupal\Core\Hook\Attribute\Hook for additional information. - */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class Preprocess extends Hook { - /** - * {@inheritdoc} - */ - public const string PREFIX = 'preprocess'; - -} diff --git a/core/lib/Drupal/Core/Mailer/Transport/SendmailCommandValidationTransportFactory.php b/core/lib/Drupal/Core/Mailer/Transport/SendmailCommandValidationTransportFactory.php new file mode 100644 index 000000000000..84dfb64c7b28 --- /dev/null +++ b/core/lib/Drupal/Core/Mailer/Transport/SendmailCommandValidationTransportFactory.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Mailer\Transport; + +use Drupal\Core\Site\Settings; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * Command validation decorator for sendmail transport factory. + */ +class SendmailCommandValidationTransportFactory implements TransportFactoryInterface { + + /** + * Construct command validation decorator for sendmail transport factory. + * + * @param \Symfony\Component\Mailer\Transport\TransportFactoryInterface $inner + * The decorated sendmail transport factory. + */ + public function __construct( + #[AutowireDecorated] + protected TransportFactoryInterface $inner, + ) { + } + + /** + * {@inheritdoc} + */ + public function create(Dsn $dsn): TransportInterface { + $command = $dsn->getOption('command'); + if (!empty($command)) { + $commands = Settings::get('mailer_sendmail_commands', []); + if (!in_array($command, $commands, TRUE)) { + throw new \RuntimeException("Unsafe sendmail command {$command}"); + } + } + + return $this->inner->create($dsn); + } + + /** + * {@inheritdoc} + */ + public function supports(Dsn $dsn): bool { + return $this->inner->supports($dsn); + } + +} diff --git a/core/lib/Drupal/Core/Mailer/TransportServiceFactory.php b/core/lib/Drupal/Core/Mailer/TransportServiceFactory.php new file mode 100644 index 000000000000..8950d44e3648 --- /dev/null +++ b/core/lib/Drupal/Core/Mailer/TransportServiceFactory.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Mailer; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * The default mailer transport service factory. + */ +class TransportServiceFactory implements TransportServiceFactoryInterface { + + use TransportServiceFactoryTrait; + + /** + * Constructs a new transport service factory. + * + * @param Iterable<TransportFactoryInterface> $factories + * A list of transport factories. + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory service. + */ + public function __construct( + #[AutowireIterator(tag: 'mailer.transport_factory')] + iterable $factories, + protected ConfigFactoryInterface $configFactory, + ) { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + */ + public function createTransport(): TransportInterface { + $dsn = $this->configFactory->get('system.mail')->get('mailer_dsn'); + $dsnObject = new Dsn(...$dsn); + return $this->fromDsnObject($dsnObject); + } + +} diff --git a/core/lib/Drupal/Core/Mailer/TransportServiceFactoryInterface.php b/core/lib/Drupal/Core/Mailer/TransportServiceFactoryInterface.php new file mode 100644 index 000000000000..8a2b5368db05 --- /dev/null +++ b/core/lib/Drupal/Core/Mailer/TransportServiceFactoryInterface.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Mailer; + +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * An interface defining mailer transport service factory implementations. + * + * The transport service factory is responsible to create a transport instance + * according to the site configuration. The default service factory looks up the + * `mailer_dsn` key from the `system.mail` config and returns an appropriate + * transport implementation. + * + * Contrib and custom code may choose to replace or decorate the transport + * service factory in order to provide a mailer transport instance which + * requires more complex setup. + */ +interface TransportServiceFactoryInterface { + + /** + * Creates and returns a configured mailer transport class. + */ + public function createTransport(): TransportInterface; + +} diff --git a/core/lib/Drupal/Core/Mailer/TransportServiceFactoryTrait.php b/core/lib/Drupal/Core/Mailer/TransportServiceFactoryTrait.php new file mode 100644 index 000000000000..c4aa2c736a4e --- /dev/null +++ b/core/lib/Drupal/Core/Mailer/TransportServiceFactoryTrait.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Mailer; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * A trait containing helper methods for transport service construction. + */ +trait TransportServiceFactoryTrait { + + /** + * A list of transport factories. + * + * @var Iterable<TransportFactoryInterface> + */ + protected iterable $factories; + + /** + * Constructs a transport instance given a DSN object. + * + * @param \Symfony\Component\Mailer\Transport\Dsn $dsn + * The mailer DSN object. + * + * @throws \Symfony\Component\Mailer\Exception\IncompleteDsnException + * @throws \Symfony\Component\Mailer\Exception\UnsupportedSchemeException + */ + protected function fromDsnObject(Dsn $dsn): TransportInterface { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return $factory->create($dsn); + } + } + + throw new UnsupportedSchemeException($dsn); + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php index 31de8a4485cf..0cbfa25c850c 100644 --- a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php +++ b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php @@ -5,6 +5,7 @@ namespace Drupal\Core\Menu; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheCollector; use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Path\PathMatcherInterface; use Drupal\Core\Routing\RouteMatchInterface; /** @@ -40,11 +41,23 @@ class MenuActiveTrail extends CacheCollector implements MenuActiveTrailInterface * The cache backend. * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock backend. + * @param \Drupal\Core\Path\PathMatcherInterface|null $pathMatcher + * The path.matcher service. */ - public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match, CacheBackendInterface $cache, LockBackendInterface $lock) { + public function __construct( + MenuLinkManagerInterface $menu_link_manager, + RouteMatchInterface $route_match, + CacheBackendInterface $cache, + LockBackendInterface $lock, + protected ?PathMatcherInterface $pathMatcher = NULL, + ) { parent::__construct(NULL, $cache, $lock); $this->menuLinkManager = $menu_link_manager; $this->routeMatch = $route_match; + if ($this->pathMatcher === NULL) { + @trigger_error('Calling ' . __METHOD__ . ' without the $pathMatcher argument is deprecated in drupal:11.2.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3523495', E_USER_DEPRECATED); + $this->pathMatcher = \Drupal::service('path.matcher'); + } } /** @@ -129,6 +142,7 @@ class MenuActiveTrail extends CacheCollector implements MenuActiveTrailInterface // The menu links coming from the storage are already sorted by depth, // weight and ID. $found = NULL; + $links = []; $route_name = $this->routeMatch->getRouteName(); // On a default (not custom) 403 page the route name is NULL. On a custom @@ -139,10 +153,20 @@ class MenuActiveTrail extends CacheCollector implements MenuActiveTrailInterface // Load links matching this route. $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name); - // Select the first matching link. - if ($links) { - $found = reset($links); - } + } + + // If the request is for the site's front page, then menu links containing + // <front> must also be loaded since it's a special route that's an alias + // for the page. + // @todo Combine the two calls to loadLinksByRoute() in + // https://www.drupal.org/project/drupal/issues/3523497 + if ($this->pathMatcher->isFrontPage()) { + $links = array_merge($links, $this->menuLinkManager->loadLinksByRoute('<front>', [], $menu_name)); + } + + // Select the first matching link. + if ($links) { + $found = reset($links); } return $found; } diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php index 8410e5af3f7d..9322c3628c61 100644 --- a/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php @@ -23,16 +23,16 @@ interface MenuActiveTrailInterface { public function getActiveTrailIds($menu_name); /** - * Fetches a menu link which matches the route name, parameters and menu name. + * Fetches a menu link that matches the currently active route. * * @param string|null $menu_name * (optional) The menu within which to find the active link. If omitted, all * menus will be searched. * * @return \Drupal\Core\Menu\MenuLinkInterface|null - * The menu link for the given route name, parameters and menu, or NULL if - * there is no matching menu link or the current user cannot access the - * current page (i.e. we have a 403 response). + * The menu link for the currently active route, or NULL if there is no + * matching menu link or the current user cannot access the current page + * (i.e. we have a 403 response). */ public function getActiveLink($menu_name = NULL); diff --git a/core/lib/Drupal/Core/ProxyClass/Cron.php b/core/lib/Drupal/Core/ProxyClass/Cron.php deleted file mode 100644 index 640b9d030c53..000000000000 --- a/core/lib/Drupal/Core/ProxyClass/Cron.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php -// phpcs:ignoreFile - -/** - * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Cron' "core/lib/Drupal/Core". - */ - -namespace Drupal\Core\ProxyClass { - - /** - * Provides a proxy class for \Drupal\Core\Cron. - * - * @see \Drupal\Component\ProxyBuilder - */ - class Cron implements \Drupal\Core\CronInterface - { - - use \Drupal\Core\DependencyInjection\DependencySerializationTrait; - - /** - * The id of the original proxied service. - * - * @var string - */ - protected $drupalProxyOriginalServiceId; - - /** - * The real proxied service, after it was lazy loaded. - * - * @var \Drupal\Core\Cron - */ - protected $service; - - /** - * The service container. - * - * @var \Symfony\Component\DependencyInjection\ContainerInterface - */ - protected $container; - - /** - * Constructs a ProxyClass Drupal proxy object. - * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * The container. - * @param string $drupal_proxy_original_service_id - * The service ID of the original service. - */ - public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id) - { - $this->container = $container; - $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; - } - - /** - * Lazy loads the real service from the container. - * - * @return object - * Returns the constructed real service. - */ - protected function lazyLoadItself() - { - if (!isset($this->service)) { - $this->service = $this->container->get($this->drupalProxyOriginalServiceId); - } - - return $this->service; - } - - /** - * {@inheritdoc} - */ - public function run() - { - return $this->lazyLoadItself()->run(); - } - - } - -} diff --git a/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php b/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php index f1db3a342af1..9feb9bed8da7 100644 --- a/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php +++ b/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php @@ -93,11 +93,14 @@ final class ConsoleInputCollector implements InputCollectorInterface { $method = $settings['method']; $arguments = $settings['arguments'] ?? []; - // Most of the input-collecting methods of StyleInterface have a `default` - // parameter. - $arguments += [ - 'default' => $default_value, - ]; + if ($method !== 'askHidden') { + // Most of the input-collecting methods of StyleInterface have a `default` + // parameter. + $arguments += [ + 'default' => $default_value, + ]; + } + // We don't support using Symfony Console's inline validation; instead, // input definitions should define constraints. unset($arguments['validator']); diff --git a/core/lib/Drupal/Core/Render/Element/ComponentElement.php b/core/lib/Drupal/Core/Render/Element/ComponentElement.php index 70e42154cbb9..62db902c0681 100644 --- a/core/lib/Drupal/Core/Render/Element/ComponentElement.php +++ b/core/lib/Drupal/Core/Render/Element/ComponentElement.php @@ -67,6 +67,14 @@ class ComponentElement extends RenderElementBase { ), $props ); + + // Handle children as slots. + $children = Element::children($element, TRUE); + foreach ($children as $key) { + $element['#slots'][$key] = $element[$key]; + unset($element[$key]); + } + $inline_template = $this->generateComponentTemplate( $element['#component'], $element['#slots'], diff --git a/core/lib/Drupal/Core/Render/Element/RenderElementBase.php b/core/lib/Drupal/Core/Render/Element/RenderElementBase.php index 451df8a0e369..5ff3a5a0f98e 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElementBase.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElementBase.php @@ -38,7 +38,10 @@ use Drupal\Core\Url; * * Here is the list of the properties used during the rendering of all render * elements: - * - #access: (bool) Whether the element is accessible or not. When FALSE, + * - #access: (bool or AccessResultInterface) + * Whether the element is accessible or not. + * When the value is FALSE (if boolean) + * or the isAllowed() method returns FALSE (if AccessResultInterface), * the element is not rendered and user-submitted values are not taken * into consideration. * - #access_callback: A callable or function name to call to check access. diff --git a/core/lib/Drupal/Core/Render/Element/StatusReport.php b/core/lib/Drupal/Core/Render/Element/StatusReport.php index 41fc7d7fda11..057e314d24e0 100644 --- a/core/lib/Drupal/Core/Render/Element/StatusReport.php +++ b/core/lib/Drupal/Core/Render/Element/StatusReport.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Render\Element; +use Drupal\Core\Extension\Requirement\RequirementSeverity; use Drupal\Core\Render\Attribute\RenderElement; /** @@ -34,21 +35,21 @@ class StatusReport extends RenderElementBase { * This function is assigned as a #pre_render callback. */ public static function preRenderGroupRequirements($element) { - $severities = static::getSeverities(); $grouped_requirements = []; + RequirementSeverity::convertLegacyIntSeveritiesToEnums($element['#requirements'], __METHOD__); + /** @var array{title: \Drupal\Core\StringTranslation\TranslatableMarkup, value: mixed, description: \Drupal\Core\StringTranslation\TranslatableMarkup, severity: \Drupal\Core\Extension\Requirement\RequirementSeverity} $requirement */ foreach ($element['#requirements'] as $key => $requirement) { - $severity = $severities[REQUIREMENT_INFO]; + $severity = RequirementSeverity::Info; if (isset($requirement['severity'])) { - $requirement_severity = (int) $requirement['severity'] === REQUIREMENT_OK ? REQUIREMENT_INFO : (int) $requirement['severity']; - $severity = $severities[$requirement_severity]; + $severity = $requirement['severity'] === RequirementSeverity::OK ? RequirementSeverity::Info : $requirement['severity']; } elseif (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install') { - $severity = $severities[REQUIREMENT_OK]; + $severity = RequirementSeverity::OK; } - $grouped_requirements[$severity['status']]['title'] = $severity['title']; - $grouped_requirements[$severity['status']]['type'] = $severity['status']; - $grouped_requirements[$severity['status']]['items'][$key] = $requirement; + $grouped_requirements[$severity->status()]['title'] = $severity->title(); + $grouped_requirements[$severity->status()]['type'] = $severity->status(); + $grouped_requirements[$severity->status()]['items'][$key] = $requirement; } // Order the grouped requirements by a set order. @@ -68,22 +69,28 @@ class StatusReport extends RenderElementBase { * @return array * An associative array of the requirements severities. The keys are the * requirement constants defined in install.inc. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3410939 */ public static function getSeverities() { + @trigger_error('Calling ' . __METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3410939', \E_USER_DEPRECATED); return [ - REQUIREMENT_INFO => [ + RequirementSeverity::Info->value => [ 'title' => t('Checked', [], ['context' => 'Examined']), 'status' => 'checked', ], - REQUIREMENT_OK => [ + RequirementSeverity::OK->value => [ 'title' => t('OK'), 'status' => 'ok', ], - REQUIREMENT_WARNING => [ + RequirementSeverity::Warning->value => [ 'title' => t('Warnings found'), 'status' => 'warning', ], - REQUIREMENT_ERROR => [ + RequirementSeverity::Error->value => [ 'title' => t('Errors found'), 'status' => 'error', ], diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 8fb473aa8df2..1f26381bb181 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -8,6 +8,7 @@ use Drupal\Component\Utility\Variable; use Drupal\Component\Utility\Xss; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Form\FormHelper; use Drupal\Core\Render\Element\RenderCallbackInterface; @@ -96,6 +97,9 @@ class Renderer implements RendererInterface { * {@inheritdoc} */ public function renderRoot(&$elements) { + if (!$elements) { + return ''; + } // Disallow calling ::renderRoot() from within another ::renderRoot() call. if ($this->isRenderingRoot) { $this->isRenderingRoot = FALSE; @@ -104,21 +108,39 @@ class Renderer implements RendererInterface { // Render in its own render context. $this->isRenderingRoot = TRUE; - $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { - return $this->render($elements, TRUE); - }); - $this->isRenderingRoot = FALSE; + try { + $output = $this->renderInIsolation($elements); + } + // Since #pre_render, #post_render, #lazy_builder callbacks and theme + // functions or templates may be used for generating a render array's + // content, and we might be rendering the main content for the page, it is + // possible that any of them throw an exception that will cause a different + // page to be rendered (e.g. throwing + // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause + // the 404 page to be rendered). That page might also use + // Renderer::renderRoot() but if exceptions aren't caught here, it will be + // impossible to call Renderer::renderRoot() again. + // Hence, catch all exceptions, reset the isRenderingRoot property and + // re-throw exceptions. + finally { + $this->isRenderingRoot = FALSE; + } + + if ((string) $output === '') { + return ''; + } - return $output; + return $elements['#markup']; } /** * {@inheritdoc} */ public function renderInIsolation(&$elements) { - return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { - return $this->render($elements, TRUE); - }); + $context = new RenderContext(); + return Markup::create($this->executeInRenderContext($context, function () use (&$elements, $context) { + return $this->doRenderRoot($elements, $context); + })); } /** @@ -188,31 +210,60 @@ class Renderer implements RendererInterface { * {@inheritdoc} */ public function render(&$elements, $is_root_call = FALSE) { - // Since #pre_render, #post_render, #lazy_builder callbacks and theme - // functions or templates may be used for generating a render array's - // content, and we might be rendering the main content for the page, it is - // possible that any of them throw an exception that will cause a different - // page to be rendered (e.g. throwing - // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause - // the 404 page to be rendered). That page might also use - // Renderer::renderRoot() but if exceptions aren't caught here, it will be - // impossible to call Renderer::renderRoot() again. - // Hence, catch all exceptions, reset the isRenderingRoot property and - // re-throw exceptions. - try { - return $this->doRender($elements, $is_root_call); + $context = $this->getCurrentRenderContext(); + if (!isset($context)) { + throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); } - catch (\Exception $e) { - // Mark the ::rootRender() call finished due to this exception & re-throw. - $this->isRenderingRoot = FALSE; - throw $e; + + if ($is_root_call) { + trigger_error(__METHOD__ . ' with $is_root_call is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use ' . __CLASS__ . '::renderRoot() instead. See https://www.drupal.org/node/3497318.'); + return $this->doRenderRoot($elements, $context); + } + + return $this->doRender($elements, $context); + } + + /** + * See the docs for ::render(). + */ + protected function doRenderRoot(array &$elements, RenderContext $context): string|MarkupInterface { + if (!$elements) { + return ''; + } + // Set the bubbleable rendering metadata that has configurable defaults + // to ensure that the final render array definitely has these configurable + // defaults, even when no subtree is render cached. + $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; + + if (isset($elements['#cache']['contexts'])) { + $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } + else { + $elements['#cache']['contexts'] = $required_cache_contexts; + } + + // Render the elements normally. + $return = $this->doRender($elements, $context); + + // If there is no output, return early as placeholders can't make a + // difference. + if ((string) $return === '') { + return $return; + } + + // Only when rendering the root do placeholders have to be processed. If we + // were to replace them while rendering cacheable nested elements, their + // cacheable metadata would still bubble all the way up the render tree, + // effectively making the use of placeholders pointless. + $this->replacePlaceholders($elements); + + return $elements['#markup']; } /** * See the docs for ::render(). */ - protected function doRender(&$elements, $is_root_call = FALSE) { + protected function doRender(array &$elements, RenderContext $context): string|MarkupInterface { if (empty($elements)) { return ''; } @@ -233,10 +284,6 @@ class Renderer implements RendererInterface { $this->addCacheableDependency($elements, $elements['#access']); if (!$elements['#access']->isAllowed()) { // Abort, but bubble new cache metadata from the access result. - $context = $this->getCurrentRenderContext(); - if (!isset($context)) { - throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderInIsolation() call. Use renderInIsolation()/renderRoot() or #lazy_builder/#pre_render instead."); - } $context->push(new BubbleableMetadata()); $context->update($elements); $context->bubble(); @@ -259,12 +306,10 @@ class Renderer implements RendererInterface { } $context->push(new BubbleableMetadata()); - // Set the bubbleable rendering metadata that has configurable defaults, if: - // - this is the root call, to ensure that the final render array definitely - // has these configurable defaults, even when no subtree is render cached. - // - this is a render cacheable subtree, to ensure that the cached data has - // the configurable defaults (which may affect the ID and invalidation). - if ($is_root_call || isset($elements['#cache']['keys'])) { + // Set the bubbleable rendering metadata that has configurable defaults if + // this is a render cacheable subtree, to ensure that the cached data has + // the configurable defaults (which may affect the ID and invalidation). + if (isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); @@ -287,12 +332,6 @@ class Renderer implements RendererInterface { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; - // Only when we're in a root (non-recursive) Renderer::render() call, - // placeholders must be processed, to prevent breaking the render cache - // in case of nested elements with #cache set. - if ($is_root_call) { - $this->replacePlaceholders($elements); - } // Mark the element markup as safe if is it a string. if (is_string($elements['#markup'])) { $elements['#markup'] = Markup::create($elements['#markup']); @@ -464,7 +503,7 @@ class Renderer implements RendererInterface { // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { - $elements['#children'] .= $this->doRender($elements[$key]); + $elements['#children'] .= $this->doRender($elements[$key], $context); } $elements['#children'] = Markup::create($elements['#children']); } @@ -558,23 +597,6 @@ class Renderer implements RendererInterface { $context->update($elements); } - // Only when we're in a root (non-recursive) Renderer::render() call, - // placeholders must be processed, to prevent breaking the render cache in - // case of nested elements with #cache set. - // - // By running them here, we ensure that: - // - they run when #cache is disabled, - // - they run when #cache is enabled and there is a cache miss. - // Only the case of a cache hit when #cache is enabled, is not handled here, - // that is handled earlier in Renderer::render(). - if ($is_root_call) { - $this->replacePlaceholders($elements); - // @todo remove as part of https://www.drupal.org/node/2511330. - if ($context->count() !== 1) { - throw new \LogicException('A stray RendererInterface::render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); - } - } - // Rendering is finished, all necessary info collected! $context->bubble(); @@ -757,6 +779,9 @@ class Renderer implements RendererInterface { * {@inheritdoc} */ public function addCacheableDependency(array &$elements, $dependency) { + if (!$dependency instanceof CacheableDependencyInterface) { + @trigger_error(sprintf("Calling %s() with an object that doesn't implement %s is deprecated in drupal:11.3.0 and will throw an error in drupal:13.0.0. See https://www.drupal.org/node/3525389", __METHOD__, CacheableDependencyInterface::class), E_USER_DEPRECATED); + } $meta_a = CacheableMetadata::createFromRenderArray($elements); $meta_b = CacheableMetadata::createFromObject($dependency); $meta_a->merge($meta_b)->applyTo($elements); diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 92fa661f6b0f..020e594755fb 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -323,7 +323,7 @@ interface RendererInterface { * (Internal use only.) Whether this is a recursive call or not. See * ::renderRoot(). * - * @return \Drupal\Component\Render\MarkupInterface + * @return \Drupal\Component\Render\MarkupInterface|string * The rendered HTML. * * @throws \LogicException diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php index 0170626181c0..6927ba2ebbec 100644 --- a/core/lib/Drupal/Core/Session/SessionManager.php +++ b/core/lib/Drupal/Core/Session/SessionManager.php @@ -6,6 +6,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Database\Connection; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; /** @@ -162,6 +163,16 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter parent::save(); } + $allowedKeys = array_map( + fn (SessionBagInterface $bag) => $bag->getStorageKey(), + $this->bags + ); + $allowedKeys[] = $this->getMetadataBag()->getStorageKey(); + $deprecatedKeys = array_diff(array_keys($_SESSION), $allowedKeys); + if (count($deprecatedKeys) > 0) { + @trigger_error(sprintf('Storing values directly in $_SESSION is deprecated in drupal:11.2.0 and will become unsupported in drupal:12.0.0. Use $request->getSession()->set() instead. Affected keys: %s. See https://www.drupal.org/node/3518527', implode(", ", $deprecatedKeys)), E_USER_DEPRECATED); + } + $this->startedLazy = FALSE; } diff --git a/core/lib/Drupal/Core/Template/Loader/ComponentLoader.php b/core/lib/Drupal/Core/Template/Loader/ComponentLoader.php index d141d202ecb0..e3669f8f1450 100644 --- a/core/lib/Drupal/Core/Template/Loader/ComponentLoader.php +++ b/core/lib/Drupal/Core/Template/Loader/ComponentLoader.php @@ -122,13 +122,8 @@ class ComponentLoader implements LoaderInterface { catch (ComponentNotFoundException) { throw new LoaderError('Unable to find component'); } - // If any of the templates, or the component definition, are fresh. Then the - // component is fresh. $metadata_path = $component->getPluginDefinition()[YamlDirectoryDiscovery::FILE_KEY]; - if ($file_is_fresh($metadata_path)) { - return TRUE; - } - return $file_is_fresh($component->getTemplatePath()); + return $file_is_fresh($component->getTemplatePath()) && $file_is_fresh($metadata_path); } } diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index 6a4dcfcf8b02..00345c8b8461 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -599,7 +599,7 @@ trait FunctionalTestSetupTrait { ]; // If we only have one db driver available, we cannot set the driver. - if (count($this->getDatabaseTypes()) == 1) { + if (count(Database::getDriverList()->getInstallableList()) == 1) { unset($parameters['forms']['install_settings_form']['driver']); } return $parameters; @@ -730,27 +730,4 @@ trait FunctionalTestSetupTrait { $callbacks = []; } - /** - * Returns all supported database driver installer objects. - * - * This wraps DatabaseDriverList::getInstallableList() for use without a - * current container. - * - * @return \Drupal\Core\Database\Install\Tasks[] - * An array of available database driver installer objects. - */ - protected function getDatabaseTypes() { - if (isset($this->originalContainer) && $this->originalContainer) { - \Drupal::setContainer($this->originalContainer); - } - $database_types = []; - foreach (Database::getDriverList()->getInstallableList() as $name => $driver) { - $database_types[$name] = $driver->getInstallTasks(); - } - if (isset($this->originalContainer) && $this->originalContainer) { - \Drupal::unsetContainer(); - } - return $database_types; - } - } diff --git a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php index cb754e1afaa4..0c000d675c3c 100644 --- a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php +++ b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php @@ -95,13 +95,14 @@ class SimpletestTestRunResultsStorage implements TestRunResultsStorageInterface * {@inheritdoc} */ public function removeResults(TestRun $test_run): int { - $this->connection->startTransaction('delete_test_run'); + $transaction = $this->connection->startTransaction('delete_test_run'); $this->connection->delete('simpletest') ->condition('test_id', $test_run->id()) ->execute(); $count = $this->connection->delete('simpletest_test_id') ->condition('test_id', $test_run->id()) ->execute(); + $transaction->commitOrRelease(); return $count; } @@ -169,9 +170,10 @@ class SimpletestTestRunResultsStorage implements TestRunResultsStorageInterface */ public function cleanUp(): int { // Clear test results. - $this->connection->startTransaction('delete_simpletest'); + $transaction = $this->connection->startTransaction('delete_simpletest'); $this->connection->delete('simpletest')->execute(); $count = $this->connection->delete('simpletest_test_id')->execute(); + $transaction->commitOrRelease(); return $count; } diff --git a/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php b/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php index e19e759f173c..8cdb368e2e95 100644 --- a/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php +++ b/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php @@ -14,6 +14,11 @@ class ComponentMetadata { use StringTranslationTrait; /** + * The ID of the component, in the form of provider:machine_name. + */ + public readonly string $id; + + /** * The absolute path to the component directory. * * @var string @@ -115,6 +120,7 @@ class ComponentMetadata { if (str_starts_with($path, $app_root)) { $path = substr($path, strlen($app_root)); } + $this->id = $metadata_info['id']; $this->mandatorySchemas = $enforce_schemas; $this->path = $path; @@ -149,7 +155,7 @@ class ComponentMetadata { private function parseSchemaInfo(array $metadata_info): ?array { if (empty($metadata_info['props'])) { if ($this->mandatorySchemas) { - throw new InvalidComponentException(sprintf('The component "%s" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.', $metadata_info['id'])); + throw new InvalidComponentException(sprintf('The component "%s" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.', $this->id)); } $schema = NULL; } @@ -167,6 +173,12 @@ class ComponentMetadata { $schema_props = $metadata_info['props']; foreach ($schema_props['properties'] ?? [] as $name => $prop_schema) { $type = $prop_schema['type'] ?? ''; + if (isset($prop_schema['enum'], $prop_schema['meta:enum'])) { + $enum_keys_diff = array_diff($prop_schema['enum'], array_keys($prop_schema['meta:enum'])); + if (!empty($enum_keys_diff)) { + throw new InvalidComponentException(sprintf('The values for the %s prop enum in component %s must be defined in meta:enum.', $name, $this->id)); + } + } $schema['properties'][$name]['type'] = array_unique([ ...(array) $type, 'object', @@ -197,6 +209,14 @@ class ComponentMetadata { * The normalized value object. */ public function normalize(): array { + $meta = []; + if (!empty($this->schema['properties'])) { + foreach ($this->schema['properties'] as $prop_name => $prop_definition) { + if (!empty($prop_definition['meta:enum'])) { + $meta['properties'][$prop_name] = $this->getEnumOptions($prop_name); + } + } + } return [ 'path' => $this->path, 'machineName' => $this->machineName, @@ -204,7 +224,42 @@ class ComponentMetadata { 'name' => $this->name, 'group' => $this->group, 'variants' => $this->variants, + 'meta' => $meta, ]; } + /** + * Get translated options labels from enumeration. + * + * @param string $propertyName + * The enum property name. + * + * @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup> + * An array with enum options as keys and the (non-rendered) + * translated labels as values. + */ + public function getEnumOptions(string $propertyName): array { + $options = []; + if (isset($this->schema['properties'][$propertyName])) { + $prop_definition = $this->schema['properties'][$propertyName]; + if (!empty($prop_definition['enum'])) { + $translation_context = $prop_definition['x-translation-context'] ?? ''; + // We convert ['a', 'b'], into ['a' => t('a'), 'b' => t('b')]. + $options = array_combine( + $prop_definition['enum'], + // @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + array_map(fn($value) => $this->t($value, [], ['context' => $translation_context]), $prop_definition['enum']), + ); + if (!empty($prop_definition['meta:enum'])) { + foreach ($prop_definition['meta:enum'] as $enum_value => $enum_label) { + $options[$enum_value] = + // @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + $this->t($enum_label, [], ['context' => $translation_context]); + } + } + } + } + return $options; + } + } diff --git a/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php b/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php index 246f143d4e25..ff102b5170a9 100644 --- a/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php +++ b/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php @@ -199,7 +199,9 @@ class ComponentValidator { $errors = array_filter( $this->validator->getErrors(), function (array $error) use ($context): bool { - if (($error['constraint'] ?? '') !== 'type') { + // Support 5.0 ($error['constraint']) and 6.0 + // ($error['constraint']['name']) at the same time. + if (($error['constraint']['name'] ?? $error['constraint'] ?? '') !== 'type') { return TRUE; } return !Element::isRenderArray($context[$error['property']] ?? NULL); diff --git a/core/lib/Drupal/Core/Theme/ComponentPluginManager.php b/core/lib/Drupal/Core/Theme/ComponentPluginManager.php index a0c93317699f..5a3b62773ea5 100644 --- a/core/lib/Drupal/Core/Theme/ComponentPluginManager.php +++ b/core/lib/Drupal/Core/Theme/ComponentPluginManager.php @@ -164,6 +164,10 @@ class ComponentPluginManager extends DefaultPluginManager implements Categorizin public function clearCachedDefinitions(): void { parent::clearCachedDefinitions(); $this->componentNegotiator->clearCache(); + // When clearing cached definitions from theme install or uninstall, the + // container is not rebuilt. Unset discovery so it will be re-instantiated + // in getDiscovery() with the updated list of theme directories. + $this->discovery = NULL; } /** |