diff options
Diffstat (limited to 'core/lib')
211 files changed, 4293 insertions, 1255 deletions
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 63514b60eff..9b6dc876501 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 052542ecfca..5fb3e2e3a75 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 3575798a7c1..24977f8da9f 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/Datetime/DateTimePlus.php b/core/lib/Drupal/Component/Datetime/DateTimePlus.php index 4f95d6d8b66..90d9c300854 100644 --- a/core/lib/Drupal/Component/Datetime/DateTimePlus.php +++ b/core/lib/Drupal/Component/Datetime/DateTimePlus.php @@ -53,62 +53,6 @@ class DateTimePlus { const RFC7231 = 'D, d M Y H:i:s \G\M\T'; /** - * An array of possible date parts. - * - * @var string[] - */ - protected static $dateParts = [ - 'year', - 'month', - 'day', - 'hour', - 'minute', - 'second', - ]; - - /** - * The value of the time value passed to the constructor. - * - * @var string - */ - protected $inputTimeRaw = ''; - - /** - * The prepared time, without timezone, for this date. - * - * @var string - */ - protected $inputTimeAdjusted = ''; - - /** - * The value of the timezone passed to the constructor. - * - * @var string - */ - protected $inputTimeZoneRaw = ''; - - /** - * The prepared timezone object used to construct this date. - * - * @var string - */ - protected $inputTimeZoneAdjusted = ''; - - /** - * The value of the format passed to the constructor. - * - * @var string - */ - protected $inputFormatRaw = ''; - - /** - * The prepared format, if provided. - * - * @var string - */ - protected $inputFormatAdjusted = ''; - - /** * The value of the language code passed to the constructor. * * @var string|null diff --git a/core/lib/Drupal/Component/DependencyInjection/composer.json b/core/lib/Drupal/Component/DependencyInjection/composer.json index ccf16002da9..df9e6465181 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 b832a6c45b1..d78d1b980c0 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/Gettext/PoItem.php b/core/lib/Drupal/Component/Gettext/PoItem.php index 7cc32568ad2..89dee6cc085 100644 --- a/core/lib/Drupal/Component/Gettext/PoItem.php +++ b/core/lib/Drupal/Component/Gettext/PoItem.php @@ -271,7 +271,7 @@ class PoItem { private function formatSingular() { $output = ''; $output .= 'msgid ' . $this->formatString($this->source); - $output .= 'msgstr ' . (isset($this->translation) ? $this->formatString($this->translation) : '""'); + $output .= 'msgstr ' . (isset($this->translation) ? $this->formatString($this->translation) : '""' . "\n"); return $output; } diff --git a/core/lib/Drupal/Component/HttpFoundation/composer.json b/core/lib/Drupal/Component/HttpFoundation/composer.json index 283e0713ca5..85e62e4ef9f 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 e11003efcb2..86408c23390 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/PluginManagerBase.php b/core/lib/Drupal/Component/Plugin/PluginManagerBase.php index 3ce0076d786..9ec5d9b193f 100644 --- a/core/lib/Drupal/Component/Plugin/PluginManagerBase.php +++ b/core/lib/Drupal/Component/Plugin/PluginManagerBase.php @@ -120,13 +120,13 @@ abstract class PluginManagerBase implements PluginManagerInterface { * @param array $configuration * An array of configuration relevant to the plugin instance. * + * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn * @return string * The id of an existing plugin to use when the plugin does not exist. * * @throws \BadMethodCallException * If the method is not implemented in the concrete plugin manager class. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn protected function getFallbackPluginId($plugin_id, array $configuration = []) { throw new \BadMethodCallException(static::class . '::getFallbackPluginId() not implemented.'); } diff --git a/core/lib/Drupal/Component/Plugin/composer.json b/core/lib/Drupal/Component/Plugin/composer.json index 6a4f1fc7e2c..2c5a4864291 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/ProxyBuilder/ProxyBuilder.php b/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php index 1e9fca562f5..6d45c3a0acb 100644 --- a/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php +++ b/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php @@ -98,7 +98,7 @@ EOS; $output .= $this->buildUseStatements(); - // The actual class; + // The actual class. $properties = <<<'EOS' /** * The id of the original proxied service. @@ -324,7 +324,7 @@ EOS; $output .= " \\$class_name::$function_name("; } - // Add parameters; + // Add parameters. $parameters = []; foreach ($reflection_method->getParameters() as $parameter) { $parameters[] = '$' . $parameter->getName(); diff --git a/core/lib/Drupal/Component/Render/FormattableMarkup.php b/core/lib/Drupal/Component/Render/FormattableMarkup.php index 6db6288d47d..c6e5ebcb9dd 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 10068bba3ce..22325e14874 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/Component/Utility/DeprecationHelper.php b/core/lib/Drupal/Component/Utility/DeprecationHelper.php index f9a4aebba43..3d2e3d1d153 100644 --- a/core/lib/Drupal/Component/Utility/DeprecationHelper.php +++ b/core/lib/Drupal/Component/Utility/DeprecationHelper.php @@ -38,8 +38,8 @@ final class DeprecationHelper { */ public static function backwardsCompatibleCall(string $currentVersion, string $deprecatedVersion, callable $currentCallable, callable $deprecatedCallable): mixed { // Normalize the version string when it's a dev version to the first point - // release of that minor. E.g. 10.2.x-dev and 10.2-dev both translate to - // 10.2.0 + // release of that minor. E.g. "10.2.x-dev" and "10.2-dev" both translate to + // "10.2.0". $normalizedVersion = str_ends_with($currentVersion, '-dev') ? str_replace(['.x-dev', '-dev'], '.0', $currentVersion) : $currentVersion; return version_compare($normalizedVersion, $deprecatedVersion, '>=') ? $currentCallable() : $deprecatedCallable(); diff --git a/core/lib/Drupal/Component/Utility/Tags.php b/core/lib/Drupal/Component/Utility/Tags.php index f96667f85f0..317d00c4005 100644 --- a/core/lib/Drupal/Component/Utility/Tags.php +++ b/core/lib/Drupal/Component/Utility/Tags.php @@ -20,7 +20,7 @@ class Tags { */ public static function explode($tags) { // This regexp allows the following types of user input: - // this, "company, llc", "and ""this"" w,o.rks", foo bar + // this, "company, llc", "and ""this"" w,o.rks", foo bar. $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; preg_match_all($regexp, $tags, $matches); $typed_tags = array_unique($matches[1]); diff --git a/core/lib/Drupal/Component/Utility/UserAgent.php b/core/lib/Drupal/Component/Utility/UserAgent.php index cc42d2f25b8..80cdb19c41e 100644 --- a/core/lib/Drupal/Component/Utility/UserAgent.php +++ b/core/lib/Drupal/Component/Utility/UserAgent.php @@ -45,7 +45,7 @@ class UserAgent { // 1#( language-range [ ";" "q" "=" qvalue ] ) // language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" ) // @endcode - // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" + // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5". $ua_langcodes = []; if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($http_accept_language), $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { diff --git a/core/lib/Drupal/Core/Access/AccessGroupAnd.php b/core/lib/Drupal/Core/Access/AccessGroupAnd.php new file mode 100644 index 00000000000..d4535ccac52 --- /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 00000000000..eee44102c95 --- /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 00000000000..a6e5e671aaa --- /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 00000000000..96b966e8d87 --- /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/Action/ActionPluginCollection.php b/core/lib/Drupal/Core/Action/ActionPluginCollection.php index a5e57d4fa88..5b7d3efa0ed 100644 --- a/core/lib/Drupal/Core/Action/ActionPluginCollection.php +++ b/core/lib/Drupal/Core/Action/ActionPluginCollection.php @@ -13,8 +13,8 @@ class ActionPluginCollection extends DefaultSingleLazyPluginCollection { * {@inheritdoc} * * @return \Drupal\Core\Action\ActionInterface + * The action plugin instance. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function &get($instance_id) { return parent::get($instance_id); } diff --git a/core/lib/Drupal/Core/Action/ConfigurableActionBase.php b/core/lib/Drupal/Core/Action/ConfigurableActionBase.php index dd1e8ccdc54..a2a55f354e6 100644 --- a/core/lib/Drupal/Core/Action/ConfigurableActionBase.php +++ b/core/lib/Drupal/Core/Action/ConfigurableActionBase.php @@ -5,6 +5,7 @@ namespace Drupal\Core\Action; use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ConfigurableTrait; use Drupal\Core\Plugin\PluginFormInterface; /** @@ -12,6 +13,8 @@ use Drupal\Core\Plugin\PluginFormInterface; */ abstract class ConfigurableActionBase extends ActionBase implements ConfigurableInterface, DependentPluginInterface, PluginFormInterface { + use ConfigurableTrait; + /** * {@inheritdoc} */ @@ -24,27 +27,6 @@ abstract class ConfigurableActionBase extends ActionBase implements Configurable /** * {@inheritdoc} */ - public function defaultConfiguration() { - return []; - } - - /** - * {@inheritdoc} - */ - public function getConfiguration() { - return $this->configuration; - } - - /** - * {@inheritdoc} - */ - public function setConfiguration(array $configuration) { - $this->configuration = $configuration + $this->defaultConfiguration(); - } - - /** - * {@inheritdoc} - */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { } diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 87fb3588cb4..e56385da262 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -56,6 +56,23 @@ class AjaxResponse extends JsonResponse implements AttachmentsInterface { } /** + * Merges other ajax response with this one. + * + * Adds commands and merges attachments from the other ajax response. + * + * @param \Drupal\Core\Ajax\AjaxResponse $other + * An AJAX response to merge. + * + * @return $this + * Returns this after merging. + */ + public function mergeWith(AjaxResponse $other): AjaxResponse { + $this->commands = array_merge($this->getCommands(), $other->getCommands()); + $this->attachments = BubbleableMetadata::mergeAttachments($this->getAttachments(), $other->getAttachments()); + return $this; + } + + /** * Gets all AJAX commands. * * @return array diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php index e853f63b78c..ef94e99c9fa 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolver.php +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php @@ -286,7 +286,8 @@ class AssetResolver implements AssetResolverInterface { * {@inheritdoc} */ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?LanguageInterface $language = NULL) { - if (!$assets->getLibraries() && !$assets->getSettings()) { + $asset_settings = $assets->getSettings(); + if (!$assets->getLibraries() && !$asset_settings) { return [[], []]; } if (!isset($language)) { @@ -309,14 +310,14 @@ class AssetResolver implements AssetResolverInterface { // If all the libraries to load contained only CSS, there is nothing further // to do here, so return early. - if (!$libraries_to_load && !$assets->getSettings()) { + if (!$libraries_to_load && !$asset_settings) { return [[], []]; } // Add the theme name to the cache key since themes may implement // hook_library_info_alter(). Additionally add the current language to // support translation of JavaScript files via hook_js_alter(). - $cid = 'js:' . $theme_info->getName() . ':' . $language->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; + $cid = 'js:' . $theme_info->getName() . ':' . $language->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . ':' . (int) $optimize; if ($cached = $this->cache->get($cid)) { [$js_assets_header, $js_assets_footer, $settings, $settings_in_header] = $cached->data; @@ -389,32 +390,30 @@ class AssetResolver implements AssetResolverInterface { $js_assets_footer = $collection_optimizer->optimize($js_assets_footer, $libraries_to_load); } - // If the core/drupalSettings library is being loaded or is already - // loaded, get the JavaScript settings assets, and convert them into a - // single "regular" JavaScript asset. - $libraries_to_load = $this->getLibrariesToLoad($assets); - $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); - $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0; - - // Initialize settings to FALSE since they are not needed by default. This - // distinguishes between an empty array which must still allow - // hook_js_settings_alter() to be run. - $settings = FALSE; - if ($settings_required && $settings_have_changed) { - $settings = $this->getJsSettingsAssets($assets); - // Allow modules to add cached JavaScript settings. - $this->moduleHandler->invokeAllWith('js_settings_build', function (callable $hook, string $module) use (&$settings, $assets) { - $hook($settings, $assets); - }); - } + // Always build settings from js libraries. They may or may not be + // used later depending on whether the core/drupalSettings library is + // requested. + $settings = $this->getJsSettingsAssets($assets); + // Allow modules to add cached JavaScript settings. + $this->moduleHandler->invokeAllWith('js_settings_build', function (callable $hook, string $module) use (&$settings, $assets) { + $hook($settings, $assets); + }); $settings_in_header = in_array('core/drupalSettings', $header_js_libraries); $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']); } - if ($settings !== FALSE) { + // If the core/drupalSettings library is being loaded or is already + // loaded, get the JavaScript settings assets, and convert them into a + // single "regular" JavaScript asset. But only if there are settings to + // add. Do the quickest checks first. + $process_settings = FALSE; + if (count($libraries_to_load) > 0 || count($asset_settings) > 0) { + $process_settings = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); + } + if ($process_settings) { // Attached settings override both library definitions and // hook_js_settings_build(). - $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); + $settings = NestedArray::mergeDeepArray([$settings, $asset_settings], TRUE); // Allow modules and themes to alter the JavaScript settings. $this->moduleHandler->alter('js_settings', $settings, $assets); $this->themeManager->alter('js_settings', $settings, $assets); diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php index 8d3fd93ecee..f099aaf719b 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php @@ -148,7 +148,14 @@ class CssCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterfa $data .= "/* @license " . $css_asset['license']['name'] . " " . $css_asset['license']['url'] . " */\n"; } $current_license = $css_asset['license']; - $data .= $this->optimizer->optimize($css_asset); + + // Append this file if already minified; otherwise optimize it. + if (isset($css_asset['minified']) && $css_asset['minified']) { + $data .= file_get_contents($css_asset['data']); + } + else { + $data .= $this->optimizer->optimize($css_asset); + } } // Per the W3C specification at // https://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import rules must diff --git a/core/lib/Drupal/Core/Asset/CssOptimizer.php b/core/lib/Drupal/Core/Asset/CssOptimizer.php index 9b5559c696d..8847c1e5caa 100644 --- a/core/lib/Drupal/Core/Asset/CssOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssOptimizer.php @@ -130,8 +130,7 @@ class CssOptimizer implements AssetOptimizerInterface { if ($reset_base_path) { $base_path = ''; } - // Store the value of $optimize for preg_replace_callback with nested - // @import loops. + // Store $optimize for preg_replace_callback with nested @import loops. if (isset($optimize)) { $_optimize = $optimize; } diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php index 5db7f9a2cd3..720724ca7dd 100644 --- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php @@ -14,19 +14,18 @@ interface BreadcrumbBuilderInterface { * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The current route match. - * phpcs:disable Drupal.Commenting - * @todo Uncomment new method parameters before drupal:12.0.0, see - * https://www.drupal.org/project/drupal/issues/3459277. - * + * phpcs:ignore Drupal.Commenting.FunctionComment.ParamNameNoMatch * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata * The cacheable metadata to add to if your check varies by or depends * on something. Anything you specify here does not have to be repeated in * the build() method as it will be merged in automatically. - * phpcs:enable * * @return bool * TRUE if this builder should be used or FALSE to let other builders * decide. + * + * @todo Uncomment new method parameters before drupal:12.0.0, see + * https://www.drupal.org/project/drupal/issues/3459277. */ public function applies(RouteMatchInterface $route_match /* , CacheableMetadata $cacheable_metadata */); diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbPreprocess.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbPreprocess.php new file mode 100644 index 00000000000..4de3fde48a4 --- /dev/null +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbPreprocess.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\Core\Breadcrumb; + +/** + * Breadcrumb theme preprocess. + * + * @internal + */ +class BreadcrumbPreprocess { + + /** + * Prepares variables for breadcrumb templates. + * + * Default template: breadcrumb.html.twig. + * + * @param array $variables + * An associative array containing: + * - links: A list of \Drupal\Core\Link objects which should be rendered. + */ + public function preprocessBreadcrumb(array &$variables): void { + $variables['breadcrumb'] = []; + /** @var \Drupal\Core\Link $link */ + foreach ($variables['links'] as $key => $link) { + $variables['breadcrumb'][$key] = [ + 'text' => $link->getText(), + 'url' => $link->getUrl()->toString(), + ]; + } + } + +} diff --git a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php index 6c02649d270..dad8bc10a21 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 00000000000..24c110372d1 --- /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 cb88c69495a..9602bc8ba5d 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/Command/DbCommandBase.php b/core/lib/Drupal/Core/Command/DbCommandBase.php index c333c8a00c9..53ad859383c 100644 --- a/core/lib/Drupal/Core/Command/DbCommandBase.php +++ b/core/lib/Drupal/Core/Command/DbCommandBase.php @@ -38,7 +38,7 @@ class DbCommandBase extends Command { if (Database::getConnectionInfo('db-tools')) { throw new \RuntimeException('Database "db-tools" is already defined. Cannot define database provided.'); } - $info = Database::convertDbUrlToConnectionInfo($input->getOption('database-url'), \Drupal::root()); + $info = Database::convertDbUrlToConnectionInfo($input->getOption('database-url')); Database::addConnectionInfo('db-tools', 'default', $info); $key = 'db-tools'; } diff --git a/core/lib/Drupal/Core/Command/GenerateProxyClassApplication.php b/core/lib/Drupal/Core/Command/GenerateProxyClassApplication.php index 2a36dde2845..88dca58edcf 100644 --- a/core/lib/Drupal/Core/Command/GenerateProxyClassApplication.php +++ b/core/lib/Drupal/Core/Command/GenerateProxyClassApplication.php @@ -11,7 +11,7 @@ use Symfony\Component\Console\Input\InputInterface; * Provides a console command to generate proxy classes. * * @see lazy_services - * @see core/scripts/generate-proxy.sh + * @see core/scripts/generate-proxy-class.php */ class GenerateProxyClassApplication extends Application { diff --git a/core/lib/Drupal/Core/Command/GenerateProxyClassCommand.php b/core/lib/Drupal/Core/Command/GenerateProxyClassCommand.php index f3993223707..e6b9cd02c86 100644 --- a/core/lib/Drupal/Core/Command/GenerateProxyClassCommand.php +++ b/core/lib/Drupal/Core/Command/GenerateProxyClassCommand.php @@ -12,7 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface; * Provides a console command to generate proxy classes. * * @see lazy_services - * @see core/scripts/generate-proxy.sh + * @see core/scripts/generate-proxy-class.php */ class GenerateProxyClassCommand extends Command { diff --git a/core/lib/Drupal/Core/Condition/ConditionPluginCollection.php b/core/lib/Drupal/Core/Condition/ConditionPluginCollection.php index c9a0c11499d..cd46f7ba161 100644 --- a/core/lib/Drupal/Core/Condition/ConditionPluginCollection.php +++ b/core/lib/Drupal/Core/Condition/ConditionPluginCollection.php @@ -21,8 +21,8 @@ class ConditionPluginCollection extends DefaultLazyPluginCollection { * {@inheritdoc} * * @return \Drupal\Core\Condition\ConditionInterface + * The condition plugin instance for the given instance ID. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function &get($instance_id) { return parent::get($instance_id); } diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index 2e78c30b5ac..9d604934f7d 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -209,6 +209,8 @@ class Config extends StorableConfigBase { // Ensure that the schema wrapper has the latest data. $this->schemaWrapper = NULL; $this->data = $this->castValue(NULL, $this->data); + // Reclaim the memory used by the schema wrapper. + $this->schemaWrapper = NULL; } else { foreach ($this->data as $key => $value) { diff --git a/core/lib/Drupal/Core/Config/ConfigBase.php b/core/lib/Drupal/Core/Config/ConfigBase.php index 6c8148612ed..adb8cf70baf 100644 --- a/core/lib/Drupal/Core/Config/ConfigBase.php +++ b/core/lib/Drupal/Core/Config/ConfigBase.php @@ -102,7 +102,7 @@ abstract class ConfigBase implements RefinableCacheableDependencyInterface { } // The name must not contain any of the following characters: - // : ? * < > " ' / \ + // : ? * < > ' " / \ if (preg_match('/[:?*<>"\'\/\\\\]/', $name)) { throw new ConfigNameException("Invalid character in Config object name $name."); } diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 60da9de1247..299693f2c77 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -272,7 +272,7 @@ class DatabaseStorage implements StorageInterface { * be unserialized. */ public function decode($raw) { - $data = @unserialize($raw); + $data = @unserialize($raw, ['allowed_classes' => FALSE]); return is_array($data) ? $data : FALSE; } 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 fbc7542a7de..9f6d1188dc8 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/Config/Schema/Mapping.php b/core/lib/Drupal/Core/Config/Schema/Mapping.php index 22f3040a6f7..3b859dd1ab9 100644 --- a/core/lib/Drupal/Core/Config/Schema/Mapping.php +++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php @@ -166,8 +166,8 @@ class Mapping extends ArrayElement { $all_type_definitions = $typed_data_manager->getDefinitions(); $possible_type_definitions = array_intersect_key($all_type_definitions, array_fill_keys($possible_types, TRUE)); // TRICKY: \Drupal\Core\Config\TypedConfigManager::getDefinition() does the - // necessary resolving, but TypedConfigManager::getDefinitions() does not! - // 🤷♂️ + // necessary resolving, but TypedConfigManager::getDefinitions() does not + // 🤷♂️! // @see \Drupal\Core\Config\TypedConfigManager::getDefinitionWithReplacements() // @see ::getValidKeys() $valid_keys_per_type = []; @@ -273,7 +273,7 @@ class Mapping extends ArrayElement { // use in a regex. So: // `module\.something\.foo_.*` // or - // `.*\.third_party\..*` + // `.*\.third_party\..*`. $regex = str_replace(['.', '[]'], ['\.', '.*'], $name); // Now find all possible types: // 1. `module.something.foo_foo`, `module.something.foo_bar`, etc. diff --git a/core/lib/Drupal/Core/Config/Schema/TypeResolver.php b/core/lib/Drupal/Core/Config/Schema/TypeResolver.php index af9c1c53aba..1cbe3d37976 100644 --- a/core/lib/Drupal/Core/Config/Schema/TypeResolver.php +++ b/core/lib/Drupal/Core/Config/Schema/TypeResolver.php @@ -99,7 +99,7 @@ class TypeResolver { } $previous_name = $name; if (!is_array($data) || !isset($data[$name])) { - // Key not found, return original value + // Key not found, return original value. return $expression; } if (!$parts) { @@ -126,7 +126,7 @@ class TypeResolver { } $data = $data[$name]; } - // Return the original value + // Return the original value. return $expression; } diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index 99fdb0d5978..ae2ba86b3ad 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -376,14 +376,13 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI else { // No definition for this level. Collapse multiple wildcards to a single // wildcard to see if there is a greedy match. For example, - // breakpoint.breakpoint.*.* becomes - // breakpoint.breakpoint.* + // "breakpoint.breakpoint.*.*" becomes "breakpoint.breakpoint.*". $one_star = preg_replace('/\.([:\.\*]*)$/', '.*', $replaced); if ($one_star != $replaced && isset($this->definitions[$one_star])) { return $one_star; } - // Check for next level. For example, if breakpoint.breakpoint.* has - // been checked and no match found then check breakpoint.*.* + // Check for next level. For example, if "breakpoint.breakpoint.*" has + // been checked and no match found then check "breakpoint.*.*". return $this->getFallbackName($replaced); } } diff --git a/core/lib/Drupal/Core/Cron.php b/core/lib/Drupal/Core/Cron.php index 1773d810a25..982e4714fc9 100644 --- a/core/lib/Drupal/Core/Cron.php +++ b/core/lib/Drupal/Core/Cron.php @@ -107,7 +107,7 @@ class Cron implements CronInterface { // Add watchdog message. $this->logger->info('Cron run completed.'); - // Return TRUE so other functions can check if it did run successfully + // Return TRUE so other functions can check if it did run successfully. $return = TRUE; } diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 489b2f5f94d..a0d9fc67d1a 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. @@ -1112,13 +1112,13 @@ abstract class Connection { * \Drupal\Core\Database\Transaction\TransactionManagerBase, and instantiate * it here. * + * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn * @return \Drupal\Core\Database\Transaction\TransactionManagerInterface * The transaction manager. * * @throws \LogicException * If the transaction manager is undefined or unavailable. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn, Drupal.Commenting.FunctionComment.Missing protected function driverTransactionManager(): TransactionManagerInterface { throw new \LogicException('The database driver has no TransactionManager implementation'); } diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php index e76bc2d6991..23b5b79c7b6 100644 --- a/core/lib/Drupal/Core/Database/Database.php +++ b/core/lib/Drupal/Core/Database/Database.php @@ -13,6 +13,8 @@ use Drupal\Core\Cache\NullBackend; * This class is un-extendable. It acts to encapsulate all control and * shepherding of database connections into a single location without the use of * globals. + * + * @final */ abstract class Database { @@ -495,8 +497,8 @@ abstract class Database { * * @param string $url * The URL. - * @param string $root - * The root directory of the Drupal installation. + * @param string|bool|null $root + * (deprecated) The root directory of the Drupal installation. * @param bool|null $include_test_drivers * (optional) Whether to include test extensions. If FALSE, all 'tests' * directories are excluded in the search. When NULL will be determined by @@ -511,7 +513,16 @@ abstract class Database { * @throws \RuntimeException * Exception thrown when a module provided database driver does not exist. */ - public static function convertDbUrlToConnectionInfo($url, $root, ?bool $include_test_drivers = NULL) { + public static function convertDbUrlToConnectionInfo(string $url, $root = NULL, ?bool $include_test_drivers = NULL): array { + if ($root !== NULL) { + if (is_bool($root)) { + $include_test_drivers = $root; + } + else { + @trigger_error("Passing a string \$root value to " . __METHOD__ . "() is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287", E_USER_DEPRECATED); + } + } + // Check that the URL is well formed, starting with 'scheme://', where // 'scheme' is a database driver name. if (preg_match('/^(.*):\/\//', $url, $matches) !== 1) { diff --git a/core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php b/core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php new file mode 100644 index 00000000000..799e9ca9560 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php @@ -0,0 +1,12 @@ +<?php + +namespace Drupal\Core\Database\Exception; + +use Drupal\Core\Database\DatabaseException; +use Drupal\Core\Database\SchemaException; + +/** + * Exception thrown if the Primary Key must be dropped before an operation. + */ +class SchemaPrimaryKeyMustBeDroppedException extends SchemaException implements DatabaseException { +} diff --git a/core/lib/Drupal/Core/Database/Query/Condition.php b/core/lib/Drupal/Core/Database/Query/Condition.php index 89757a754f0..c03440e5820 100644 --- a/core/lib/Drupal/Core/Database/Query/Condition.php +++ b/core/lib/Drupal/Core/Database/Query/Condition.php @@ -222,16 +222,16 @@ class Condition implements ConditionInterface, \Countable { } $arguments += $condition['field']->arguments(); // If the operator and value were not passed in to the - // @see ConditionInterface::condition() method (and thus have the - // default value as defined over there) it is assumed to be a valid - // condition on its own: ignore the operator and value parts. + // ConditionInterface::condition() method (and thus have the default + // value as defined over there) it is assumed to be a valid condition + // on its own: ignore the operator and value parts. $ignore_operator = $condition['operator'] === '=' && $condition['value'] === NULL; } elseif (!isset($condition['operator'])) { // Left hand part is a literal string added with the - // @see ConditionInterface::where() method. Put brackets around - // the snippet and collect the arguments from the value part. - // Also ignore the operator and value parts. + // ConditionInterface::where() method. Put brackets around the snippet + // and collect the arguments from the value part. Also ignore the + // operator and value parts. $field_fragment = '(' . $condition['field'] . ')'; $arguments += $condition['value']; $ignore_operator = TRUE; diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index d284ea285a5..629037ad133 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -810,13 +810,13 @@ class Select extends Query implements SelectInterface { // Create a sanitized comment string to prepend to the query. $comments = $this->connection->makeComment($this->comments); - // SELECT + // SELECT. $query = $comments . 'SELECT '; if ($this->distinct) { $query .= 'DISTINCT '; } - // FIELDS and EXPRESSIONS + // FIELDS and EXPRESSIONS. $fields = []; foreach ($this->tables as $alias => $table) { if (!empty($table['all_fields'])) { @@ -870,13 +870,13 @@ class Select extends Query implements SelectInterface { } } - // WHERE + // WHERE. if (count($this->condition)) { // There is an implicit string cast on $this->condition. $query .= "\nWHERE " . $this->condition; } - // GROUP BY + // GROUP BY. if ($this->group) { $group_by_fields = array_map(function (string $field): string { return $this->connection->escapeField($field); @@ -884,7 +884,7 @@ class Select extends Query implements SelectInterface { $query .= "\nGROUP BY " . implode(', ', $group_by_fields); } - // HAVING + // HAVING. if (count($this->having)) { // There is an implicit string cast on $this->having. $query .= "\nHAVING " . $this->having; @@ -898,7 +898,7 @@ class Select extends Query implements SelectInterface { } } - // ORDER BY + // ORDER BY. if ($this->order) { $query .= "\nORDER BY "; $fields = []; diff --git a/core/lib/Drupal/Core/Database/Schema.php b/core/lib/Drupal/Core/Database/Schema.php index 97e73b1bf8c..9135ae471a6 100644 --- a/core/lib/Drupal/Core/Database/Schema.php +++ b/core/lib/Drupal/Core/Database/Schema.php @@ -171,7 +171,7 @@ abstract class Schema implements PlaceholderInterface { protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) { $info = $this->connection->getConnectionOptions(); - // Retrieve the table name and schema + // Retrieve the table name and schema. $table_info = $this->getPrefixInfo($table_name, $add_prefix); $condition = $this->connection->condition('AND'); @@ -539,6 +539,7 @@ abstract class Schema implements PlaceholderInterface { * @param string $table * The name of the table. * + * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn * @return array * A schema array with the following keys: 'primary key', 'unique keys' and * 'indexes', and values as arrays of database columns. @@ -548,7 +549,6 @@ abstract class Schema implements PlaceholderInterface { * @throws \RuntimeException * If the driver does not implement this method. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn, Drupal.Commenting.FunctionComment.Missing protected function introspectIndexSchema($table) { if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); @@ -658,6 +658,7 @@ abstract class Schema implements PlaceholderInterface { * @param array $table * A Schema API table definition array. * + * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn * @return array * An array of SQL statements to create the table. * @@ -670,7 +671,6 @@ abstract class Schema implements PlaceholderInterface { * method, or to make it private for each driver, and ::createTable actually * an abstract method here for implementation in each driver. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn, Drupal.Commenting.FunctionComment.Missing protected function createTableSql($name, $table) { throw new \BadMethodCallException(get_class($this) . '::createTableSql() not implemented.'); } diff --git a/core/lib/Drupal/Core/Database/Statement/PdoResult.php b/core/lib/Drupal/Core/Database/Statement/PdoResult.php index 1353ea8e8ad..f046001076a 100644 --- a/core/lib/Drupal/Core/Database/Statement/PdoResult.php +++ b/core/lib/Drupal/Core/Database/Statement/PdoResult.php @@ -31,6 +31,18 @@ class PdoResult extends ResultBase { } /** + * Returns the client-level database PDO statement object. + * + * This method should normally be used only within database driver code. + * + * @return \PDOStatement + * The client-level database PDO statement. + */ + public function getClientStatement(): \PDOStatement { + return $this->clientStatement; + } + + /** * {@inheritdoc} */ public function rowCount(): ?int { diff --git a/core/lib/Drupal/Core/Database/Statement/PdoTrait.php b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php index 3e1f104c9f4..d8bf62b4fdf 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 { @@ -49,28 +49,22 @@ trait PdoTrait { } /** - * Returns the client-level database PDO statement object. + * Returns the client-level database statement object. * * This method should normally be used only within database driver code. * - * @return \PDOStatement - * The client-level database PDO statement. + * @return object + * The client-level database statement. * * @throws \RuntimeException * If the client-level statement is not set. */ - public function getClientStatement(): \PDOStatement { - if (isset($this->clientStatement)) { - assert($this->clientStatement instanceof \PDOStatement); - return $this->clientStatement; - } - throw new \LogicException('\\PDOStatement not initialized'); - } + abstract public function getClientStatement(): object; /** * 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 +112,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 +169,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 @@ -188,7 +182,6 @@ trait PdoTrait { * @return array<array<scalar|null>|object|scalar|null> * An array of results. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn, Drupal.Commenting.FunctionComment.Missing protected function clientFetchAll(?FetchAs $mode = NULL, int|string|null $columnOrClass = NULL, array|null $constructorArguments = NULL): array { return match ($mode) { FetchAs::Column => $this->getClientStatement()->fetchAll( diff --git a/core/lib/Drupal/Core/Database/Statement/ResultBase.php b/core/lib/Drupal/Core/Database/Statement/ResultBase.php index 6232581f906..af1b12a5653 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 c193c5d3502..e266616975f 100644 --- a/core/lib/Drupal/Core/Database/Statement/StatementBase.php +++ b/core/lib/Drupal/Core/Database/Statement/StatementBase.php @@ -86,6 +86,36 @@ abstract class StatementBase implements \Iterator, StatementInterface { } /** + * Determines if the client-level database statement object exists. + * + * This method should normally be used only within database driver code. + * + * @return bool + * TRUE if the client statement exists, FALSE otherwise. + */ + public function hasClientStatement(): bool { + return isset($this->clientStatement); + } + + /** + * Returns the client-level database statement object. + * + * This method should normally be used only within database driver code. + * + * @return object + * The client-level database statement. + * + * @throws \RuntimeException + * If the client-level statement is not set. + */ + public function getClientStatement(): object { + if ($this->hasClientStatement()) { + return $this->clientStatement; + } + throw new \LogicException('Client statement not initialized'); + } + + /** * {@inheritdoc} */ public function getConnectionTarget(): string { @@ -180,7 +210,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 +247,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 +322,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 +355,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 c4cafb9d289..7f906620674 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 96bc07e7f89..dc1d3c98eb3 100644 --- a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php +++ b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php @@ -97,11 +97,30 @@ class StatementPrefetchIterator extends StatementBase { } /** + * Returns the client-level database PDO statement object. + * + * This method should normally be used only within database driver code. + * + * @return \PDOStatement + * The client-level database PDO statement. + * + * @throws \RuntimeException + * If the client-level statement is not set. + */ + public function getClientStatement(): \PDOStatement { + if (isset($this->clientStatement)) { + assert($this->clientStatement instanceof \PDOStatement); + return $this->clientStatement; + } + throw new \LogicException('\\PDOStatement not initialized'); + } + + /** * {@inheritdoc} */ 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 88dc007f540..27293131a90 100644 --- a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php +++ b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php @@ -48,11 +48,30 @@ class StatementWrapperIterator extends StatementBase { } /** + * Returns the client-level database PDO statement object. + * + * This method should normally be used only within database driver code. + * + * @return \PDOStatement + * The client-level database PDO statement. + * + * @throws \RuntimeException + * If the client-level statement is not set. + */ + public function getClientStatement(): \PDOStatement { + if (isset($this->clientStatement)) { + assert($this->clientStatement instanceof \PDOStatement); + return $this->clientStatement; + } + throw new \LogicException('\\PDOStatement not initialized'); + } + + /** * {@inheritdoc} */ 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'])) { @@ -71,7 +90,7 @@ class StatementWrapperIterator extends StatementBase { $this->result = new PdoResult( $this->fetchMode, $this->fetchOptions, - $this->clientStatement, + $this->getClientStatement(), ); $this->markResultsetIterable($return); } diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php index dcecc44e17c..b8693e4bb76 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 aa663d94226..fa1a309a767 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 11af511f14b..a9aa2c77052 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/AutowireTrait.php b/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php index 617699bda15..a0824c65f17 100644 --- a/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php +++ b/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php @@ -34,6 +34,10 @@ trait AutowireTrait { } if (!$container->has($service)) { + if ($parameter->allowsNull()) { + $args[] = NULL; + continue; + } throw new AutowiringFailedException($service, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s::_construct()", you should configure its value explicitly.', $service, $parameter->getName(), static::class)); } diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php index 1cdeb7263ce..127318c1cd0 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/DependencyInjection/Compiler/TaggedHandlersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php index 514cd9e9f55..13aa3dcf3f7 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php @@ -3,8 +3,10 @@ namespace Drupal\Core\DependencyInjection\Compiler; use Drupal\Component\Utility\Reflection; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; @@ -178,7 +180,8 @@ class TaggedHandlersPass implements CompilerPassInterface { foreach ($this->tagCache[$tag] ?? [] as $id => $attributes) { // Validate the interface. $handler = $container->getDefinition($id); - if (!is_a($handler->getClass(), $interface, TRUE)) { + $class = $this->resolveDefinitionClass($handler, $container); + if (!is_a($class, $interface, TRUE)) { throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface."); } $handlers[$id] = $attributes[0]['priority'] ?? 0; @@ -249,4 +252,33 @@ class TaggedHandlersPass implements CompilerPassInterface { $consumer->addArgument(array_keys($handlers)); } + /** + * Resolves the definition class. + * + * @param \Symfony\Component\DependencyInjection\Definition $definition + * The service definition. + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * The service container. + * + * @return class-string|null + * The resolved class-string or null if the class cannot be resolved. + */ + protected function resolveDefinitionClass(Definition $definition, ContainerBuilder $container): ?string { + $class = $definition->getClass(); + if ($class) { + return $class; + } + + if (!$definition instanceof ChildDefinition) { + return NULL; + } + + if (!$container->hasDefinition($definition->getParent())) { + return NULL; + } + + $parent = $container->getDefinition($definition->getParent()); + return $this->resolveDefinitionClass($parent, $container); + } + } diff --git a/core/lib/Drupal/Core/Display/VariantBase.php b/core/lib/Drupal/Core/Display/VariantBase.php index dafb060d59f..328bb89a2db 100644 --- a/core/lib/Drupal/Core/Display/VariantBase.php +++ b/core/lib/Drupal/Core/Display/VariantBase.php @@ -4,7 +4,7 @@ namespace Drupal\Core\Display; use Drupal\Core\Cache\RefinableCacheableDependencyTrait; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Plugin\ConfigurablePluginBase; use Drupal\Core\Plugin\PluginDependencyTrait; use Drupal\Core\Session\AccountInterface; @@ -16,7 +16,7 @@ use Drupal\Core\Session\AccountInterface; * @see \Drupal\Core\Display\VariantManager * @see plugin_api */ -abstract class VariantBase extends PluginBase implements VariantInterface { +abstract class VariantBase extends ConfigurablePluginBase implements VariantInterface { use PluginDependencyTrait; use RefinableCacheableDependencyTrait; @@ -24,15 +24,6 @@ abstract class VariantBase extends PluginBase implements VariantInterface { /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - - $this->setConfiguration($configuration); - } - - /** - * {@inheritdoc} - */ public function label() { return $this->configuration['label']; } @@ -77,14 +68,6 @@ abstract class VariantBase extends PluginBase implements VariantInterface { /** * {@inheritdoc} */ - public function setConfiguration(array $configuration) { - $this->configuration = $configuration + $this->defaultConfiguration(); - return $this; - } - - /** - * {@inheritdoc} - */ public function defaultConfiguration() { return [ 'label' => '', diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 221164875ef..1a5de6c18fd 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -1315,11 +1315,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { // Get a list of namespaces and put it onto the container. $namespaces = $this->getModuleNamespacesPsr4($this->getModuleFileNames()); - // Add all components in \Drupal\Core and \Drupal\Component that have one of - // the following directories: - // - Element - // - Entity - // - Plugin + // Add all components in \Drupal\Core and \Drupal\Component that have one or + // more of Element, Entity and Plugin directories. foreach (['Core', 'Component'] as $parent_directory) { $path = 'core/lib/Drupal/' . $parent_directory; $parent_namespace = 'Drupal\\' . $parent_directory; diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index ef5669e8184..53b4ac0cdf3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -578,6 +578,17 @@ abstract class ContentEntityBase extends EntityBase implements \IteratorAggregat /** * {@inheritdoc} */ + public function getBundleEntity(): ?EntityInterface { + $entityType = $this->getEntityType(); + if (!$entityType->hasKey('bundle') || !$entityType->getBundleEntityType()) { + return NULL; + } + return $this->get($entityType->getKey('bundle'))->entity; + } + + /** + * {@inheritdoc} + */ public function uuid() { return $this->getEntityKey('uuid'); } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php index 0855f1e3f77..4efdbd8e7c3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php @@ -422,7 +422,6 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface '#open' => $new_revision_default, '#group' => 'advanced', '#weight' => 20, - '#access' => $new_revision_default || $this->entity->get($entity_type->getKey('revision'))->access('update'), '#optional' => TRUE, '#attributes' => [ 'class' => ['entity-content-form-revision-information'], @@ -436,7 +435,7 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface '#type' => 'checkbox', '#title' => $this->t('Create new revision'), '#default_value' => $new_revision_default, - '#access' => !$this->entity->isNew() && $this->entity->get($entity_type->getKey('revision'))->access('update'), + '#access' => !$this->entity->isNew(), '#group' => 'revision_information', ]; // Get log message field's key from definition. diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php index d01f7ed59ff..bd2f8f381fb 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php @@ -23,4 +23,13 @@ namespace Drupal\Core\Entity; */ interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, TranslatableRevisionableInterface, SynchronizableInterface { + /** + * Gets the bundle entity of this entity. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The entity which is the bundle of this entity, or NULL if this entity's + * entity type does not represent bundles with an entity. + */ + public function getBundleEntity(): ?EntityInterface; + } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index e7b42bc2af7..ff77c5c33a9 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -232,7 +232,7 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con throw new EntityStorageException(sprintf("Missing entity bundle. The \"%s\" bundle does not exist", $bundle)); } $values[$bundle_key] = $bundle; - // Bundle is already set + // Bundle is already set. $forbidden_keys[] = $bundle_key; } // Forbid sample generation on any keys whose values were submitted. diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityController.php b/core/lib/Drupal/Core/Entity/Controller/EntityController.php index 9a873730466..ca700c3152d 100644 --- a/core/lib/Drupal/Core/Entity/Controller/EntityController.php +++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -335,13 +335,16 @@ class EntityController implements ContainerInjectionInterface { /** * Expands the bundle information with descriptions, if known. * + * Also sorts the bundles before adding the bundle descriptions. Sorting is + * being done here to avoid having to load bundle entities multiple times. + * * @param array $bundles * An array of bundle information. * @param \Drupal\Core\Entity\EntityTypeInterface $bundle_entity_type * The bundle entity type definition. * * @return array - * The expanded array of bundle information. + * An array of sorted bundle information including bundle descriptions. */ protected function loadBundleDescriptions(array $bundles, EntityTypeInterface $bundle_entity_type) { if (!$bundle_entity_type->entityClassImplements(EntityDescriptionInterface::class)) { @@ -351,6 +354,10 @@ class EntityController implements ContainerInjectionInterface { $storage = $this->entityTypeManager->getStorage($bundle_entity_type->id()); /** @var \Drupal\Core\Entity\EntityDescriptionInterface[] $bundle_entities */ $bundle_entities = $storage->loadMultiple($bundle_names); + + uasort($bundle_entities, [$bundle_entity_type->getClass(), 'sort']); + $bundles = array_replace($bundle_entities, $bundles); + foreach ($bundles as $bundle_name => &$bundle_info) { if (isset($bundle_entities[$bundle_name])) { $bundle_info['description'] = $bundle_entities[$bundle_name]->getDescription(); diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 132aaa43997..11422c6790c 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -19,29 +19,38 @@ use Drupal\Core\Site\Settings; * entities, which can come from all or specific bundles of an entity type. * * Properties: - * - #target_type: (required) The ID of the target entity type. - * - #tags: (optional) TRUE if the element allows multiple selection. Defaults + * + * @property $target_type + * (required) The ID of the target entity type. + * @property $tags + * (optional) TRUE if the element allows multiple selection. Defaults * to FALSE. - * - #default_value: (optional) The default entity or an array of default + * @property $default_value + * (optional) The default entity or an array of default * entities, depending on the value of #tags. - * - #selection_handler: (optional) The plugin ID of the entity reference + * @property $selection_handler + * (optional) The plugin ID of the entity reference * selection handler (a plugin of type EntityReferenceSelection). The default * value is the lowest-weighted plugin that is compatible with #target_type. - * - #selection_settings: (optional) An array of settings for the selection + * @property $selection_settings + * (optional) An array of settings for the selection * handler. Settings for the default selection handler * \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection are: * - target_bundles: Array of bundles to allow (omit to allow all bundles). * - sort: Array with 'field' and 'direction' keys, determining how results * will be sorted. Defaults to unsorted. - * - #autocreate: (optional) Array of settings used to auto-create entities + * @property $autocreate + * (optional) Array of settings used to auto-create entities * that do not exist (omit to not auto-create entities). Elements: * - bundle: (required) Bundle to use for auto-created entities. * - uid: User ID to use as the author of auto-created entities. Defaults to * the current user. - * - #process_default_value: (optional) Set to FALSE if the #default_value + * @property $process_default_value + * (optional) Set to FALSE if the #default_value * property is processed and access checked elsewhere (such as by a Field API * widget). Defaults to TRUE. - * - #validate_reference: (optional) Set to FALSE if validation of the selected + * @property $validate_reference + * (optional) Set to FALSE if validation of the selected * entities is performed elsewhere. Defaults to TRUE. * * Usage example: diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php index b40e3263671..019f8535ef6 100644 --- a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php @@ -2,32 +2,15 @@ namespace Drupal\Core\Entity\EntityReferenceSelection; -use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Component\Plugin\DependentPluginInterface; -use Drupal\Component\Utility\NestedArray; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Plugin\ConfigurablePluginBase; /** * Provides a base class for configurable selection handlers. */ -abstract class SelectionPluginBase extends PluginBase implements SelectionInterface, ConfigurableInterface, DependentPluginInterface { - - /** - * Constructs a new selection object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin ID for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->setConfiguration($configuration); - } +abstract class SelectionPluginBase extends ConfigurablePluginBase implements SelectionInterface, DependentPluginInterface { /** * {@inheritdoc} @@ -42,24 +25,6 @@ abstract class SelectionPluginBase extends PluginBase implements SelectionInterf /** * {@inheritdoc} */ - public function getConfiguration() { - return $this->configuration; - } - - /** - * {@inheritdoc} - */ - public function setConfiguration(array $configuration) { - // Merge in defaults. - $this->configuration = NestedArray::mergeDeep( - $this->defaultConfiguration(), - $configuration - ); - } - - /** - * {@inheritdoc} - */ public function calculateDependencies() { return []; } diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 9eab50b9a22..f209fa87b65 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -225,6 +225,8 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { /** * A definite singular/plural name of the type. * + * @var string[] + * * Needed keys: "singular" and "plural". Can also have key: "context". * @code * [ @@ -232,8 +234,7 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { * 'plural' => '@count entities', * 'context' => 'Entity context', * ] - * - * @var string[] + * @endcode * * @see \Drupal\Core\Entity\EntityTypeInterface::getCountLabel() */ diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php index 9723026510e..9e6c2be862d 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php @@ -139,16 +139,18 @@ interface EntityTypeManagerInterface extends PluginManagerInterface, CachedDisco * {@inheritdoc} * * @return \Drupal\Core\Entity\EntityTypeInterface|null + * A plugin definition, or NULL if the plugin ID is invalid and + * $exception_on_invalid is FALSE. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getDefinition($entity_type_id, $exception_on_invalid = TRUE); /** * {@inheritdoc} * * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of plugin definitions (empty array if no definitions were + * found). Keys are plugin IDs. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getDefinitions(); } diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php index f5699e737de..b3d4a008b76 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php @@ -138,7 +138,7 @@ class ValidReferenceConstraintValidator extends ConstraintValidator implements C foreach ($invalid_target_ids as $delta => $target_id) { // Check if any of the invalid existing references are simply not // accessible by the user, in which case they need to be excluded from - // validation + // validation. if (isset($previously_referenced_ids[$target_id]) && isset($existing_entities[$target_id]) && !$existing_entities[$target_id]->access('view')) { continue; } diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php index 30a2220777e..ff3384063ef 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php @@ -204,9 +204,9 @@ interface QueryInterface extends AlterableInterface { * * @param array $headers * An array of headers of the same structure as described in - * template_preprocess_table(). Use a 'specifier' in place of a 'field' to - * specify what to sort on. This can be an entity or a field as described - * in condition(). + * \Drupal\Core\Theme\ThemePreprocess::preprocessTable(). Use a 'specifier' + * in place of a 'field' to specify what to sort on. This can be an entity + * or a field as described in condition(). * * @return $this */ diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index 04b3ead1bb7..af75135015c 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -2226,7 +2226,7 @@ function hook_entity_field_access_alter(array &$grants, array $context) { // take out node module's part in the access handling of this field. We also // don't want to switch node module's grant to // AccessResultInterface::isAllowed() , because the grants of other modules - // should still decide on their own if this field is accessible or not + // should still decide on their own if this field is accessible or not. $grants['node'] = AccessResult::neutral()->inheritCacheability($grants['node']); } } diff --git a/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php b/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php index 28ad2c04451..c5a2b7c8fc8 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php +++ b/core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php @@ -183,7 +183,7 @@ class ActiveLinkResponseFilter implements EventSubscriberInterface { } // Get the HTML: this will be the opening part of a single tag, e.g.: - // <a href="/" data-drupal-link-system-path="<front>"> + // '<a href="/" data-drupal-link-system-path="<front>">'. $tag = substr($html_markup, $pos_tag_start ?? 0, $pos_tag_end - $pos_tag_start + 1); // Parse it into a DOMDocument so we can reliably read and modify diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index a00d087e834..d4185e82669 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -7,6 +7,7 @@ use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\ConfigImporterEvent; use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase; use Drupal\Core\Config\ConfigNameException; +use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ConfigImportModuleUninstallValidatorInterface; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ThemeExtensionList; @@ -48,12 +49,15 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase { * The module extension list. * @param \Traversable $uninstallValidators * The uninstall validator services. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. */ public function __construct( ThemeExtensionList $theme_extension_list, ModuleExtensionList $extension_list_module, #[AutowireIterator(tag: 'module_install.uninstall_validator')] protected \Traversable $uninstallValidators, + protected readonly Connection $connection, ) { $this->themeList = $theme_extension_list; $this->moduleExtensionList = $extension_list_module; @@ -103,6 +107,7 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase { $current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension'); $install_profile = $current_core_extension['profile'] ?? NULL; $new_install_profile = $core_extension['profile'] ?? NULL; + $database_driver_module = $this->connection->getProvider(); // Ensure the profile is not changing. if ($install_profile !== $new_install_profile) { @@ -159,7 +164,10 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase { $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall'); foreach ($uninstalls as $module) { foreach (array_keys($module_data[$module]->required_by) as $dependent_module) { - if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) { + if ($module_data[$dependent_module]->status && + !in_array($dependent_module, $uninstalls, TRUE) && + !in_array($dependent_module, [$install_profile, $database_driver_module], TRUE) + ) { $module_name = $module_data[$module]->info['name']; $dependent_module_name = $module_data[$dependent_module]->info['name']; $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', [ diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php index 67ab669bade..71cba69d6cf 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php @@ -167,8 +167,8 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase { $parameters->add($this->redirectDestination->getAsArray() + ['_exception_statuscode' => $status_code]); $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); - // Only 2xx responses should have their status code overridden; any - // other status code should be passed on: redirects (3xx), error (5xx)… + // Only 2xx responses should have their status code overridden; any other + // status code should be passed on: redirects (3xx), error (5xx) etc. // @see https://www.drupal.org/node/2603788#comment-10504916 if ($response->isSuccessful()) { $response->setStatusCode($status_code); diff --git a/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php b/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php index f2e8d7137c8..c40c9b830fa 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 53cf3c95aa5..599e61f1df8 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -19,16 +19,16 @@ class ModuleHandler implements ModuleHandlerInterface { /** * List of loaded files. * - * @var array + * @var array<string, true> * An associative array whose keys are file paths of loaded files, relative * to the application's root directory. */ protected $loadedFiles; /** - * List of installed modules. + * Installed modules, as extension objects keyed by module name. * - * @var \Drupal\Core\Extension\Extension[] + * @var array<string, \Drupal\Core\Extension\Extension> */ protected $moduleList; @@ -40,9 +40,9 @@ class ModuleHandler implements ModuleHandlerInterface { protected $loaded = FALSE; /** - * List of events which implement an alter hook keyed by hook name(s). + * Lists of callbacks which implement an alter hook, keyed by hook name(s). * - * @var array + * @var array<string, list<callable>> */ protected array $alterEventListeners = []; @@ -56,7 +56,11 @@ class ModuleHandler implements ModuleHandlerInterface { /** * A list of module include file keys. * - * @var array + * The array keys are generated from the arguments to ->loadInclude(). + * Each value is either the path of a file that was successfully included, or + * FALSE if the given file did not exist. + * + * @var array<string, string|false> */ protected $includeFileKeys = []; @@ -230,7 +234,9 @@ class ModuleHandler implements ModuleHandlerInterface { protected function add($type, $name, $path) { $pathname = "$path/$name.info.yml"; $php_file_path = $this->root . "/$path/$name.$type"; - $filename = file_exists($php_file_path) ? "$name.$type" : NULL; + if ($filename = file_exists($php_file_path) ? "$name.$type" : NULL) { + include_once $php_file_path; + } $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename); $this->resetImplementations(); $hook_collector = HookCollectorPass::collectAllHookImplementations([$name => ['pathname' => $pathname]]); @@ -333,6 +339,9 @@ class ModuleHandler implements ModuleHandlerInterface { */ public function resetImplementations() { $this->alterEventListeners = []; + $this->invokeMap = []; + $this->listenersByHook = []; + $this->modulesByHook = []; } /** @@ -619,12 +628,12 @@ class ModuleHandler implements ModuleHandlerInterface { /** * Reorder modules for alters. * - * @param array $modules - * A list of modules. + * @param list<string> $modules + * A list of module names. * @param string $hook * The hook being worked on, for example form_alter. * - * @return array + * @return list<string> * The list, potentially reordered and changed by * hook_module_implements_alter(). */ @@ -730,6 +739,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 +765,7 @@ class ModuleHandler implements ModuleHandlerInterface { } } - return $this->listenersByHook[$hook] ?? []; + return $this->listenersByHook[$hook]; } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index 529fd7275a8..98d5c893867 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -46,7 +46,7 @@ interface ModuleHandlerInterface { /** * Returns the list of currently active modules. * - * @return \Drupal\Core\Extension\Extension[] + * @return array<string, \Drupal\Core\Extension\Extension> * An associative array whose keys are the names of the modules and whose * values are Extension objects. */ @@ -69,7 +69,7 @@ interface ModuleHandlerInterface { /** * Sets an explicit list of currently active modules. * - * @param \Drupal\Core\Extension\Extension[] $module_list + * @param array<string, \Drupal\Core\Extension\Extension> $module_list * An associative array whose keys are the names of the modules and whose * values are Extension objects. */ @@ -106,12 +106,12 @@ interface ModuleHandlerInterface { /** * Determines which modules require and are required by each module. * - * @param array $modules + * @param array<string, \Drupal\Core\Extension\Extension> $modules * An array of module objects keyed by module name. Each object contains * information discovered during a Drupal\Core\Extension\ExtensionDiscovery * scan. * - * @return array + * @return array<string, \Drupal\Core\Extension\Extension> * The same array with the new keys for each module: * - requires: An array with the keys being the modules that this module * requires. @@ -171,7 +171,7 @@ interface ModuleHandlerInterface { /** * Retrieves a list of hooks that are declared through hook_hook_info(). * - * @return array + * @return array<string, array{group: string}> * An associative array whose keys are hook names and whose values are an * associative array containing a group name. The structure of the array * is the same as the return value of hook_hook_info(). @@ -411,7 +411,7 @@ interface ModuleHandlerInterface { * This is useful for tasks such as finding a file that exists in all module * directories. * - * @return array + * @return array<string, string> * An associative array of the directories for all enabled modules, keyed by * the extension machine name. */ 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 00000000000..ec085c0cb5b --- /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 364d672c12f..172193ca855 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 40cd1824106..7eaad4b9ec2 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; @@ -74,7 +75,7 @@ use Drupal\Core\Utility\UpdateException; * Once a module requires 12.0.0 as a minimum version of Drupal the module can * safely remove hook_hook_info() implementations. * - * @return array + * @return array<string, array{group: string}> * An associative array whose keys are hook names and whose values are an * associative array containing: * - group: A string defining the group to which the hook belongs. The module @@ -113,7 +114,7 @@ function hook_hook_info(): array { * you will have to change the order of hook_form_alter() implementation in * hook_module_implements_alter(). * - * @param array $implementations + * @param array<string, string|false> $implementations * An array keyed by the module's name. The value of each item corresponds * to a $group, which is usually FALSE, unless the implementation is in a * file named $module.$group.inc. @@ -787,7 +788,7 @@ function hook_install_tasks_alter(&$tasks, $install_state) { // phpcs:enable function hook_update_N(&$sandbox) { // For non-batch updates, the signature can simply be: - // function hook_update_N() { + // "function hook_update_N() {". // Example function body for adding a field to a database table, which does // not require a batch operation: @@ -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,36 +1133,33 @@ 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 = []; - // Report Drupal version + // Report Drupal version. if ($phase == 'runtime') { $requirements['drupal'] = [ 'title' => t('Drupal'), 'value' => \Drupal::VERSION, - 'severity' => REQUIREMENT_INFO, + 'severity' => RequirementSeverity::Info, ]; } - // Test PHP version + // Test PHP version. $requirements['php'] = [ 'title' => t('PHP'), 'value' => ($phase == 'runtime') ? Link::fromTextAndUrl(phpversion(), Url::fromRoute('system.php'))->toString() : phpversion(), ]; 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 + // Report cron status. if ($phase == 'runtime') { $cron_last = \Drupal::state()->get('system.cron_last'); @@ -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,34 +1227,31 @@ 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 = []; - // Report Drupal version + // Report Drupal version. $requirements['drupal'] = [ 'title' => t('Drupal'), 'value' => \Drupal::VERSION, - 'severity' => REQUIREMENT_INFO, + 'severity' => RequirementSeverity::Info, ]; - // Test PHP version + // Test PHP version. $requirements['php'] = [ 'title' => t('PHP'), 'value' => Link::fromTextAndUrl(phpversion(), Url::fromRoute('system.php'))->toString(), ]; 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 + // Report cron status. $cron_last = \Drupal::state()->get('system.cron_last'); $requirements['cron']['title'] = t('Cron maintenance tasks'); if (is_numeric($cron_last)) { @@ -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,25 +1304,21 @@ 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 = []; - // Test PHP version + // Test PHP version. $requirements['php'] = [ 'title' => t('PHP'), 'value' => phpversion(), ]; 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/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Field/FieldItemInterface.php index f00aebbc046..dbc79b29c54 100644 --- a/core/lib/Drupal/Core/Field/FieldItemInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemInterface.php @@ -249,6 +249,10 @@ interface FieldItemInterface extends ComplexDataInterface { /** * Defines the storage-level settings for this plugin. * + * Setting names defined by this method must not duplicate the setting names + * returned by this plugin's implementation of defaultFieldSettings(), as + * both lists of settings are merged. + * * @return array * A list of default settings, keyed by the setting name. */ @@ -257,6 +261,10 @@ interface FieldItemInterface extends ComplexDataInterface { /** * Defines the field-level settings for this plugin. * + * Setting names defined by this method must not duplicate the setting names + * returned by this plugin's implementation of defaultStorageSettings(), as + * both lists of settings are merged. + * * @return array * A list of default settings, keyed by the setting name. */ diff --git a/core/lib/Drupal/Core/Field/FieldPreprocess.php b/core/lib/Drupal/Core/Field/FieldPreprocess.php new file mode 100644 index 00000000000..4f06ce2403f --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldPreprocess.php @@ -0,0 +1,205 @@ +<?php + +namespace Drupal\Core\Field; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Render\Element; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\AttributeHelper; + +/** + * Field theme preprocess. + * + * @internal + */ +class FieldPreprocess { + + use StringTranslationTrait; + + /** + * Prepares variables for field templates. + * + * Default template: field.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: A render element representing the field. + * - attributes: A string containing the attributes for the wrapping div. + * - title_attributes: A string containing the attributes for the title. + */ + public function preprocessField(array &$variables): void { + $element = $variables['element']; + + // Creating variables for the template. + $variables['entity_type'] = $element['#entity_type']; + $variables['field_name'] = $element['#field_name']; + $variables['field_type'] = $element['#field_type']; + $variables['label_display'] = $element['#label_display']; + + $variables['label_hidden'] = ($element['#label_display'] == 'hidden'); + // Always set the field label - allow themes to decide whether to display + // it. In addition the label should be rendered but hidden to support screen + // readers. + $variables['label'] = $element['#title']; + + $variables['multiple'] = $element['#is_multiple']; + + static $default_attributes; + if (!isset($default_attributes)) { + $default_attributes = new Attribute(); + } + + // Merge attributes when a single-value field has a hidden label. + if ($element['#label_display'] == 'hidden' && !$variables['multiple'] && !empty($element['#items'][0]->_attributes)) { + $variables['attributes'] = AttributeHelper::mergeCollections($variables['attributes'], (array) $element['#items'][0]->_attributes); + } + + // We want other preprocess functions and the theme implementation to have + // fast access to the field item render arrays. The item render array keys + // (deltas) should always be numerically indexed starting from 0, and + // looping on those keys is faster than calling Element::children() or + // looping on all keys within $element, since that requires traversal of all + // element properties. + $variables['items'] = []; + $delta = 0; + while (!empty($element[$delta])) { + $variables['items'][$delta]['content'] = $element[$delta]; + + // Modules can add field item attributes (to + // $item->_attributes) within hook_entity_prepare_view(). Some field + // formatters move those attributes into some nested formatter-specific + // element in order have them rendered on the desired HTML element (e.g., + // on the <a> element of a field item being rendered as a link). Other + // field formatters leave them within + // $element['#items'][$delta]['_attributes'] to be rendered on the item + // wrappers provided by field.html.twig. + $variables['items'][$delta]['attributes'] = !empty($element['#items'][$delta]->_attributes) ? new Attribute($element['#items'][$delta]->_attributes) : clone($default_attributes); + $delta++; + } + } + + /** + * Prepares variables for individual form element templates. + * + * Default template: field-multiple-value-form.html.twig. + * + * Combines multiple values into a table with drag-n-drop reordering. + * + * @param array $variables + * An associative array containing: + * - element: A render element representing the form element. + */ + public function preprocessFieldMultipleValueForm(array &$variables): void { + $element = $variables['element']; + $variables['multiple'] = $element['#cardinality_multiple']; + $variables['attributes'] = $element['#attributes']; + + if ($variables['multiple']) { + $table_id = Html::getUniqueId($element['#field_name'] . '_values'); + // Using table id allows handing nested content with the same field names. + $order_class = $table_id . '-delta-order'; + $header_attributes = new Attribute(['class' => ['label']]); + if (!empty($element['#required'])) { + $header_attributes['class'][] = 'js-form-required'; + $header_attributes['class'][] = 'form-required'; + } + $header = [ + [ + 'data' => [ + '#type' => 'html_tag', + '#tag' => 'h4', + '#value' => $element['#title'], + '#attributes' => $header_attributes, + ], + 'colspan' => 2, + 'class' => ['field-label'], + ], + [], + $this->t('Order', [], ['context' => 'Sort order']), + ]; + $rows = []; + + // Sort items according to '_weight' (needed when the form comes back + // after preview or failed validation). + $items = []; + $variables['button'] = []; + foreach (Element::children($element) as $key) { + if ($key === 'add_more') { + $variables['button'] = &$element[$key]; + } + else { + $items[] = &$element[$key]; + } + } + usort($items, function ($a, $b) { + // Sorts using ['_weight']['#value']. + $a_weight = (is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0); + $b_weight = (is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0); + return $a_weight - $b_weight; + }); + + // Add the items as table rows. + foreach ($items as $item) { + $item['_weight']['#attributes']['class'] = [$order_class]; + + // Remove weight form element from item render array so it can be + // rendered in a separate table column. + $delta_element = $item['_weight']; + unset($item['_weight']); + + // Render actions in a separate column. + $actions = []; + if (isset($item['_actions'])) { + $actions = $item['_actions']; + unset($item['_actions']); + } + + $cells = [ + ['data' => '', 'class' => ['field-multiple-drag']], + ['data' => $item], + ['data' => $actions], + ['data' => $delta_element, 'class' => ['delta-order']], + ]; + $rows[] = [ + 'data' => $cells, + 'class' => ['draggable'], + ]; + } + + $variables['table'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#attributes' => [ + 'id' => $table_id, + 'class' => ['field-multiple-table'], + ], + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => $order_class, + ], + ], + ]; + + if (!empty($element['#description'])) { + $description_id = $element['#attributes']['aria-describedby']; + $description_attributes['id'] = $description_id; + $variables['description']['attributes'] = new Attribute($description_attributes); + $variables['description']['content'] = $element['#description']; + + // Add the description's id to the table aria attributes. + $variables['table']['#attributes']['aria-describedby'] = $element['#attributes']['aria-describedby']; + } + } + else { + $variables['elements'] = []; + foreach (Element::children($element) as $key) { + $variables['elements'][] = $element[$key]; + } + } + } + +} diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php index 6997c1bb8fa..5a7ec04c169 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php @@ -127,11 +127,11 @@ class DecimalItem extends NumericItemBase { $max = is_numeric($settings['max']) ? $settings['max'] : pow(10, ($precision - $scale)) - 1; $min = is_numeric($settings['min']) ? $settings['min'] : -pow(10, ($precision - $scale)) + 1; - // Get the number of decimal digits for the $max + // Get the number of decimal digits for the $max. $decimal_digits = self::getDecimalDigits($max); // Do the same for the min and keep the higher number of decimal digits. $decimal_digits = max(self::getDecimalDigits($min), $decimal_digits); - // If $min = 1.234 and $max = 1.33 then $decimal_digits = 3 + // If $min = 1.234 and $max = 1.33 then $decimal_digits = 3. $scale = rand($decimal_digits, $scale); // @see "Example #1 Calculate a random floating-point number" in diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php index 60920fad60f..2a3e3657354 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\OptGroup; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -56,8 +57,8 @@ abstract class OptionsWidgetBase extends WidgetBase { /** * {@inheritdoc} */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ?ElementInfoManagerInterface $elementInfoManager = NULL) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $property_names = $this->fieldDefinition->getFieldStorageDefinition()->getPropertyNames(); $this->column = $property_names[0]; } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php index 1523e4f618f..2f889324311 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php @@ -6,6 +6,9 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Textfield; +use Drupal\Core\Render\Element\Widget; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -66,17 +69,14 @@ class StringTextfieldWidget extends WidgetBase { /** * {@inheritdoc} */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $element['value'] = $element + [ - '#type' => 'textfield', - '#default_value' => $items[$delta]->value ?? NULL, - '#size' => $this->getSetting('size'), - '#placeholder' => $this->getSetting('placeholder'), - '#maxlength' => $this->getFieldSetting('max_length'), - '#attributes' => ['class' => ['js-text-full', 'text-full']], - ]; - - return $element; + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $value = $widget->createChild('value', Textfield::class, copyProperties: TRUE); + $value->default_value = $items[$delta]->value ?? NULL; + $value->size = $this->getSetting('size'); + $value->placeholder = $this->getSetting('placeholder'); + $value->maxlength = $this->getFieldSetting('max_length'); + $value->attributes = ['class' => ['js-text-full', 'text-full']]; + return $widget; } } diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 5c3da0a1bf5..3ca798a5be4 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -11,6 +11,9 @@ use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; +use Drupal\Core\Render\ElementInfoManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -49,19 +52,32 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, * The widget settings. * @param array $third_party_settings * Any third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, protected ?ElementInfoManagerInterface $elementInfoManager = NULL) { parent::__construct([], $plugin_id, $plugin_definition); $this->fieldDefinition = $field_definition; $this->settings = $settings; $this->thirdPartySettings = $third_party_settings; + if (!$this->elementInfoManager) { + @trigger_error('Calling ' . __METHOD__ . '() without the $elementInfoManager argument is deprecated in drupal:11.3.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3526683', E_USER_DEPRECATED); + $this->elementInfoManager = \Drupal::service('plugin.manager.element_info'); + } } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings']); + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), + ); } /** @@ -461,7 +477,9 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, '#weight' => $delta, ]; - $element = $this->formElement($items, $delta, $element, $form, $form_state); + $formObject = $this->elementInfoManager->fromRenderable($form); + $widget = $this->elementInfoManager->fromRenderable($element, Widget::class); + $element = $this->singleElementObject($items, $delta, $widget, $formObject, $form_state)->toRenderable(); if ($element) { // Allow modules to alter the field widget form element. @@ -484,6 +502,21 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, /** * {@inheritdoc} */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + return $element; + } + + /** + * {@inheritdoc} + */ + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $element = $this->formElement($items, $delta, $widget->toRenderable(), $form->toRenderable(), $form_state); + return $this->elementInfoManager->fromRenderable($element); + } + + /** + * {@inheritdoc} + */ public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { $field_name = $this->fieldDefinition->getName(); @@ -625,10 +658,11 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, * The location of processing information within $form_state. */ protected static function getWidgetStateParents(array $parents, $field_name) { + // phpcs:disable Drupal.Files.LineLength // Field processing data is placed at - // phpcs:ignore Drupal.Files.LineLength - // $form_state->get(['field_storage', '#parents', ...$parents..., '#fields', $field_name]), + // "$form_state->get(['field_storage', '#parents', ...$parents..., '#fields', $field_name])" // to avoid clashes between field names and $parents parts. + // phpcs:enable return array_merge(['field_storage', '#parents'], $parents, ['#fields', $field_name]); } diff --git a/core/lib/Drupal/Core/Field/WidgetInterface.php b/core/lib/Drupal/Core/Field/WidgetInterface.php index ab78308291b..9107bd437f7 100644 --- a/core/lib/Drupal/Core/Field/WidgetInterface.php +++ b/core/lib/Drupal/Core/Field/WidgetInterface.php @@ -3,15 +3,17 @@ namespace Drupal\Core\Field; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; use Symfony\Component\Validator\ConstraintViolationInterface; /** * Interface definition for field widget plugins. * * This interface details the methods that most plugin implementations will want - * to override. See Drupal\Core\Field\WidgetBaseInterface for base + * to override. See \Drupal\Core\Field\WidgetBaseInterface for base * wrapping methods that should most likely be inherited directly from - * Drupal\Core\Field\WidgetBase.. + * \Drupal\Core\Field\WidgetBase. * * @ingroup field_widget */ @@ -104,6 +106,51 @@ interface WidgetInterface extends WidgetBaseInterface { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state); /** + * Returns the form for a single field widget. + * + * Field widget form elements should be based on the passed-in $element, which + * contains the base form element properties derived from the field + * configuration. + * + * The BaseWidget methods will set the weight, field name and delta values for + * each form element. If there are multiple values for this field, the + * formElement() method will be called as many times as needed. + * + * Other modules may alter the form element provided by this function using + * hook_field_widget_single_element_form_alter() or + * hook_field_widget_single_element_WIDGET_TYPE_form_alter(). + * + * The FAPI element callbacks (such as #process, #element_validate, + * #value_callback, etc.) used by the widget do not have access to the + * original $field_definition passed to the widget's constructor. Therefore, + * if any information is needed from that definition by those callbacks, the + * widget implementing this method, or a + * hook_field_widget[_WIDGET_TYPE]_form_alter() implementation, must extract + * the needed properties from the field definition and set them as ad-hoc + * $element['#custom'] properties, for later use by its element callbacks. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * Array of default values for this field. + * @param int $delta + * The order of this item in the array of sub-elements (0, 1, 2, etc.). + * @param \Drupal\Core\Render\Element\Widget $widget + * A widget element. + * @param \Drupal\Core\Render\Element\ElementInterface $form + * The form structure where widgets are being attached to. This might be a + * full form structure, or a sub-element of a larger form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Render\Element\ElementInterface + * The wrapper object. Some widgets need to change the type of it so the + * returned object might not be a Wrapper object. + * + * @see hook_field_widget_single_element_form_alter() + * @see hook_field_widget_single_element_WIDGET_TYPE_form_alter() + */ + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface; + + /** * Assigns a field-level validation error to the right widget sub-element. * * Depending on the widget's internal structure, a field-level validation diff --git a/core/lib/Drupal/Core/File/HtaccessWriter.php b/core/lib/Drupal/Core/File/HtaccessWriter.php index c91510e63dc..52b698b38bf 100644 --- a/core/lib/Drupal/Core/File/HtaccessWriter.php +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php @@ -15,36 +15,33 @@ use Psr\Log\LoggerInterface; class HtaccessWriter implements HtaccessWriterInterface { /** - * The stream wrapper manager. - * - * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface - */ - protected $streamWrapperManager; - - /** - * The logger. - * - * @var \Psr\Log\LoggerInterface - */ - protected $logger; - - /** * Htaccess constructor. * * @param \Psr\Log\LoggerInterface $logger * The logger. - * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager + * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager * The stream wrapper manager. + * @param \Drupal\Core\Site\Settings|null $settings + * The settings. */ - public function __construct(LoggerInterface $logger, StreamWrapperManagerInterface $stream_wrapper_manager) { - $this->logger = $logger; - $this->streamWrapperManager = $stream_wrapper_manager; + public function __construct( + protected LoggerInterface $logger, + protected StreamWrapperManagerInterface $streamWrapperManager, + protected ?Settings $settings = NULL, + ) { + if (!$settings) { + @trigger_error('Calling ' . __METHOD__ . '() without the $settings argument is deprecated in drupal:11.2.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3249817', E_USER_DEPRECATED); + $this->settings = \Drupal::service('settings'); + } } /** * {@inheritdoc} */ public function ensure() { + if (!$this->settings->get('auto_create_htaccess', TRUE)) { + return; + } try { foreach ($this->defaultProtectedDirs() as $protected_dir) { $this->write($protected_dir->getPath(), $protected_dir->isPrivate()); @@ -78,11 +75,15 @@ class HtaccessWriter implements HtaccessWriterInterface { * @internal * * @return bool - * TRUE if the .htaccess file was saved or already exists, FALSE otherwise. + * TRUE if the .htaccess file was saved, already exists or auto-creation is + * disabled, FALSE otherwise. * * @see \Drupal\Component\FileSecurity\FileSecurity::writeHtaccess() */ public function write($directory, $deny_public_access = TRUE, $force_overwrite = FALSE) { + if (!$this->settings->get('auto_create_htaccess', TRUE)) { + return TRUE; + } if (StreamWrapperManager::getScheme($directory)) { $directory = $this->streamWrapperManager->normalizeUri($directory); } diff --git a/core/lib/Drupal/Core/FileTransfer/FTP.php b/core/lib/Drupal/Core/FileTransfer/FTP.php index dd50faa633a..1d349818524 100644 --- a/core/lib/Drupal/Core/FileTransfer/FTP.php +++ b/core/lib/Drupal/Core/FileTransfer/FTP.php @@ -10,6 +10,7 @@ namespace Drupal\Core\FileTransfer; * * @see https://www.drupal.org/node/3512364 */ +// phpcs:ignore Drupal.NamingConventions.ValidClassName.NoUpperAcronyms abstract class FTP extends FileTransfer { /** diff --git a/core/lib/Drupal/Core/FileTransfer/FTPExtension.php b/core/lib/Drupal/Core/FileTransfer/FTPExtension.php index 8c22591984b..9871b1907d2 100644 --- a/core/lib/Drupal/Core/FileTransfer/FTPExtension.php +++ b/core/lib/Drupal/Core/FileTransfer/FTPExtension.php @@ -132,7 +132,7 @@ class FTPExtension extends FTP implements ChmodInterface { if ($this->isDirectory($path) && $recursive) { $file_list = @ftp_nlist($this->connection, $path); if (!$file_list) { - // Empty directory - returns false + // Empty directory - returns false. return; } foreach ($file_list as $file) { diff --git a/core/lib/Drupal/Core/FileTransfer/FileTransfer.php b/core/lib/Drupal/Core/FileTransfer/FileTransfer.php index 56d07b985b6..68ec4f62152 100644 --- a/core/lib/Drupal/Core/FileTransfer/FileTransfer.php +++ b/core/lib/Drupal/Core/FileTransfer/FileTransfer.php @@ -94,12 +94,12 @@ abstract class FileTransfer { * getSettingsForm() method uses any nested settings, the same structure * will be assumed here. * + * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn * @return object * New instance of the appropriate FileTransfer subclass. * * @throws \Drupal\Core\FileTransfer\FileTransferException */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn public static function factory($jail, $settings) { throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.'); } diff --git a/core/lib/Drupal/Core/FileTransfer/Local.php b/core/lib/Drupal/Core/FileTransfer/Local.php index 87dd544c416..28c6b5cca99 100644 --- a/core/lib/Drupal/Core/FileTransfer/Local.php +++ b/core/lib/Drupal/Core/FileTransfer/Local.php @@ -38,7 +38,7 @@ class Local extends FileTransfer implements ChmodInterface { * {@inheritdoc} */ public function connect() { - // No-op + // No-op. } /** diff --git a/core/lib/Drupal/Core/FileTransfer/SSH.php b/core/lib/Drupal/Core/FileTransfer/SSH.php index 6b7d983eef5..883d8b66f63 100644 --- a/core/lib/Drupal/Core/FileTransfer/SSH.php +++ b/core/lib/Drupal/Core/FileTransfer/SSH.php @@ -10,6 +10,7 @@ namespace Drupal\Core\FileTransfer; * * @see https://www.drupal.org/node/3512364 */ +// phpcs:ignore Drupal.NamingConventions.ValidClassName.NoUpperAcronyms class SSH extends FileTransfer implements ChmodInterface { /** diff --git a/core/lib/Drupal/Core/Form/FormBase.php b/core/lib/Drupal/Core/Form/FormBase.php index d88810943ac..44ca953ab07 100644 --- a/core/lib/Drupal/Core/Form/FormBase.php +++ b/core/lib/Drupal/Core/Form/FormBase.php @@ -6,6 +6,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Logger\LoggerChannelTrait; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Routing\RedirectDestinationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; @@ -76,6 +77,13 @@ abstract class FormBase implements FormInterface, ContainerInjectionInterface { protected $routeMatch; /** + * The element info manager. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface + */ + protected ElementInfoManagerInterface $elementInfoManager; + + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { @@ -126,6 +134,29 @@ abstract class FormBase implements FormInterface, ContainerInjectionInterface { } /** + * The element info manager. + * + * @return \Drupal\Core\Render\ElementInfoManagerInterface + * The element info manager. + */ + protected function elementInfoManager(): ElementInfoManagerInterface { + if (!isset($this->elementInfoManager)) { + $this->elementInfoManager = $this->container()->get('plugin.manager.element_info'); + } + return $this->elementInfoManager; + } + + /** + * Sets the element info manager for this form. + * + * @return $this + */ + public function setElementInfoManager(ElementInfoManagerInterface $elementInfoManager): static { + $this->elementInfoManager = $elementInfoManager; + return $this; + } + + /** * Sets the config factory for this form. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index ee3e4893fec..c40b341a7ec 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -602,16 +602,16 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS return; } - // If $form_state->isRebuilding() has been set and input has been + // If $form_state->setRebuild(TRUE) was called and input has been // processed without validation errors, we are in a multi-step workflow - // that is not yet complete. A new $form needs to be constructed based on - // the changes made to $form_state during this request. Normally, a submit - // handler sets $form_state->isRebuilding() if a fully executed form - // requires another step. However, for forms that have not been fully - // executed (e.g., Ajax submissions triggered by non-buttons), there is no - // submit handler to set $form_state->isRebuilding(). It would not make - // sense to redisplay the identical form without an error for the user to - // correct, so we also rebuild error-free non-executed forms, regardless + // that is not yet complete. A new $form needs to be constructed based + // on the changes made to $form_state during this request. + // + // Typically, a submit handler calls $form_state->setRebuild(TRUE) when + // a fully executed form requires another step. However, for forms that + // have not been fully executed (e.g., AJAX submissions triggered by + // non-buttons), there is no submit handler to call setRebuild(). In + // that case, we also rebuild error-free, non-executed forms, regardless // of $form_state->isRebuilding(). // @todo Simplify this logic; considering Ajax and non-HTML front-ends, // along with element-level #submit properties, it makes no sense to @@ -1317,7 +1317,11 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS $buttons[] = $element; $form_state->setButtons($buttons); if ($this->buttonWasClicked($element, $form_state)) { - $form_state->setTriggeringElement($element); + // A correctly formatted request should only have one triggering + // element. + if (empty($form_state->getTriggeringElement())) { + $form_state->setTriggeringElement($element); + } } } } diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php index 7ec819333ab..bb040b6759e 100644 --- a/core/lib/Drupal/Core/Form/FormState.php +++ b/core/lib/Drupal/Core/Form/FormState.php @@ -27,13 +27,13 @@ class FormState implements FormStateInterface { * on a form element may use this reference to access other information in the * form the element is contained in. * + * @var array + * * @see self::getCompleteForm() * * This property is uncacheable. - * - * @var array */ - // phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName, Drupal.Commenting.VariableComment.Missing + // phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName protected $complete_form; /** 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 158010463d2..00000000000 --- 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 34dbc8ebf91..0084e651180 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 47642859a20..00000000000 --- 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/Hook/HookCollectorPass.php b/core/lib/Drupal/Core/Hook/HookCollectorPass.php index 7c0f913ca21..75bbb039414 100644 --- a/core/lib/Drupal/Core/Hook/HookCollectorPass.php +++ b/core/lib/Drupal/Core/Hook/HookCollectorPass.php @@ -403,12 +403,6 @@ class HookCollectorPass implements CompilerPassInterface { $extension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); - if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth() && !$skip_procedural) { - // There is an expectation for all modules and profiles to be loaded. - // .module and .profile files are not supposed to be in subdirectories. - // These need to be loaded even if the module has no procedural hooks. - include_once $filename; - } if ($extension === 'php') { $cached = $hook_file_cache->get($filename); if ($cached) { @@ -512,11 +506,13 @@ class HookCollectorPass implements CompilerPassInterface { $function = $module . '_' . $hook; if ($hook === 'hook_info') { $this->hookInfo[] = $function; + include_once $fileinfo->getPathname(); } elseif ($hook === 'module_implements_alter') { $message = "$function without a #[LegacyModuleImplementsAlter] attribute is deprecated in drupal:11.2.0 and removed in drupal:12.0.0. See https://www.drupal.org/node/3496788"; @trigger_error($message, E_USER_DEPRECATED); $this->moduleImplementsAlters[] = $function; + include_once $fileinfo->getPathname(); } $this->proceduralImplementations[$hook][] = $module; if ($fileinfo->getExtension() !== 'module') { diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php index 5be51f9ca8b..9d9fec5feaa 100644 --- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php @@ -86,7 +86,7 @@ class SiteConfigureForm extends ConfigFormBase { global $install_state; $form['#title'] = $this->t('Configure site'); - // Warn about settings.php permissions risk + // Warn about settings.php permissions risk. $settings_dir = $this->sitePath; $settings_file = $settings_dir . '/settings.php'; // Check that $_POST is empty so we only show this message when the form is diff --git a/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php b/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php index 586195082a5..6d34a92c8ba 100644 --- a/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php @@ -255,7 +255,7 @@ class SiteSettingsForm extends FormBase { // configure one. if (empty(Settings::get('config_sync_directory'))) { if (empty($install_state['config_install_path'])) { - // Add a randomized config directory name to settings.php + // Add a randomized config directory name to settings.php. $config_sync_directory = $this->createRandomConfigDirectory(); } else { diff --git a/core/lib/Drupal/Core/Layout/LayoutDefault.php b/core/lib/Drupal/Core/Layout/LayoutDefault.php index 0c9b4063ad7..e9aac0eb490 100644 --- a/core/lib/Drupal/Core/Layout/LayoutDefault.php +++ b/core/lib/Drupal/Core/Layout/LayoutDefault.php @@ -2,18 +2,17 @@ namespace Drupal\Core\Layout; -use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait; use Drupal\Core\Plugin\ContextAwarePluginTrait; -use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Plugin\ConfigurablePluginBase; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Plugin\PreviewAwarePluginInterface; /** * Provides a default class for Layout plugins. */ -class LayoutDefault extends PluginBase implements LayoutInterface, PluginFormInterface, PreviewAwarePluginInterface { +class LayoutDefault extends ConfigurablePluginBase implements LayoutInterface, PluginFormInterface, PreviewAwarePluginInterface { use ContextAwarePluginAssignmentTrait; use ContextAwarePluginTrait; @@ -35,14 +34,6 @@ class LayoutDefault extends PluginBase implements LayoutInterface, PluginFormInt /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->setConfiguration($configuration); - } - - /** - * {@inheritdoc} - */ public function build(array $regions) { // Ensure $build only contains defined regions and in the order defined. $build = []; @@ -64,20 +55,6 @@ class LayoutDefault extends PluginBase implements LayoutInterface, PluginFormInt /** * {@inheritdoc} */ - public function getConfiguration() { - return $this->configuration; - } - - /** - * {@inheritdoc} - */ - public function setConfiguration(array $configuration) { - $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration); - } - - /** - * {@inheritdoc} - */ public function defaultConfiguration() { return [ 'label' => '', @@ -95,8 +72,8 @@ class LayoutDefault extends PluginBase implements LayoutInterface, PluginFormInt * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition + * The layout plugin definition for this plugin. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getPluginDefinition() { return parent::getPluginDefinition(); } diff --git a/core/lib/Drupal/Core/Layout/LayoutInterface.php b/core/lib/Drupal/Core/Layout/LayoutInterface.php index b3b61a9f74f..2b5089ce807 100644 --- a/core/lib/Drupal/Core/Layout/LayoutInterface.php +++ b/core/lib/Drupal/Core/Layout/LayoutInterface.php @@ -29,8 +29,8 @@ interface LayoutInterface extends PluginInspectionInterface, DerivativeInspectio * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition + * The layout plugin definition for this plugin. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getPluginDefinition(); } diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php index b0fad27e11e..095d25ec3cf 100644 --- a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php +++ b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php @@ -196,8 +196,8 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition[] + * An array of plugin definitions, sorted by category and label. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getSortedDefinitions(?array $definitions = NULL, $label_key = 'label') { // Sort the plugins first by category, then by label. $definitions = $definitions ?? $this->getDefinitions(); @@ -214,8 +214,9 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition[][] + * Keys are category names, and values are arrays of which the keys are + * plugin IDs and the values are plugin definitions. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getGroupedDefinitions(?array $definitions = NULL, $label_key = 'label') { $definitions = $this->getSortedDefinitions($definitions ?? $this->getDefinitions(), $label_key); $grouped_definitions = []; diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php index a2b877335a9..f2da5c269fc 100644 --- a/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php +++ b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php @@ -24,40 +24,43 @@ interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterfac * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutInterface + * The created layout plugin instance. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function createInstance($plugin_id, array $configuration = []); /** * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition|null + * The plugin definition for the given plugin ID, or NULL if it does not + * exist. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getDefinition($plugin_id, $exception_on_invalid = TRUE); /** * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition[] + * An array of plugin definitions (empty array if no definitions were + * found). Keys are plugin IDs. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getDefinitions(); /** * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition[] + * An array of plugin definitions, sorted by category and label. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getSortedDefinitions(?array $definitions = NULL); /** * {@inheritdoc} * * @return \Drupal\Core\Layout\LayoutDefinition[][] + * Keys are category names, and values are arrays of which the keys are + * plugin IDs and the values are plugin definitions. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getGroupedDefinitions(?array $definitions = NULL); /** 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 00000000000..84dfb64c7b2 --- /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 00000000000..8950d44e364 --- /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 00000000000..8a2b5368db0 --- /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 00000000000..c4aa2c736a4 --- /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/MenuPreprocess.php b/core/lib/Drupal/Core/Menu/MenuPreprocess.php new file mode 100644 index 00000000000..a0834704c53 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuPreprocess.php @@ -0,0 +1,73 @@ +<?php + +namespace Drupal\Core\Menu; + +/** + * Menu theme preprocess. + * + * @internal + */ +class MenuPreprocess { + + /** + * Prepares variables for single local task link templates. + * + * Default template: menu-local-task.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: A render element containing: + * - #link: A menu link array with 'title', 'url', and (optionally) + * 'localized_options' keys. + * - #active: A boolean indicating whether the local task is active. + */ + public function preprocessMenuLocalTask(array &$variables): void { + $link = $variables['element']['#link']; + $link += [ + 'localized_options' => [], + ]; + $link_text = $link['title']; + + if (!empty($variables['element']['#active'])) { + $variables['is_active'] = TRUE; + } + + $link['localized_options']['set_active_class'] = TRUE; + + $variables['link'] = [ + '#type' => 'link', + '#title' => $link_text, + '#url' => $link['url'], + '#options' => $link['localized_options'], + ]; + } + + /** + * Prepares variables for single local action link templates. + * + * Default template: menu-local-action.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: A render element containing: + * - #link: A menu link array with 'title', 'url', and (optionally) + * 'localized_options' keys. + */ + public function preprocessMenuLocalAction(array &$variables): void { + $link = $variables['element']['#link']; + $link += [ + 'localized_options' => [], + ]; + $link['localized_options']['attributes']['class'][] = 'button'; + $link['localized_options']['attributes']['class'][] = 'button-action'; + $link['localized_options']['set_active_class'] = TRUE; + + $variables['link'] = [ + '#type' => 'link', + '#title' => $link['title'], + '#options' => $link['localized_options'], + '#url' => $link['url'], + ]; + } + +} diff --git a/core/lib/Drupal/Core/Pager/PagerPreprocess.php b/core/lib/Drupal/Core/Pager/PagerPreprocess.php new file mode 100644 index 00000000000..e8921818440 --- /dev/null +++ b/core/lib/Drupal/Core/Pager/PagerPreprocess.php @@ -0,0 +1,171 @@ +<?php + +namespace Drupal\Core\Pager; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Url; + +/** + * Pager theme preprocess. + * + * @internal + */ +class PagerPreprocess { + + public function __construct(protected PagerManagerInterface $pagerManager) { + } + + /** + * Prepares variables for pager templates. + * + * Default template: pager.html.twig. + * + * Menu callbacks that display paged query results should use #type => pager + * to retrieve a pager control so that users can view other results. Format a + * list of nearby pages with additional query results. + * + * @param array $variables + * An associative array containing: + * - pager: A render element containing: + * - #tags: An array of labels for the controls in the pager. + * - #element: An optional integer to distinguish between multiple pagers + * on one page. + * - #pagination_heading_level: An optional heading level for the pager. + * - #parameters: An associative array of query string parameters to + * append to the pager links. + * - #route_parameters: An associative array of the route parameters. + * - #quantity: The number of pages in the list. + */ + public function preprocessPager(array &$variables): void { + $element = $variables['pager']['#element']; + $parameters = $variables['pager']['#parameters']; + $quantity = empty($variables['pager']['#quantity']) ? 0 : $variables['pager']['#quantity']; + $route_name = $variables['pager']['#route_name']; + $route_parameters = $variables['pager']['#route_parameters'] ?? []; + + $pager = $this->pagerManager->getPager($element); + + // Nothing to do if there is no pager. + if (!isset($pager)) { + return; + } + + $pager_max = $pager->getTotalPages(); + + // Nothing to do if there is only one page. + if ($pager_max <= 1) { + return; + } + + $tags = $variables['pager']['#tags']; + + // Calculate various markers within this pager piece: + // Middle is used to "center" pages around the current page. + $pager_middle = ceil($quantity / 2); + $current_page = $pager->getCurrentPage(); + // The current pager is the page we are currently paged to. + $pager_current = $current_page + 1; + // The first pager is the first page listed by this pager piece (re + // quantity). + $pager_first = $pager_current - $pager_middle + 1; + // The last is the last page listed by this pager piece (re quantity). + $pager_last = $pager_current + $quantity - $pager_middle; + // End of marker calculations. + + // Prepare for generation loop. + $i = $pager_first; + if ($pager_last > $pager_max) { + // Adjust "center" if at end of query. + $i = $i + ($pager_max - $pager_last); + $pager_last = $pager_max; + } + if ($i <= 0) { + // Adjust "center" if at start of query. + $pager_last = $pager_last + (1 - $i); + $i = 1; + } + // End of generation loop preparation. + + // Create the "first" and "previous" links if we are not on the first page. + $items = []; + if ($current_page > 0) { + $items['first'] = []; + $items['first']['attributes'] = new Attribute(); + $options = [ + 'query' => $this->pagerManager->getUpdatedParameters($parameters, $element, 0), + ]; + $items['first']['href'] = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + if (isset($tags[0])) { + $items['first']['text'] = $tags[0]; + } + + $items['previous'] = []; + $items['previous']['attributes'] = new Attribute(); + $options = [ + 'query' => $this->pagerManager->getUpdatedParameters($parameters, $element, $current_page - 1), + ]; + $items['previous']['href'] = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + if (isset($tags[1])) { + $items['previous']['text'] = $tags[1]; + } + } + + // Add an ellipsis if there are further previous pages. + if ($i > 1) { + $variables['ellipses']['previous'] = TRUE; + } + // Now generate the actual pager piece. + for (; $i <= $pager_last && $i <= $pager_max; $i++) { + $options = [ + 'query' => $this->pagerManager->getUpdatedParameters($parameters, $element, $i - 1), + ]; + $items['pages'][$i]['href'] = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $items['pages'][$i]['attributes'] = new Attribute(); + if ($i == $pager_current) { + $variables['current'] = $i; + $items['pages'][$i]['attributes']->setAttribute('aria-current', 'page'); + } + } + // Add an ellipsis if there are further next pages. + if ($i < $pager_max + 1) { + $variables['ellipses']['next'] = TRUE; + } + + // Create the "next" and "last" links if we are not on the last page. + if ($current_page < ($pager_max - 1)) { + $items['next'] = []; + $items['next']['attributes'] = new Attribute(); + $options = [ + 'query' => $this->pagerManager->getUpdatedParameters($parameters, $element, $current_page + 1), + ]; + $items['next']['href'] = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + if (isset($tags[3])) { + $items['next']['text'] = $tags[3]; + } + + $items['last'] = []; + $items['last']['attributes'] = new Attribute(); + $options = [ + 'query' => $this->pagerManager->getUpdatedParameters($parameters, $element, $pager_max - 1), + ]; + $items['last']['href'] = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + if (isset($tags[4])) { + $items['last']['text'] = $tags[4]; + } + } + + $variables['items'] = $items; + $variables['heading_id'] = Html::getUniqueId('pagination-heading'); + $variables['pagination_heading_level'] = $variables['pager']['#pagination_heading_level'] ?? 'h4'; + if (!preg_match('/^h[1-6]$/', $variables['pagination_heading_level'])) { + $variables['pagination_heading_level'] = 'h4'; + } + + // The rendered link needs to play well with any other query parameter used + // on the page, like exposed filters, so for the cacheability all query + // parameters matter. + $variables['#cache']['contexts'][] = 'url.query_args'; + } + +} diff --git a/core/lib/Drupal/Core/Plugin/ConfigurablePluginBase.php b/core/lib/Drupal/Core/Plugin/ConfigurablePluginBase.php new file mode 100644 index 00000000000..0d51a802556 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/ConfigurablePluginBase.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Plugin; + +use Drupal\Component\Plugin\ConfigurableInterface; + +/** + * Base class for plugins that are configurable. + * + * Provides boilerplate methods for implementing + * Drupal\Component\Plugin\ConfigurableInterface. Configurable plugins may + * extend this base class instead of PluginBase. If your plugin must extend a + * different base class, you may use \Drupal\Component\Plugin\ConfigurableTrait + * directly and call setConfiguration() in your constructor. + * + * @see \Drupal\Core\Plugin\ConfigurableTrait + */ +abstract class ConfigurablePluginBase extends PluginBase implements ConfigurableInterface { + use ConfigurableTrait; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->setConfiguration($configuration); + } + +} diff --git a/core/lib/Drupal/Core/Plugin/ConfigurableTrait.php b/core/lib/Drupal/Core/Plugin/ConfigurableTrait.php new file mode 100644 index 00000000000..bfe4a661512 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/ConfigurableTrait.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Plugin; + +use Drupal\Component\Utility\NestedArray; + +/** + * Implementation class for \Drupal\Component\Plugin\ConfigurableInterface. + * + * In order for configurable plugins to maintain their configuration, the + * default configuration must be merged into any explicitly defined + * configuration. This trait provides the appropriate getters and setters to + * handle this logic, removing the need for excess boilerplate. + * + * To use this trait implement ConfigurableInterface and add a constructor. In + * the constructor call the parent constructor and then call setConfiguration(). + * That will merge the explicitly defined plugin configuration and the default + * plugin configuration. + * + * @ingroup Plugin + */ +trait ConfigurableTrait { + + /** + * Configuration information passed into the plugin. + * + * This property is declared in \Drupal\Component\Plugin\PluginBase as well, + * which most classes using this trait will ultimately be extending. It is + * re-declared here to make the trait self-contained and to permit use of the + * trait in classes that do not extend PluginBase. + * + * @var array + */ + protected $configuration; + + /** + * Gets this plugin's configuration. + * + * @return array + * An associative array containing the plugin's configuration. + * + * @see \Drupal\Component\Plugin\ConfigurableInterface::getConfiguration() + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * Sets the configuration for this plugin instance. + * + * The provided configuration is merged with the plugin's default + * configuration. If the same configuration key exists in both configurations, + * then the value in the provided configuration will override the default. + * + * @param array $configuration + * An associative array containing the plugin's configuration. + * + * @return $this + * + * @see \Drupal\Component\Plugin\ConfigurableInterface::setConfiguration() + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeepArray([$this->defaultConfiguration(), $configuration], TRUE); + return $this; + } + + /** + * Gets default configuration for this plugin. + * + * @return array + * An associative array containing the default configuration. + * + * @see \Drupal\Component\Plugin\ConfigurableInterface::defaultConfiguration() + */ + public function defaultConfiguration() { + return []; + } + +} diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php index 89ebd9e5ebe..05737babb9e 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php @@ -17,8 +17,8 @@ interface ContextInterface extends ComponentContextInterface, CacheableDependenc * {@inheritdoc} * * @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface + * The defining characteristic representation of the context. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getContextDefinition(); /** diff --git a/core/lib/Drupal/Core/Plugin/ContextAwarePluginTrait.php b/core/lib/Drupal/Core/Plugin/ContextAwarePluginTrait.php index 42a9e1acc4b..3446744652c 100644 --- a/core/lib/Drupal/Core/Plugin/ContextAwarePluginTrait.php +++ b/core/lib/Drupal/Core/Plugin/ContextAwarePluginTrait.php @@ -122,8 +122,8 @@ trait ContextAwarePluginTrait { * {@inheritdoc} * * @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface[] + * The array of context definitions, keyed by context name. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getContextDefinitions() { $definition = $this->getPluginDefinition(); if ($definition instanceof ContextAwarePluginDefinitionInterface) { @@ -137,8 +137,8 @@ trait ContextAwarePluginTrait { * {@inheritdoc} * * @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface + * The definition against which the context value must validate. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getContextDefinition($name) { $definition = $this->getPluginDefinition(); if ($definition instanceof ContextAwarePluginDefinitionInterface) { diff --git a/core/lib/Drupal/Core/ProxyClass/Cron.php b/core/lib/Drupal/Core/ProxyClass/Cron.php deleted file mode 100644 index 640b9d030c5..00000000000 --- 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 f1db3a342af..9feb9bed8da 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/Recipe/InputConfigurator.php b/core/lib/Drupal/Core/Recipe/InputConfigurator.php index cec8e588611..3d1f871abbb 100644 --- a/core/lib/Drupal/Core/Recipe/InputConfigurator.php +++ b/core/lib/Drupal/Core/Recipe/InputConfigurator.php @@ -171,12 +171,17 @@ final class InputConfigurator { * Returns the default value for an input definition. * * @param array $definition - * An input definition. Must contain a `source` element, which can be either - * 'config' or 'value'. If `source` is 'config', then there must also be a - * `config` element, which is a two-element indexed array containing - * (in order) the name of an extant config object, and a property path - * within that object. If `source` is 'value', then there must be a `value` - * element, which will be returned as-is. + * An input definition. Must contain a `source` element, which can be one + * of `config`, `env`, or `value`: + * - If `source` is `config`, there must also be a `config` element, which + * is a two-element indexed array containing (in order) the name of an + * extant config object, and a property path within that object. + * - If `source` is `env`, there must also be an `env` element, which is + * the name of an environment variable to return. The value will always + * be returned as a string. If the environment variable is not set, an + * empty string will be returned. + * - If `source` is 'value', then there must be a `value` element, which + * will be returned as-is. * * @return mixed * The default value. @@ -192,6 +197,17 @@ final class InputConfigurator { } return $config->get($key); } + elseif ($settings['source'] === 'env') { + // getenv() accepts NULL to return an array of all environment variables, + // but this makes no sense in a recipe. There is no valid situation where + // the name of the environment variable should be empty. + if (empty($settings['env'])) { + throw new \RuntimeException("The name of the environment variable cannot be empty."); + } + // If the variable doesn't exist, getenv() returns FALSE; we can represent + // that as an empty string. + return (string) getenv($settings['env']); + } return $settings['value']; } diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php index 888f54e4f42..aa539c1c70c 100644 --- a/core/lib/Drupal/Core/Recipe/Recipe.php +++ b/core/lib/Drupal/Core/Recipe/Recipe.php @@ -209,7 +209,7 @@ final class Recipe { ]), ], // The `prompt` and `form` elements, though optional, have their - // own sets of constraints, + // own sets of constraints. 'prompt' => new Optional([ new Collection([ 'method' => [ @@ -237,7 +237,7 @@ final class Recipe { 'default' => new Required([ new Collection([ 'source' => new Required([ - new Choice(['value', 'config']), + new Choice(['value', 'config', 'env']), ]), 'value' => new Optional(), 'config' => new Optional([ @@ -250,6 +250,10 @@ final class Recipe { ]), ]), ]), + 'env' => new Optional([ + new Type('string'), + new NotBlank(), + ]), ]), new Callback(self::validateDefaultValueDefinition(...)), ]), diff --git a/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php b/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php index 5f8d8dd32d5..dcfbbad1f17 100644 --- a/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php +++ b/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php @@ -61,10 +61,7 @@ trait RecipeInputFormTrait { * @endcode * * The `#tree` property will always be set to TRUE. - * - * @var array */ - // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis.UnusedVariable, Drupal.Commenting.VariableComment.Missing public array $form = []; /** diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php index 0c63ce2ac49..5ce90c94655 100644 --- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php @@ -88,7 +88,8 @@ class BareHtmlPageRenderer implements BareHtmlPageRendererInterface { * The page to attach to. */ public function systemPageAttachments(array &$page): void { - // Ensure the same CSS is loaded in template_preprocess_maintenance_page(). + // Ensure the same CSS is loaded in + // \Drupal\Core\Theme\ThemePreprocess::preprocessMaintenancePage(). $page['#attached']['library'][] = 'system/base'; if (\Drupal::service('router.admin_context')->isAdminRoute()) { $page['#attached']['library'][] = 'system/admin'; diff --git a/core/lib/Drupal/Core/Render/Element/Button.php b/core/lib/Drupal/Core/Render/Element/Button.php index a8f12e93963..95b78d9c490 100644 --- a/core/lib/Drupal/Core/Render/Element/Button.php +++ b/core/lib/Drupal/Core/Render/Element/Button.php @@ -18,11 +18,15 @@ use Drupal\Core\Render\Element; * using JavaScript or other mechanisms. * * Properties: - * - #limit_validation_errors: An array of form element keys that will block + * + * @property $limit_validation_errors + * An array of form element keys that will block * form submission when validation for these elements or any child elements * fails. Specify an empty array to suppress all form validation errors. - * - #value: The text to be shown on the button. - * - #submit_button: This has a default value of TRUE. If set to FALSE, the + * @property $value + * The text to be shown on the button. + * @property $submit_button + * This has a default value of TRUE. If set to FALSE, the * 'type' attribute is set to 'button.' * * diff --git a/core/lib/Drupal/Core/Render/Element/Checkbox.php b/core/lib/Drupal/Core/Render/Element/Checkbox.php index 65be5d22bf1..220d1c8f969 100644 --- a/core/lib/Drupal/Core/Render/Element/Checkbox.php +++ b/core/lib/Drupal/Core/Render/Element/Checkbox.php @@ -10,7 +10,9 @@ use Drupal\Core\Render\Element; * Provides a form element for a single checkbox. * * Properties: - * - #return_value: The value to return when the checkbox is checked. + * + * @property $return_value + * The value to return when the checkbox is checked. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Checkboxes.php b/core/lib/Drupal/Core/Render/Element/Checkboxes.php index 234f25aa904..c713cc9cb53 100644 --- a/core/lib/Drupal/Core/Render/Element/Checkboxes.php +++ b/core/lib/Drupal/Core/Render/Element/Checkboxes.php @@ -9,7 +9,9 @@ use Drupal\Core\Render\Attribute\FormElement; * Provides a form element for a set of checkboxes. * * Properties: - * - #options: An associative array whose keys are the values returned for each + * + * @property $options + * An associative array whose keys are the values returned for each * checkbox, and whose values are the labels next to each checkbox. The * #options array cannot have a 0 key, as it would not be possible to discern * checked and unchecked states. diff --git a/core/lib/Drupal/Core/Render/Element/Color.php b/core/lib/Drupal/Core/Render/Element/Color.php index 254300d976f..616ed9dbe08 100644 --- a/core/lib/Drupal/Core/Render/Element/Color.php +++ b/core/lib/Drupal/Core/Render/Element/Color.php @@ -11,7 +11,9 @@ use Drupal\Component\Utility\Color as ColorUtility; * Provides a form element for choosing a color. * * Properties: - * - #default_value: Default value, in a format like #ffffff. + * + * @property $default_value + * Default value, in a format like #ffffff. * * Example usage: * @code diff --git a/core/lib/Drupal/Core/Render/Element/ComponentElement.php b/core/lib/Drupal/Core/Render/Element/ComponentElement.php index 62db902c068..befd6aa269b 100644 --- a/core/lib/Drupal/Core/Render/Element/ComponentElement.php +++ b/core/lib/Drupal/Core/Render/Element/ComponentElement.php @@ -11,11 +11,16 @@ use Drupal\Core\Security\DoTrustedCallbackTrait; * Provides a Single-Directory Component render element. * * Properties: - * - #component: The machine name of the component. - * - #variant: (optional) The variant to be used for the component. - * - #props: an associative array where the keys are the names of the + * + * @property $component + * The machine name of the component. + * @property $variant + * (optional) The variant to be used for the component. + * @property $props + * an associative array where the keys are the names of the * component props, and the values are the prop values. - * - #slots: an associative array where the keys are the slot names, and the + * @property $slots + * an associative array where the keys are the slot names, and the * values are the slot values. Expected slot values are renderable arrays. * - #propsAlter: an array of trusted callbacks. These are used to prepare the * context. Typical uses include replacing tokens in props. diff --git a/core/lib/Drupal/Core/Render/Element/Container.php b/core/lib/Drupal/Core/Render/Element/Container.php index d5a2092718e..105f1413efd 100644 --- a/core/lib/Drupal/Core/Render/Element/Container.php +++ b/core/lib/Drupal/Core/Render/Element/Container.php @@ -14,7 +14,9 @@ use Drupal\Core\Render\Element; * an HTML ID. * * Properties: - * - #optional: Indicates whether the container should render when it has no + * + * @property $optional + * Indicates whether the container should render when it has no * visible children. Defaults to FALSE. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Date.php b/core/lib/Drupal/Core/Render/Element/Date.php index 8304167ae70..fbaeae4d600 100644 --- a/core/lib/Drupal/Core/Render/Element/Date.php +++ b/core/lib/Drupal/Core/Render/Element/Date.php @@ -9,13 +9,18 @@ use Drupal\Core\Render\Element; * Provides a form element for date or time selection. * * Properties: - * - #attributes: An associative array containing: + * + * @property $attributes + * An associative array containing: * - type: The type of date field rendered, valid values include 'date', * 'time', 'datetime', and 'datetime-local'. - * - #date_date_format: The date format used in PHP formats. - * - #default_value: A string representing the date formatted as Y-m-d, or + * @property $date_date_format + * The date format used in PHP formats. + * @property $default_value + * A string representing the date formatted as Y-m-d, or * hh:mm for time. - * - #size: The size of the input element in characters. + * @property $size + * The size of the input element in characters. * * @code * $form['expiration'] = [ diff --git a/core/lib/Drupal/Core/Render/Element/Details.php b/core/lib/Drupal/Core/Render/Element/Details.php index 28e7396887d..99a98272704 100644 --- a/core/lib/Drupal/Core/Render/Element/Details.php +++ b/core/lib/Drupal/Core/Render/Element/Details.php @@ -13,10 +13,14 @@ use Drupal\Core\Render\Element; * element, showing or hiding the contained elements. * * Properties: - * - #title: The title of the details container. Defaults to "Details". - * - #open: Indicates whether the container should be open by default. + * + * @property $title + * The title of the details container. Defaults to "Details". + * @property $open + * Indicates whether the container should be open by default. * Defaults to FALSE. - * - #summary_attributes: An array of attributes to apply to the <summary> + * @property $summary_attributes + * An array of attributes to apply to the <summary> * element. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Dropbutton.php b/core/lib/Drupal/Core/Render/Element/Dropbutton.php index 20bb375ea87..4ac2d6dda8a 100644 --- a/core/lib/Drupal/Core/Render/Element/Dropbutton.php +++ b/core/lib/Drupal/Core/Render/Element/Dropbutton.php @@ -17,9 +17,12 @@ use Drupal\Core\Render\Attribute\RenderElement; * element property #links to provide $variables['links'] for theming. * * Properties: - * - #links: An array of links to actions. See template_preprocess_links() for + * + * @property $links + * An array of links to actions. See template_preprocess_links() for * documentation the properties of links in this array. - * - #dropbutton_type: A string defining a type of dropbutton variant for + * @property $dropbutton_type + * A string defining a type of dropbutton variant for * styling proposes. Renders as class `dropbutton--#dropbutton_type`. * * Usage Example: diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php index f7debae9efd..dae671666c3 100644 --- a/core/lib/Drupal/Core/Render/Element/ElementInterface.php +++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php @@ -41,6 +41,22 @@ interface ElementInterface extends PluginInspectionInterface, RenderCallbackInte public function getInfo(); /** + * Initialize storage. + * + * This will only have an effect the first time it is called, once it has + * been called, subsequent calls will not have an effect. + * Only the plugin manager should ever call this method. + * + * @param array $element + * The containing element. + * + * @return $this + * + * @internal + */ + public function initializeInternalStorage(array &$element): static; + + /** * Sets a form element's class attribute. * * Adds 'required' and 'error' classes as needed. @@ -52,4 +68,101 @@ interface ElementInterface extends PluginInspectionInterface, RenderCallbackInte */ public static function setAttributes(&$element, $class = []); + /** + * Returns a render array. + * + * @param string|null $wrapper_key + * An optional wrapper. + * + * @return array|\Drupal\Core\Render\Element\ElementInterface + * A render array. Make sure to take the return value as a reference. + * If $wrapper_key is not given then the stored render element is returned. + * If $wrapper_key is given then [$wrapper_key => &$element] is returned. + * The return value is typed with array|ElementInterface to prepare for + * Drupal 12, where the plan for this method is to return an + * ElementInterface object. If that plan goes through then in Drupal 13 + * support for render arrays will be dropped. + */ + public function &toRenderable(?string $wrapper_key = NULL): array|ElementInterface; + + /** + * Returns child elements. + * + * @return \Traversable<\Drupal\Core\Render\Element\ElementInterface> + * Keys will be children names, values are render objects. + */ + public function getChildren(): \Traversable; + + /** + * Gets a child. + * + * @param int|string|list<int|string> $name + * The name of the child. Can also be an integer. Or a list of these. + * It is an integer when the field API uses the delta for children. + * + * @return ?\Drupal\Core\Render\Element\ElementInterface + * The child render object. + */ + public function getChild(int|string|array $name): ?ElementInterface; + + /** + * Adds a child render element. + * + * @param int|string $name + * The name of the child. Can also be an integer when the child is a delta. + * @param array|\Drupal\Core\Render\Element\ElementInterface $child + * A render array or a render object. + * + * @return \Drupal\Core\Render\Element\ElementInterface + * The added child as a render object. + */ + public function addChild(int|string $name, ElementInterface|array &$child): ElementInterface; + + /** + * Creates a render object and attaches it to the current render object. + * + * @param int|string $name + * The name of the child. Can also be an integer. + * @param class-string<T> $class + * The class of the render object. + * @param array $configuration + * An array of configuration relevant to the render object. + * @param bool $copyProperties + * Copy properties (but not children) from the parent. This is useful for + * widgets for example. + * + * @return T + * The child render object. + * + * @template T of \Drupal\Core\Render\Element\ElementInterface + */ + public function createChild(int|string $name, string $class, array $configuration = [], bool $copyProperties = FALSE): ElementInterface; + + /** + * Removes a child. + * + * @param int|string $name + * The name of the child. Can also be an integer. + * + * @return ?\Drupal\Core\Render\Element\ElementInterface + * The removed render object if any, or NULL if the child could not be + * found. + */ + public function removeChild(int|string $name): ?ElementInterface; + + /** + * Change the type of the element. + * + * Changes only the #type all other properties and children are preserved. + * + * @param class-string<T> $class + * The class of the new render object. + * + * @return T + * The new render object. + * + * @template T of \Drupal\Core\Render\Element\ElementInterface + */ + public function changeType(string $class): ElementInterface; + } diff --git a/core/lib/Drupal/Core/Render/Element/Email.php b/core/lib/Drupal/Core/Render/Element/Email.php index 82e688ce38e..debb2e10f44 100644 --- a/core/lib/Drupal/Core/Render/Element/Email.php +++ b/core/lib/Drupal/Core/Render/Element/Email.php @@ -10,9 +10,13 @@ use Drupal\Core\Render\Element; * Provides a form input element for entering an email address. * * Properties: - * - #default_value: An RFC-compliant email address. - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $default_value + * An RFC-compliant email address. + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Example usage: * @code diff --git a/core/lib/Drupal/Core/Render/Element/File.php b/core/lib/Drupal/Core/Render/Element/File.php index df50492a9ec..9336eda0696 100644 --- a/core/lib/Drupal/Core/Render/Element/File.php +++ b/core/lib/Drupal/Core/Render/Element/File.php @@ -13,8 +13,11 @@ use Drupal\Core\Render\Element; * will automatically be added to the form element. * * Properties: - * - #multiple: A Boolean indicating whether multiple files may be uploaded. - * - #size: The size of the file input element in characters. + * + * @property $multiple + * A Boolean indicating whether multiple files may be uploaded. + * @property $size + * The size of the file input element in characters. * * The value of this form element will always be an array of * \Symfony\Component\HttpFoundation\File\UploadedFile objects, regardless of diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php index 14ab865dd3b..e991e7e7aac 100644 --- a/core/lib/Drupal/Core/Render/Element/FormElement.php +++ b/core/lib/Drupal/Core/Render/Element/FormElement.php @@ -18,7 +18,8 @@ abstract class FormElement extends FormElementBase { * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + $elementInfoManager = \Drupal::service('plugin.manager.element_info'); + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); @trigger_error('\Drupal\Core\Render\Element\FormElement is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\Core\Render\Element\FormElementBase instead. See https://www.drupal.org/node/3436275', E_USER_DEPRECATED); } diff --git a/core/lib/Drupal/Core/Render/Element/FormElementBase.php b/core/lib/Drupal/Core/Render/Element/FormElementBase.php index 581607d716a..320ba9a75a1 100644 --- a/core/lib/Drupal/Core/Render/Element/FormElementBase.php +++ b/core/lib/Drupal/Core/Render/Element/FormElementBase.php @@ -11,6 +11,15 @@ use Drupal\Core\Url; * * Form elements are a subset of render elements, representing elements for * HTML forms, which can be referenced in form arrays. See the + * + * @see \Drupal\Core\Render\Attribute\FormElement + * @see \Drupal\Core\Render\Element\FormElementInterface + * @see \Drupal\Core\Render\ElementInfoManager + * @see \Drupal\Core\Render\Element\RenderElementBase + * @see plugin_api + * + * @ingroup theme_render + * @see \Drupal\Core\Form\FormHelper::processStates() * @link theme_render Render API topic @endlink for an overview of render * arrays and render elements, and the @link form_api Form API topic @endlink * for an overview of forms and form arrays. @@ -30,69 +39,75 @@ use Drupal\Core\Url; * processing of form elements, besides those properties documented in * \Drupal\Core\Render\Element\RenderElementBase (for example: #prefix, * #suffix): - * - #after_build: (array) Array of callables or function names, which are - * called after the element is built. Arguments: $element, $form_state. - * - #ajax: (array) Array of elements to specify Ajax behavior. See - * the @link ajax Ajax API topic @endlink for more information. - * - #array_parents: (string[], read-only) Array of names of all the element's - * parents (including itself) in the render array. See also #parents, #tree. - * - #default_value: Default value for the element. See also #value. - * - #description: (string) Help or description text for the element. In an - * ideal user interface, the #title should be enough to describe the element, - * so most elements should not have a description; if you do need one, make - * sure it is translated. If it is not already wrapped in a safe markup - * object, it will be filtered for XSS safety. - * - #disabled: (bool) If TRUE, the element is shown but does not accept - * user input. - * - #element_validate: (array) Array of callables or function names, which - * are called to validate the input. Arguments: $element, $form_state, $form. - * - #field_prefix: (string) Prefix to display before the HTML input element. - * Should be translated, normally. If it is not already wrapped in a safe - * markup object, will be filtered for XSS safety. Note that the contents of - * this prefix are wrapped in a <span> element, so the value should not - * contain block level HTML. Any HTML added must be valid, i.e. any tags - * introduced inside this prefix must also be terminated within the prefix. - * - #field_suffix: (string) Suffix to display after the HTML input element. - * Should be translated, normally. If it is not already wrapped in a safe - * markup object, will be filtered for XSS safety. Note that the contents of - * this suffix are wrapped in a <span> element, so the value should not - * contain block level HTML. Any HTML must also be valid, i.e. any tags - * introduce inside this suffix must also be terminated within the suffix. - * - #value: (mixed) A value that cannot be edited by the user. - * - #has_garbage_value: (bool) Internal only. Set to TRUE to indicate that the - * #value property of an element should not be used or processed. - * - #input: (bool, internal) Whether or not the element accepts input. - * - #parents: (string[], read-only) Array of names of the element's parents - * for purposes of getting values out of $form_state. See also - * #array_parents, #tree. - * - #process: (array) Array of callables or function names, which are - * called during form building. Arguments: $element, $form_state, $form. - * - #processed: (bool, internal) Set to TRUE when the element is processed. - * - #required: (bool) Whether or not input is required on the element. - * - #states: (array) Information about JavaScript states, such as when to - * hide or show the element based on input on other elements. - * See \Drupal\Core\Form\FormHelper::processStates() for documentation. - * - #title: (string) Title of the form element. Should be translated. - * - #title_display: (string) Where and how to display the #title. Possible - * values: - * - before: Label goes before the element (default for most elements). - * - after: Label goes after the element (default for radio elements). - * - invisible: Label is there but is made invisible using CSS. - * - attribute: Make it the title attribute (hover tooltip). - * - #tree: (bool) TRUE if the values of this element and its children should - * be hierarchical in $form_state; FALSE if the values should be flat. - * See also #parents, #array_parents. - * - #value_callback: (callable) Callable or function name, which is called - * to transform the raw user input to the element's value. Arguments: - * $element, $input, $form_state. - * - * @see \Drupal\Core\Render\Attribute\FormElement - * @see \Drupal\Core\Render\Element\FormElementInterface - * @see \Drupal\Core\Render\ElementInfoManager - * @see \Drupal\Core\Render\Element\RenderElementBase - * @see plugin_api - * - * @ingroup theme_render + * @property array $after_build + * Array of callables or function names, which are called after the element + * is built. Arguments: $element, $form_state. + * @property array $ajax + * Array of elements to specify Ajax behavior. See the @link ajax Ajax API + * topic @endlink for more information. + * @property array<string> $array_parents + * Array of names of all the element's parents (including itself) in the + * render array. See also #parents, #tree. + * @property mixed $default_value + * Default value for the element. See also #value. + * @property scalar|\Stringable|\Drupal\Core\Render\RenderableInterface|array $description + * Help or description text for the element. In an ideal user interface, + * the #title should be enough to describe the element, so most elements + * should not have a description; if you do need one, make sure it is + * translated. It can be anything that Twig can print and will be filtered + * for XSS as necessary. + * @property bool $disabled + * If TRUE, the element is shown but does not accept user input. + * @property array<callable> $element_validate + * Array of callables or function names, which are called to validate the + * input. Arguments: $element, $form_state, $form. + * @property string $field_prefix + * Prefix to display before the HTML input element. Should be translated, + * normally. If it is not already wrapped in a safe markup object, will be + * filtered for XSS safety. Note that the contents of this prefix are + * wrapped in a <span> element, so the value should not contain block level + * HTML. Any HTML added must be valid, i.e. any tags introduced inside this + * prefix must also be terminated within the prefix. + * @property string $field_suffix + * Suffix to display after the HTML input element. Should be translated, + * normally. If it is not already wrapped in a safe markup object, will be + * filtered for XSS safety. Note that the contents of this suffix are + * wrapped in a <span> element, so the value should not contain block + * level HTML. Any HTML must also be valid, i.e. any tags introduce inside + * this suffix must also be terminated within the suffix. + * @property mixed $value + * A value that cannot be edited by the user. + * @property bool $has_garbage_value + * @internal + * Set to TRUE to indicate that the #value property of an + * element should not be used or processed. + * @property bool $input + * @internal + * Whether the element accepts input. + * @property array<string> $parents + * Array of names of the element's parents for purposes of getting values + * out of $form_state. See also #array_parents, #tree. + * @property array $process + * Array of callables or function names, which are called during form + * building. Arguments: $element, $form_state, $form. + * @property bool, internal $processed + * Set to TRUE when the element is processed. + * @property bool $required + * Whether input is required on the element. + * @property array $states + * Information about JavaScript states, such as when to hide or show the + * element based on input on other elements. + * @property string $title + * Title of the form element. Should be translated. + * @property \Drupal\Core\Render\Element\TitleDisplay $title_display + * Where and how to display the #title. + * @property bool $tree + * TRUE if the values of this element and its children should be hierarchical + * in $form_state; FALSE if the values should be flat. See also #parents, + * #array_parents. + * @property callable $value_callback + * Callable or function name, which is called to transform the raw user + * input to the element's value. Arguments: $element, $input, $form_state. */ abstract class FormElementBase extends RenderElementBase implements FormElementInterface { diff --git a/core/lib/Drupal/Core/Render/Element/Generic.php b/core/lib/Drupal/Core/Render/Element/Generic.php new file mode 100644 index 00000000000..4a0b6f09ebd --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Generic.php @@ -0,0 +1,31 @@ +<?php + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Attribute\RenderElement; + +/** + * Provides a generic, empty element. + * + * Manually creating this element is not necessary; however, the system + * often needs to convert render arrays that do not have a type. While + * arrays without a #type are valid PHP code, it is not possible to create + * an object without a class. + */ +#[RenderElement('generic')] +class Generic extends RenderElementBase { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return []; + } + + /** + * {@inheritdoc} + */ + protected function setType(): void { + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Hidden.php b/core/lib/Drupal/Core/Render/Element/Hidden.php index db3b23a08cc..1c8856143e6 100644 --- a/core/lib/Drupal/Core/Render/Element/Hidden.php +++ b/core/lib/Drupal/Core/Render/Element/Hidden.php @@ -11,9 +11,12 @@ use Drupal\Core\Render\Element; * Specify either #default_value or #value but not both. * * Properties: - * - #default_value: The initial value of the form element. JavaScript may + * + * @property $default_value + * The initial value of the form element. JavaScript may * alter the value prior to submission. - * - #value: The value of the form element. The Form API ensures that this + * @property $value + * The value of the form element. The Form API ensures that this * value remains unchanged by the browser. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/HtmlTag.php b/core/lib/Drupal/Core/Render/Element/HtmlTag.php index 3b4dbd25d46..277f121df2c 100644 --- a/core/lib/Drupal/Core/Render/Element/HtmlTag.php +++ b/core/lib/Drupal/Core/Render/Element/HtmlTag.php @@ -13,12 +13,17 @@ use Drupal\Core\Template\Attribute; * Provides a render element for any HTML tag, with properties and value. * * Properties: - * - #tag: The tag name to output. - * - #attributes: (array, optional) HTML attributes to apply to the tag. The + * + * @property $tag + * The tag name to output. + * @property $attributes + * (array, optional) HTML attributes to apply to the tag. The * attributes are escaped, see \Drupal\Core\Template\Attribute. - * - #value: (string|MarkupInterface, optional) The textual contents of the tag. + * @property $value + * (string|MarkupInterface, optional) The textual contents of the tag. * Strings will be XSS admin filtered. - * - #noscript: (bool, optional) When set to TRUE, the markup + * @property $noscript + * (bool, optional) When set to TRUE, the markup * (including any prefix or suffix) will be wrapped in a <noscript> element. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Icon.php b/core/lib/Drupal/Core/Render/Element/Icon.php index d903f0d1700..e71771044e7 100644 --- a/core/lib/Drupal/Core/Render/Element/Icon.php +++ b/core/lib/Drupal/Core/Render/Element/Icon.php @@ -12,9 +12,13 @@ use Drupal\Core\Theme\Icon\IconDefinition; * Provides a render element to display an icon. * * Properties: - * - #pack_id: (string) Icon Pack provider plugin id. - * - #icon_id: (string) Name of the icon. - * - #settings: (array) Settings sent to the inline Twig template. + * + * @property $pack_id + * (string) Icon Pack provider plugin id. + * @property $icon_id + * (string) Name of the icon. + * @property $settings + * (array) Settings sent to the inline Twig template. * * Usage Example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/InlineTemplate.php b/core/lib/Drupal/Core/Render/Element/InlineTemplate.php index 2003e1bfb2a..68f5fbf7091 100644 --- a/core/lib/Drupal/Core/Render/Element/InlineTemplate.php +++ b/core/lib/Drupal/Core/Render/Element/InlineTemplate.php @@ -8,8 +8,11 @@ use Drupal\Core\Render\Attribute\RenderElement; * Provides a render element where the user supplies an in-line Twig template. * * Properties: - * - #template: The inline Twig template used to render the element. - * - #context: (array) The variables to substitute into the Twig template. + * + * @property $template + * The inline Twig template used to render the element. + * @property $context + * (array) The variables to substitute into the Twig template. * Each variable may be a string or a render array. * * Usage example: @@ -46,8 +49,8 @@ class InlineTemplate extends RenderElementBase { * The element. * * @return array + * The modified element with the rendered #markup in it. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public static function preRenderInlineTemplate($element) { /** @var \Drupal\Core\Template\TwigEnvironment $environment */ $environment = \Drupal::service('twig'); diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php index 2a00e0526b6..b04704b1681 100644 --- a/core/lib/Drupal/Core/Render/Element/Link.php +++ b/core/lib/Drupal/Core/Render/Element/Link.php @@ -14,8 +14,11 @@ use Drupal\Core\Url as CoreUrl; * Provides a link render element. * * Properties: - * - #title: The link text. - * - #url: \Drupal\Core\Url object containing URL information pointing to an + * + * @property $title + * The link text. + * @property $url + * \Drupal\Core\Url object containing URL information pointing to an * internal or external link. See \Drupal\Core\Utility\LinkGeneratorInterface. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php index ec03e46616e..93fbfe5be7f 100644 --- a/core/lib/Drupal/Core/Render/Element/MachineName.php +++ b/core/lib/Drupal/Core/Render/Element/MachineName.php @@ -21,7 +21,9 @@ use Drupal\Core\Render\Attribute\FormElement; * machine name form element. * * Properties: - * - #machine_name: An associative array containing: + * + * @property $machine_name + * An associative array containing: * - exists: A callable to invoke for checking whether a submitted machine * name value already exists. The arguments passed to the callback will be: * - The submitted value. @@ -49,9 +51,11 @@ use Drupal\Core\Render\Attribute\FormElement; * form element rather than in the suffix of the source element. The source * element must appear in the form structure before this element. Defaults * to FALSE. - * - #maxlength: (optional) Maximum allowed length of the machine name. Defaults + * @property $maxlength + * (optional) Maximum allowed length of the machine name. Defaults * to 64. - * - #disabled: (optional) Should be set to TRUE if an existing machine name + * @property $disabled + * (optional) Should be set to TRUE if an existing machine name * must not be changed after initial creation. * * Usage example: @@ -296,7 +300,7 @@ class MachineName extends Textfield { return $overrides[$langcode]; } - $file = dirname(__DIR__, 3) . '/Component/Transliteration/data' . '/' . preg_replace('/[^a-zA-Z\-]/', '', $langcode) . '.php'; + $file = dirname(__DIR__, 3) . '/Component/Transliteration/data/' . preg_replace('/[^a-zA-Z\-]/', '', $langcode) . '.php'; $overrides[$langcode] = []; if (is_file($file)) { diff --git a/core/lib/Drupal/Core/Render/Element/MoreLink.php b/core/lib/Drupal/Core/Render/Element/MoreLink.php index 9d990942953..57303585d5b 100644 --- a/core/lib/Drupal/Core/Render/Element/MoreLink.php +++ b/core/lib/Drupal/Core/Render/Element/MoreLink.php @@ -8,7 +8,9 @@ use Drupal\Core\Render\Attribute\RenderElement; * Provides a link render element for a "more" link, like those used in blocks. * * Properties: - * - #title: The text of the link to generate (defaults to 'More'). + * + * @property $title + * The text of the link to generate (defaults to 'More'). * * See \Drupal\Core\Render\Element\Link for additional properties. * diff --git a/core/lib/Drupal/Core/Render/Element/Number.php b/core/lib/Drupal/Core/Render/Element/Number.php index 7096b2925be..2be176fd363 100644 --- a/core/lib/Drupal/Core/Render/Element/Number.php +++ b/core/lib/Drupal/Core/Render/Element/Number.php @@ -11,10 +11,15 @@ use Drupal\Component\Utility\Number as NumberUtility; * Provides a form element for numeric input, with special numeric validation. * * Properties: - * - #default_value: A valid floating point number. - * - #min: Minimum value. - * - #max: Maximum value. - * - #step: Ensures that the number is an even multiple of step, offset by #min + * + * @property $default_value + * A valid floating point number. + * @property $min + * Minimum value. + * @property $max + * Maximum value. + * @property $step + * Ensures that the number is an even multiple of step, offset by #min * if specified. A #min of 1 and a #step of 2 would allow values of 1, 3, 5, * etc. * diff --git a/core/lib/Drupal/Core/Render/Element/Page.php b/core/lib/Drupal/Core/Render/Element/Page.php index f271c6adfcf..cc7cd16dc1e 100644 --- a/core/lib/Drupal/Core/Render/Element/Page.php +++ b/core/lib/Drupal/Core/Render/Element/Page.php @@ -8,7 +8,7 @@ use Drupal\Core\Render\Attribute\RenderElement; * Provides a render element for the content of an HTML page. * * This represents the "main part" of the HTML page's body; see html.html.twig. - */ + */ #[RenderElement('page')] class Page extends RenderElementBase { diff --git a/core/lib/Drupal/Core/Render/Element/Pager.php b/core/lib/Drupal/Core/Render/Element/Pager.php index a31c7f2f371..f6f4b05a324 100644 --- a/core/lib/Drupal/Core/Render/Element/Pager.php +++ b/core/lib/Drupal/Core/Render/Element/Pager.php @@ -13,15 +13,22 @@ use Drupal\Core\Render\Attribute\RenderElement; * extend a select query with \Drupal\Core\Database\Query\PagerSelectExtender. * * Properties: - * - #element: (optional, int) The pager ID, to distinguish between multiple + * + * @property $element + * (optional, int) The pager ID, to distinguish between multiple * pagers on the same page (defaults to 0). - * - #pagination_heading_level: (optional) A heading level for the pager. - * - #parameters: (optional) An associative array of query string parameters to + * @property $pagination_heading_level + * (optional) A heading level for the pager. + * @property $parameters + * (optional) An associative array of query string parameters to * append to the pager. - * - #quantity: The maximum number of numbered page links to create (defaults + * @property $quantity + * The maximum number of numbered page links to create (defaults * to 9). - * - #tags: (optional) An array of labels for the controls in the pages. - * - #route_name: (optional) The name of the route to be used to build pager + * @property $tags + * (optional) An array of labels for the controls in the pages. + * @property $route_name + * (optional) The name of the route to be used to build pager * links. Defaults to '<none>', which will make links relative to the current * URL. This makes the page more effectively cacheable. * @@ -73,13 +80,14 @@ class Pager extends RenderElementBase { * The render array with cache contexts added. */ public static function preRenderPager(array $pager) { - // Note: the default pager theme process function - // template_preprocess_pager() also calls + // Note: the default pager theme preprocess function + // \Drupal\Core\Pager\PagerPreprocess::preprocessPager() also calls // \Drupal\Core\Pager\PagerManagerInterface::getUpdatedParameters(), which // maintains the existing query string. Therefore - // template_preprocess_pager() adds the 'url.query_args' cache context, - // which causes the more specific cache context below to be optimized away. - // In other themes, however, that may not be the case. + // \Drupal\Core\Pager\PagerPreprocess::preprocessPager() adds the + // 'url.query_args' cache context which causes the more specific cache + // context below to be optimized away. In other themes, however, that may + // not be the case. $pager['#cache']['contexts'][] = 'url.query_args.pagers:' . $pager['#element']; return $pager; } diff --git a/core/lib/Drupal/Core/Render/Element/Password.php b/core/lib/Drupal/Core/Render/Element/Password.php index 0c2e99d054b..3b0b5d3a378 100644 --- a/core/lib/Drupal/Core/Render/Element/Password.php +++ b/core/lib/Drupal/Core/Render/Element/Password.php @@ -10,8 +10,11 @@ use Drupal\Core\Render\Element; * Provides a form element for entering a password, with hidden text. * * Properties: - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php index 3ca411682a5..95a1677c7f4 100644 --- a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php +++ b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php @@ -12,7 +12,9 @@ use Drupal\Core\Render\Attribute\FormElement; * entered passwords match. * * Properties: - * - #size: The size of the input element in characters. + * + * @property $size + * The size of the input element in characters. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Radios.php b/core/lib/Drupal/Core/Render/Element/Radios.php index 7dc1815b36e..08880ce1689 100644 --- a/core/lib/Drupal/Core/Render/Element/Radios.php +++ b/core/lib/Drupal/Core/Render/Element/Radios.php @@ -10,7 +10,9 @@ use Drupal\Component\Utility\Html as HtmlUtility; * Provides a form element for a set of radio buttons. * * Properties: - * - #options: An associative array, where the keys are the returned values for + * + * @property $options + * An associative array, where the keys are the returned values for * each radio button, and the values are the labels next to each radio button. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Range.php b/core/lib/Drupal/Core/Render/Element/Range.php index 0588dd55523..210b19c4ca3 100644 --- a/core/lib/Drupal/Core/Render/Element/Range.php +++ b/core/lib/Drupal/Core/Render/Element/Range.php @@ -12,8 +12,11 @@ use Drupal\Core\Render\Element; * Provides an HTML5 input element with type of "range". * * Properties: - * - #min: Minimum value (defaults to 0). - * - #max: Maximum value (defaults to 100). + * + * @property $min + * Minimum value (defaults to 0). + * @property $max + * Maximum value (defaults to 100). * Refer to \Drupal\Core\Render\Element\Number for additional properties. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index c37338c3219..fd29573aad1 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -18,7 +18,8 @@ abstract class RenderElement extends RenderElementBase { * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + $elementInfoManager = \Drupal::service('plugin.manager.element_info'); + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); @trigger_error('\Drupal\Core\Render\Element\RenderElement is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\Core\Render\Element\RenderElementBase instead. See https://www.drupal.org/node/3436275', E_USER_DEPRECATED); } diff --git a/core/lib/Drupal/Core/Render/Element/RenderElementBase.php b/core/lib/Drupal/Core/Render/Element/RenderElementBase.php index 451df8a0e36..a2562144b2d 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElementBase.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElementBase.php @@ -2,12 +2,16 @@ namespace Drupal\Core\Render\Element; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a base class for render element plugins. @@ -37,85 +41,110 @@ use Drupal\Core\Url; * \Drupal\Core\StringTranslation\TranslatableMarkup objects instead. * * 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, - * 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. - * Argument: element. - * - #allowed_tags: (array) Array of allowed HTML tags for XSS filtering of - * #markup, #prefix, #suffix, etc. - * - #attached: (array) Array of attachments associated with the element. - * See the "Attaching libraries in render arrays" section of the + * elements. These are available as properties on the render element (handled + * by magic setter/getter) and also the render array starting with a # + * character. For example $element['#access'] or $elementObject->access. + * + * @property bool|\Drupal\Core\Access\AccessResultInterface $access + * 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. + * @property callable $access_callback + * A callable or function name to call to check access. Argument: element. + * @property array<string> $allowed_tags + * Array of allowed HTML tags for XSS filtering of #markup, #prefix, #suffix, + * etc. + * @property array $attached + * Array of attachments associated with the element. See the "Attaching + * libraries in render arrays" section of the * @link theme_render Render API topic @endlink for an overview, and * \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments - * for a list of what this can contain. Besides this list, it may also contain - * a 'placeholders' element; see the Placeholders section of the + * for a list of what this can contain. Besides this list, it may also + * contain a 'placeholders' element; see the Placeholders section of the * @link theme_render Render API topic @endlink for an overview. - * - #attributes: (array) HTML attributes for the element. The first-level - * keys are the attribute names, such as 'class', and the attributes are - * usually given as an array of string values to apply to that attribute - * (the rendering system will concatenate them together into a string in - * the HTML output). - * - #cache: (array) Cache information. See the Caching section of the + * @property array $attributes + * HTML attributes for the element. The first-level keys are the attribute + * names, such as 'class', and the attributes are usually given as an array + * of string values to apply to that attribute (the rendering system will + * concatenate them together into a string in the HTML output). + * @property array $cache + * Cache information. See the Caching section of the * @link theme_render Render API topic @endlink for more information. - * - #children: (array, internal) Array of child elements of this element. - * Set and used during the rendering process. - * - #create_placeholder: (bool) TRUE if the element has placeholders that - * are generated by #lazy_builder callbacks. Set internally during rendering - * in some cases. See also #attached. - * - #defaults_loaded: (bool) Set to TRUE during rendering when the defaults - * for the element #type have been added to the element. - * - #value: (mixed) A value that cannot be edited by the user. - * - #has_garbage_value: (bool) Internal only. Set to TRUE to indicate that the - * #value property of an element should not be used or processed. - * - #id: (string) The HTML ID on the element. This is automatically set for - * form elements, but not for all render elements; you can override the - * default value or add an ID by setting this property. - * - #lazy_builder: (array) Array whose first element is a lazy building - * callback (callable), and whose second is an array of scalar arguments to - * the callback. To use lazy building, the element array must be very - * simple: no properties except #lazy_builder, #cache, #weight, and - * #create_placeholder, and no children. A lazy builder callback typically - * generates #markup and/or placeholders; see the Placeholders section of the + * @property array $children + * Array of child elements of this element. Set and used during the + * rendering process. + * @property bool $create_placeholder + * TRUE if the element has placeholders that are generated by #lazy_builder + * callbacks. Set internally during rendering in some cases. See also + * #attached. + * @property bool $defaults_loaded + * Set to TRUE during rendering when the defaults for the element #type have + * been added to the element. + * @property mixed $value + * A value that cannot be edited by the user. + * @property bool $has_garbage_value + * @internal + * Set to TRUE to indicate that the #value property of an element should not + * be used or processed. + * @property string $id + * The HTML ID on the element. This is automatically set for form elements, + * but not for all render elements; you can override the default value or + * add an ID by setting this property. + * @property array<callable, array<scalar>> $lazy_builder + * Array whose first element is a lazy building callback (callable), and + * whose second is an array of scalar arguments to the callback. To use + * lazy building, the element array must be very simple: no properties + * except #lazy_builder, #cache, #weight, and #create_placeholder, and no + * children. A lazy builder callback typically generates #markup and/or + * placeholders; see the Placeholders section of the * @link theme_render Render API topic @endlink for information about * placeholders. - * - #markup: (string) During rendering, this will be set to the HTML markup - * output. It can also be set on input, as a fallback if there is no - * theming for the element. This will be filtered for XSS problems during - * rendering; see also #plain_text and #allowed_tags. - * - #plain_text: (string) Elements can set this instead of #markup. All HTML - * tags will be escaped in this text, and if both #plain_text and #markup - * are provided, #plain_text is used. - * - #post_render: (array) Array of callables or function names, which are - * called after the element is rendered. Arguments: rendered element string, - * children. - * - #pre_render: (array) Array of callables or function names, which are - * called just before the element is rendered. Argument: $element. - * Return value: an altered $element. - * - #prefix: (string) Text to render before the entire element output. See - * also #suffix. If it is not already wrapped in a safe markup object, will - * be filtered for XSS safety. - * - #printed: (bool, internal) Set to TRUE when an element and its children - * have been rendered. - * - #render_children: (bool, internal) Set to FALSE by the rendering process - * if the #theme call should be bypassed (normally, the theme is used to - * render the children). Set to TRUE by the rendering process if the children - * should be rendered by rendering each one separately and concatenating. - * - #suffix: (string) Text to render after the entire element output. See - * also #prefix. If it is not already wrapped in a safe markup object, will - * be filtered for XSS safety. - * - #theme: (string) Name of the theme hook to use to render the element. - * A default is generally set for elements; users of the element can - * override this (typically by adding __suggestion suffixes). - * - #theme_wrappers: (array) Array of theme hooks, which are invoked - * after the element and children are rendered, and before #post_render - * functions. - * - #type: (string) The machine name of the type of render/form element. - * - #weight: (float) The sort order for rendering, with lower numbers coming - * before higher numbers. Default if not provided is zero; elements with - * the same weight are rendered in the order they appear in the render - * array. + * @property string $markup + * During rendering, this will be set to the HTML markup output. It can also + * be set on input, as a fallback if there is no theming for the element. + * This will be filtered for XSS problems during rendering; see also + * #plain_text and #allowed_tags. + * @property string $plain_text + * Elements can set this instead of #markup. All HTML tags will be escaped + * in this text, and if both #plain_text and #markup are provided, + * #plain_text is used. + * @property array<callable> $post_render + * Array of callables or function names, which are called after the element + * is rendered. Arguments: rendered element string, children. + * @property array<callable> $pre_render + * Array of callables or function names, which are called just before the + * element is rendered. Argument: $element. Return value: an altered + * $element. + * @property string $prefix + * Text to render before the entire element output. See also #suffix. If it + * is not already wrapped in a safe markup object, will be filtered for XSS + * safety. + * @property bool $printed + * Set to TRUE when an element and its children have been rendered. + * @property bool $render_children + * @internal + * Set to FALSE by the rendering process if the #theme call should be + * bypassed (normally, the theme is used to render the children). Set to + * TRUE by the rendering process if the children should be rendered by + * rendering each one separately and concatenating. + * @property string $suffix + * Text to render after the entire element output. See also #prefix. If it + * is not already wrapped in a safe markup object, will be filtered for XSS + * safety. + * @property string $theme + * Name of the theme hook to use to render the element. A default is + * generally set for elements; users of the element can override this + * (typically by adding __suggestion suffixes). + * @property array<string> $theme_wrappers + * Array of theme hooks, which are invoked after the element and children + * are rendered, and before #post_render functions. + * @property string $type + * The machine name of the type of render/form element. + * @property float $weight + * The sort order for rendering, with lower numbers coming before higher + * numbers. Default if not provided is zero; elements with the same weight + * are rendered in the order they appear in the render array. * * @see \Drupal\Core\Render\Attribute\RenderElement * @see \Drupal\Core\Render\ElementInterface @@ -124,7 +153,60 @@ use Drupal\Core\Url; * * @ingroup theme_render */ -abstract class RenderElementBase extends PluginBase implements ElementInterface { +abstract class RenderElementBase extends PluginBase implements ElementInterface, ContainerFactoryPluginInterface { + + /** + * Constructs a new render element object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Render\ElementInfoManagerInterface|null $elementInfoManager + * The element info manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, protected ?ElementInfoManagerInterface $elementInfoManager = NULL) { + if (!$this->elementInfoManager) { + @trigger_error('Calling ' . __METHOD__ . '() without the $elementInfoManager argument is deprecated in drupal:11.3.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3526683', E_USER_DEPRECATED); + $this->elementInfoManager = \Drupal::service('plugin.manager.element_info'); + } + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.element_info'), + ); + } + + /** + * The storage. + * + * @internal + */ + protected array $storage = []; + + /** + * The parent element. + * + * @var static + */ + protected ElementInterface $renderParent; + + /** + * The parent key. + * + * @var string + */ + protected string $renderParentName; /** * {@inheritdoc} @@ -471,4 +553,183 @@ abstract class RenderElementBase extends PluginBase implements ElementInterface return $element; } + /** + * {@inheritdoc} + */ + public function initializeInternalStorage(array &$element): static { + $this->storage = &$element; + $element['##object'] = $this; + $this->setType(); + return $this; + } + + /** + * Set type on initialize. + * + * There is no need to either call or override this method. + * + * @internal + */ + protected function setType(): void { + $this->storage['#type'] = $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function &toRenderable(?string $wrapper_key = NULL): array { + if ($wrapper_key) { + $return = [$wrapper_key => &$this->storage]; + return $return; + } + return $this->storage; + } + + /** + * Magic method: Sets a property value. + * + * @param string $name + * The name of a property. $value will be accessible with $this->name and + * also $element['#' . $name] where the element is the render array this + * object was created from. + * @param mixed $value + * The value. + */ + public function __set(string $name, $value): void { + $this->storage['#' . $name] = $value; + } + + /** + * Magic method: gets a property value. + * + * @param string $name + * The name of the property. $value is accessible with $this->name and + * also $element['#' . $name] where the element is the render array this + * object was created from. + * + * @return mixed + * The value. + */ + public function __get(string $name): mixed { + return $this->storage['#' . $name] ?? NULL; + } + + /** + * Magic method: unsets a property value. + * + * @param string $name + * The name of the property. This will unset both the object property + * $this->name and also the render key $element['#' . $name] where the + * element is the render array this object was created from. + */ + public function __unset(string $name): void { + unset($this->storage['#' . $name]); + } + + /** + * Magic method: checks if a property value is set. + * + * @param string $name + * The name of the property. Check whether the render key + * $element['#' . $name] is set where element is the render array this + * object was created from. This value is also accessible as $this->name. + * + * @return bool + * Whether it is set or not. + */ + public function __isset(string $name): bool { + return isset($this->storage['#' . $name]); + } + + /** + * {@inheritdoc} + */ + public function getChildren(): \Traversable { + foreach (Element::children($this->storage) as $key) { + yield $key => $this->elementInfoManager()->fromRenderable($this->storage[$key]); + } + } + + /** + * {@inheritdoc} + */ + public function getChild(int|string|array $name): ?ElementInterface { + $value = &NestedArray::getValue($this->storage, (array) $name, $exists); + return $exists ? $this->elementInfoManager()->fromRenderable($value) : NULL; + } + + /** + * {@inheritdoc} + */ + public function addChild(string|int $name, ElementInterface|array &$child): ElementInterface { + if ($name[0] === '#') { + throw new \LogicException('The name of children can not start with a #.'); + } + $childObject = $this->elementInfoManager()->fromRenderable($child); + $childObject->renderParent = $this; + $childObject->renderParentName = $name; + $this->storage[$name] = &$childObject->toRenderable(); + return $childObject; + } + + /** + * {@inheritdoc} + */ + public function createChild(int|string $name, string $class, array $configuration = [], bool $copyProperties = FALSE): ElementInterface { + $childObject = $this->elementInfoManager()->fromClass($class, $configuration); + $childObject = $this->addChild($name, $childObject); + if ($copyProperties) { + $childObject->storage += array_filter($this->storage, Element::property(...), \ARRAY_FILTER_USE_KEY); + } + return $childObject; + } + + /** + * {@inheritdoc} + */ + public function removeChild(int|string $name): ?ElementInterface { + $return = $this->storage[$name] ?? NULL; + unset($this->storage[$name]); + return $return ? $this->elementInfoManager()->fromRenderable($return) : NULL; + } + + /** + * {@inheritdoc} + */ + public function changeType(string $class): ElementInterface { + $this->storage['#type'] = $this->elementInfoManager()->getIdFromClass($class); + unset($this->storage['##object']); + return $this->elementInfoManager()->fromRenderable($this->storage); + } + + /** + * Returns the element info manager. + * + * @return \Drupal\Core\Render\ElementInfoManagerInterface + * The element info manager/ + */ + protected function elementInfoManager(): ElementInfoManagerInterface { + if (!$this->elementInfoManager) { + $this->elementInfoManager = \Drupal::service('plugin.manager.element_info'); + } + return $this->elementInfoManager; + } + + /** + * {@inheritdoc} + */ + public function __sleep(): array { + $vars = parent::__sleep(); + unset($this->storage['##object']); + return $vars; + } + + /** + * {@inheritdoc} + */ + public function __wakeup(): void { + parent::__wakeup(); + $this->storage['##object'] = $this; + } + } diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php index 094b963212b..c38582d4b23 100644 --- a/core/lib/Drupal/Core/Render/Element/Select.php +++ b/core/lib/Drupal/Core/Render/Element/Select.php @@ -10,7 +10,9 @@ use Drupal\Core\Render\Element; * Provides a form element for a drop-down menu or scrolling selection box. * * Properties: - * - #options: An associative array of options for the select. Do not use + * + * @property $options + * An associative array of options for the select. Do not use * placeholders that sanitize data in any labels, as doing so will lead to * double-escaping. Each array value can be: * - A single translated string representing an HTML option element, where @@ -28,18 +30,22 @@ use Drupal\Core\Render\Element; * is ignored, and the contents of the 'option' property are interpreted as * an array of options to be merged with any other regular options and * option groups found in the outer array. - * - #sort_options: (optional) If set to TRUE (default is FALSE), sort the + * @property $sort_options + * (optional) If set to TRUE (default is FALSE), sort the * options by their labels, after rendering and translation is complete. * Can be set within an option group to sort that group. - * - #sort_start: (optional) Option index to start sorting at, where 0 is the + * @property $sort_start + * (optional) Option index to start sorting at, where 0 is the * first option. Can be used within an option group. If an empty option is * being added automatically (see #empty_option and #empty_value properties), * this defaults to 1 to keep the empty option at the top of the list. * Otherwise, it defaults to 0. - * - #empty_option: (optional) The label to show for the first default option. + * @property $empty_option + * (optional) The label to show for the first default option. * By default, the label is automatically set to "- Select -" for a required * field and "- None -" for an optional field. - * - #empty_value: (optional) The value for the first default option, which is + * @property $empty_value + * (optional) The value for the first default option, which is * used to determine whether the user submitted a value or not. * - If #required is TRUE, this defaults to '' (an empty string). Note that * if #empty_value is the same as a key in #options then the value of @@ -57,15 +63,19 @@ use Drupal\Core\Render\Element; * - If #required is not TRUE and this value is set (most commonly to an * empty string), then an extra option (see #empty_option above) * representing a "non-selection" is added with this as its value. - * - #multiple: (optional) Indicates whether one or more options can be + * @property $multiple + * (optional) Indicates whether one or more options can be * selected. Defaults to FALSE. - * - #default_value: Must be NULL or not set in case there is no value for the + * @property $default_value + * Must be NULL or not set in case there is no value for the * element yet, in which case a first default option is inserted by default. * Whether this first option is a valid option depends on whether the field * is #required or not. - * - #required: (optional) Whether the user needs to select an option (TRUE) + * @property $required + * (optional) Whether the user needs to select an option (TRUE) * or not (FALSE). Defaults to FALSE. - * - #size: The number of rows in the list that should be visible at one time. + * @property $size + * The number of rows in the list that should be visible at one time. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/StatusReport.php b/core/lib/Drupal/Core/Render/Element/StatusReport.php index 41fc7d7fda1..057e314d24e 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/Element/Submit.php b/core/lib/Drupal/Core/Render/Element/Submit.php index 980f4ff6c59..d47ca57cb66 100644 --- a/core/lib/Drupal/Core/Render/Element/Submit.php +++ b/core/lib/Drupal/Core/Render/Element/Submit.php @@ -11,10 +11,13 @@ use Drupal\Core\Render\Attribute\FormElement; * the form's submit handler. * * Properties: - * - #submit: Specifies an alternate callback for form submission when the + * + * @property $submit + * Specifies an alternate callback for form submission when the * submit button is pressed. Use '::methodName' format or an array containing * the object and method name (for example, [ $this, 'methodName'] ). - * - #value: The text to be shown on the button. + * @property $value + * The text to be shown on the button. * * Usage Example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Table.php b/core/lib/Drupal/Core/Render/Element/Table.php index 85111505e9b..957b868ed63 100644 --- a/core/lib/Drupal/Core/Render/Element/Table.php +++ b/core/lib/Drupal/Core/Render/Element/Table.php @@ -14,19 +14,27 @@ use Drupal\Component\Utility\Html as HtmlUtility; * context of a form. * * Properties: - * - #header: An array of table header labels. - * - #rows: An array of the rows to be displayed. Each row is either an array + * + * @property $header + * An array of table header labels. + * @property $rows + * An array of the rows to be displayed. Each row is either an array * of cell contents or an array of properties as described in table.html.twig * Alternatively specify the data for the table as child elements of the table * element. Table elements would contain rows elements that would in turn * contain column elements. - * - #empty: Text to display when no rows are present. - * - #responsive: Indicates whether to add the drupal.tableresponsive library + * @property $empty + * Text to display when no rows are present. + * @property $responsive + * Indicates whether to add the drupal.tableresponsive library * providing responsive tables. Defaults to TRUE. - * - #sticky: Indicates whether to make the table headers sticky at + * @property $sticky + * Indicates whether to make the table headers sticky at * the top of the page. Defaults to FALSE. - * - #footer: Table footer rows, in the same format as the #rows property. - * - #caption: A localized string for the <caption> tag. + * @property $footer + * Table footer rows, in the same format as the #rows property. + * @property $caption + * A localized string for the <caption> tag. * * Usage example 1: A simple form with an additional information table which * doesn't include any other form field. @@ -394,7 +402,7 @@ class Table extends FormElementBase { * @return array * Associative array of rendered child elements for a table. * - * @see template_preprocess_table() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessTable() * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments() * @see drupal_attach_tabledrag() */ diff --git a/core/lib/Drupal/Core/Render/Element/Tableselect.php b/core/lib/Drupal/Core/Render/Element/Tableselect.php index cb38a0722d2..297115a3015 100644 --- a/core/lib/Drupal/Core/Render/Element/Tableselect.php +++ b/core/lib/Drupal/Core/Render/Element/Tableselect.php @@ -12,13 +12,19 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; * Provides a form element for a table with radios or checkboxes in left column. * * Properties: - * - #header: An array of table header labels. - * - #options: An associative array where each key is the value returned when + * + * @property $header + * An array of table header labels. + * @property $options + * An associative array where each key is the value returned when * a user selects the radio button or checkbox, and each value is the row of * table data. - * - #empty: The message to display if table does not have any options. - * - #multiple: Set to FALSE to render the table with radios instead checkboxes. - * - #js_select: Set to FALSE if you don't want the select all checkbox added to + * @property $empty + * The message to display if table does not have any options. + * @property $multiple + * Set to FALSE to render the table with radios instead checkboxes. + * @property $js_select + * Set to FALSE if you don't want the select all checkbox added to * the header. * * Other properties of the \Drupal\Core\Render\Element\Table element are also diff --git a/core/lib/Drupal/Core/Render/Element/Tel.php b/core/lib/Drupal/Core/Render/Element/Tel.php index 9d5951e7d4e..3d752708ebc 100644 --- a/core/lib/Drupal/Core/Render/Element/Tel.php +++ b/core/lib/Drupal/Core/Render/Element/Tel.php @@ -12,8 +12,11 @@ use Drupal\Core\Render\Element; * validation. * * Properties: - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Textarea.php b/core/lib/Drupal/Core/Render/Element/Textarea.php index c3fc6021924..9e41f15a891 100644 --- a/core/lib/Drupal/Core/Render/Element/Textarea.php +++ b/core/lib/Drupal/Core/Render/Element/Textarea.php @@ -9,11 +9,16 @@ use Drupal\Core\Render\Attribute\FormElement; * Provides a form element for input of multiple-line text. * * Properties: - * - #rows: Number of rows in the text box. - * - #cols: Number of columns in the text box. - * - #resizable: Controls whether the text area is resizable. Allowed values + * + * @property $rows + * Number of rows in the text box. + * @property $cols + * Number of columns in the text box. + * @property $resizable + * Controls whether the text area is resizable. Allowed values * are "none", "vertical", "horizontal", or "both" (defaults to "vertical"). - * - #maxlength: The maximum amount of characters to accept as input. + * @property $maxlength + * The maximum amount of characters to accept as input. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Textfield.php b/core/lib/Drupal/Core/Render/Element/Textfield.php index de144ab4348..47dbaf2b325 100644 --- a/core/lib/Drupal/Core/Render/Element/Textfield.php +++ b/core/lib/Drupal/Core/Render/Element/Textfield.php @@ -10,13 +10,21 @@ use Drupal\Core\Render\Element; * Provides a one-line text field form element. * * Properties: - * - #maxlength: Maximum number of characters of input allowed. - * - #size: The size of the input element in characters. - * - #autocomplete_route_name: A route to be used as callback URL by the + * + * @property $maxlength + * Maximum number of characters of input allowed. + * @property $size + * The size of the input element in characters. + * @property $autocomplete_route_name + * A route to be used as callback URL by the * autocomplete JavaScript library. - * - #autocomplete_route_parameters: An array of parameters to be used in + * @property $autocomplete_route_parameters + * An array of parameters to be used in * conjunction with the route name. - * - #pattern: A string for the native HTML5 pattern attribute. + * @property $pattern + * A string for the native HTML5 pattern attribute. + * @property $placeholder + * A string to displayed in a textfield when it has no value. * * Usage example: * diff --git a/core/lib/Drupal/Core/Render/Element/TitleDisplay.php b/core/lib/Drupal/Core/Render/Element/TitleDisplay.php new file mode 100644 index 00000000000..d194fd1b27a --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/TitleDisplay.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\Core\Render\Element; + +/** + * Defines how and where a title should be displayed for a form element. + */ +enum TitleDisplay: string { + + // Label goes before the element (default for most elements). + case Before = 'before'; + + // Label goes after the element (default for radio elements). + case After = 'after'; + + // Label is present in the markup but made invisible using CSS. + case Invisible = 'invisible'; + + // Label is set as the title attribute, displayed as a tooltip on hover. + case Attribute = 'attribute'; + +} diff --git a/core/lib/Drupal/Core/Render/Element/Url.php b/core/lib/Drupal/Core/Render/Element/Url.php index 7c5a9af2fbb..1e7d499d837 100644 --- a/core/lib/Drupal/Core/Render/Element/Url.php +++ b/core/lib/Drupal/Core/Render/Element/Url.php @@ -11,9 +11,13 @@ use Drupal\Core\Render\Element; * Provides a form element for input of a URL. * * Properties: - * - #default_value: A valid URL string. - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $default_value + * A valid URL string. + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Value.php b/core/lib/Drupal/Core/Render/Element/Value.php index ce6efe9620f..c1bb2834852 100644 --- a/core/lib/Drupal/Core/Render/Element/Value.php +++ b/core/lib/Drupal/Core/Render/Element/Value.php @@ -12,7 +12,9 @@ use Drupal\Core\Render\Attribute\FormElement; * in validation and submit processing. * * Properties: - * - #value: The value of the form element that cannot be edited by the user. + * + * @property $value + * The value of the form element that cannot be edited by the user. * * Usage Example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/VerticalTabs.php b/core/lib/Drupal/Core/Render/Element/VerticalTabs.php index bf55119913b..083b79018c0 100644 --- a/core/lib/Drupal/Core/Render/Element/VerticalTabs.php +++ b/core/lib/Drupal/Core/Render/Element/VerticalTabs.php @@ -13,7 +13,9 @@ use Drupal\Core\Render\Element; * this element's name as vertical tabs. * * Properties: - * - #default_tab: The HTML ID of the rendered details element to be used as + * + * @property $default_tab + * The HTML ID of the rendered details element to be used as * the default tab. View the source of the rendered page to determine the ID. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Weight.php b/core/lib/Drupal/Core/Render/Element/Weight.php index 89df69b1879..3fa4e2c8a96 100644 --- a/core/lib/Drupal/Core/Render/Element/Weight.php +++ b/core/lib/Drupal/Core/Render/Element/Weight.php @@ -12,7 +12,9 @@ use Drupal\Core\Render\Attribute\FormElement; * the order. * * Properties: - * - #delta: The range of possible weight values used. A delta of 10 would + * + * @property $delta + * The range of possible weight values used. A delta of 10 would * indicate possible weight values between -10 and 10. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Widget.php b/core/lib/Drupal/Core/Render/Element/Widget.php new file mode 100644 index 00000000000..ca5d87ea183 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Widget.php @@ -0,0 +1,26 @@ +<?php + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Attribute\RenderElement; + +/** + * Provides a widget element. + * + * A form wrapper containing basic properties for the widget, attach + * the widget elements to this wrapper. This element renders to an empty + * string. + * + * @property $field_parents + * The 'parents' space for the field in the form. Most widgets can simply + * overlook this property. This identifies the location where the field + * values are placed within $form_state->getValues(), and is used to + * access processing information for the field through the + * WidgetBase::getWidgetState() and WidgetBase::setWidgetState() methods. + * @property $delta + * The order of this item in the array of sub-elements. (0, 1, 2, etc.) + */ +#[RenderElement('widget')] +class Widget extends Generic { + +} diff --git a/core/lib/Drupal/Core/Render/ElementInfoManager.php b/core/lib/Drupal/Core/Render/ElementInfoManager.php index c847eea61e1..a57aaf71b1f 100644 --- a/core/lib/Drupal/Core/Render/ElementInfoManager.php +++ b/core/lib/Drupal/Core/Render/ElementInfoManager.php @@ -2,6 +2,9 @@ namespace Drupal\Core\Render; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Discovery\DiscoveryTrait; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface; @@ -11,6 +14,7 @@ use Drupal\Core\PreWarm\PreWarmableInterface; use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element\ElementInterface; use Drupal\Core\Render\Element\FormElementInterface; +use Drupal\Core\Render\Element\Generic; use Drupal\Core\Theme\ThemeManagerInterface; /** @@ -36,6 +40,16 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana protected $elementInfo; /** + * Class => plugin id mapping. + * + * More performant than reflecting runtime. + * + * @var array + * @internal + */ + protected array $reverseMapping = []; + + /** * Constructs an ElementInfoManager object. * * @param \Traversable $namespaces @@ -65,6 +79,79 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana /** * {@inheritdoc} */ + protected function getDiscovery(): DiscoveryInterface { + $discovery = parent::getDiscovery(); + return new class ($discovery, $this->reverseMapping) implements DiscoveryInterface { + use DiscoveryTrait; + + public function __construct(protected DiscoveryInterface $decorated, protected array &$reverseMapping) {} + + public function getDefinitions(): array { + $definitions = $this->decorated->getDefinitions(); + foreach ($definitions as $element_type => $definition) { + $this->reverseMapping[$definition['class']] = $element_type; + } + return $definitions; + } + + }; + } + + /** + * {@inheritdoc} + */ + protected function getCachedDefinitions(): ?array { + if (!isset($this->definitions) && $cache = $this->cacheGet($this->cacheKey)) { + $this->definitions = $cache->data['definitions']; + $this->reverseMapping = $cache->data['reverse_mapping']; + } + return $this->definitions; + } + + /** + * {@inheritdoc} + */ + protected function setCachedDefinitions($definitions): void { + $data = [ + 'definitions' => $definitions, + 'reverse_mapping' => $this->reverseMapping, + ]; + $this->cacheSet($this->cacheKey, $data, Cache::PERMANENT, $this->cacheTags); + $this->definitions = $definitions; + } + + /** + * {@inheritdoc} + */ + public function clearCachedDefinitions(): void { + $this->elementInfo = NULL; + + $cids = []; + foreach ($this->themeHandler->listInfo() as $theme_name => $info) { + $cids[] = $this->getCid($theme_name); + } + + $this->cacheBackend->deleteMultiple($cids); + + parent::clearCachedDefinitions(); + } + + /** + * Returns the CID used to cache the element info. + * + * @param string $theme_name + * The theme name. + * + * @return string + * The cache ID. + */ + protected function getCid($theme_name): string { + return 'element_info_build:' . $theme_name; + } + + /** + * {@inheritdoc} + */ public function getInfo($type) { $theme_name = $this->themeManager->getActiveTheme()->getName(); if (!isset($this->elementInfo[$theme_name])) { @@ -102,7 +189,8 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana // Otherwise, rebuild and cache. $info = []; - $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) { + $previous_error_handler = get_error_handler(); + set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) { // Ignore deprecations while building element information. if ($severity === E_USER_DEPRECATED) { // Don't execute PHP internal error handler. @@ -142,39 +230,49 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana * {@inheritdoc} * * @return \Drupal\Core\Render\Element\ElementInterface + * The render element plugin instance. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment - public function createInstance($plugin_id, array $configuration = []) { - return parent::createInstance($plugin_id, $configuration); + public function createInstance($plugin_id, array $configuration = [], &$element = []): ElementInterface { + $instance = parent::createInstance($plugin_id, $configuration); + assert($instance instanceof ElementInterface); + $instance->initializeInternalStorage($element); + return $instance; } /** * {@inheritdoc} */ - public function clearCachedDefinitions() { - $this->elementInfo = NULL; - - $cids = []; - foreach ($this->themeHandler->listInfo() as $theme_name => $info) { - $cids[] = $this->getCid($theme_name); + public function fromClass(string $class, array $configuration = []): ElementInterface { + $this->getDefinitions(); + if ($id = $this->getIdFromClass($class)) { + return $this->createInstance($id, $configuration); } + throw new \LogicException("$class is not a valid element class."); + } - $this->cacheBackend->deleteMultiple($cids); - - parent::clearCachedDefinitions(); + /** + * {@inheritdoc} + */ + public function getIdFromClass(string $class): ?string { + $this->getDefinitions(); + return $this->reverseMapping[$class] ?? NULL; } /** - * Returns the CID used to cache the element info. - * - * @param string $theme_name - * The theme name. - * - * @return string - * The cache ID. + * {@inheritdoc} */ - protected function getCid($theme_name) { - return 'element_info_build:' . $theme_name; + public function fromRenderable(ElementInterface|array &$element, string $class = Generic::class): ElementInterface { + if ($element instanceof ElementInterface) { + return $element; + } + if (isset($element['##object']) && $element['##object'] instanceof ElementInterface) { + return $element['##object']->initializeInternalStorage($element); + } + $type = $element['#type'] ?? $this->getIdFromClass($class); + if (!$type) { + throw new \LogicException('The element passed to ElementInfoManager::fromRenderable must have a #type or a valid class must be provided.'); + } + return $this->createInstance($type, element: $element); } } diff --git a/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php index ea6f21b849c..e1a891f4764 100644 --- a/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php +++ b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php @@ -3,11 +3,14 @@ namespace Drupal\Core\Render; use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Factory\FactoryInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Form; /** * Collects available render array element types. */ -interface ElementInfoManagerInterface extends DiscoveryInterface { +interface ElementInfoManagerInterface extends DiscoveryInterface, FactoryInterface { /** * Retrieves the default properties for the defined element type. @@ -61,4 +64,61 @@ interface ElementInfoManagerInterface extends DiscoveryInterface { */ public function getInfoProperty($type, $property_name, $default = NULL); + /** + * Creates a render object from a render array. + * + * @param \Drupal\Core\Render\Element\ElementInterface|array $element + * A render array or render objects. The latter is returned unchanged. + * @param class-string<T> $class + * The class of the render object being created. + * + * @return T + * A render object. + * + * @template T of ElementInterface + */ + public function fromRenderable(ElementInterface|array &$element, string $class = Form::class): ElementInterface; + + /** + * {@inheritdoc} + * + * @return \Drupal\Core\Render\Element\ElementInterface + * A fully configured render object. + */ + public function createInstance($plugin_id, array $configuration = []): ElementInterface; + + /** + * Creates a render object based on the provided class and configuration. + * + * @param class-string<T> $class + * The class of the render object being instantiated. + * @param array $configuration + * An array of configuration relevant to the render object. + * + * @return T + * A fully configured render object. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * If the render object cannot be created, such as if the class is invalid. + * + * @template T of ElementInterface + */ + public function fromClass(string $class, array $configuration = []): ElementInterface; + + /** + * Get the plugin ID from the class. + * + * Whenever possible, use the class type inference. Calling this method + * should not be necessary. + * + * @param string $class + * The class of an element object. + * + * @return ?string + * The plugin ID or null if not found. + * + * @internal + */ + public function getIdFromClass(string $class): ?string; + } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index d0150fe0127..5d25a5176c9 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; @@ -199,7 +200,7 @@ class Renderer implements RendererInterface { * {@inheritdoc} */ public function renderPlaceholder($placeholder, array $elements) { - // Get the render array for the given placeholder + // Get the render array for the given placeholder. $placeholder_element = $elements['#attached']['placeholders'][$placeholder]; $markup = $this->doRenderPlaceholder($placeholder_element); return $this->doReplacePlaceholder($placeholder, $markup, $elements, $placeholder_element); @@ -208,7 +209,13 @@ class Renderer implements RendererInterface { /** * {@inheritdoc} */ - public function render(&$elements, $is_root_call = FALSE) { + public function render(/* array */&$elements, $is_root_call = FALSE) { + + if (!is_array($elements)) { + trigger_error('Calling ' . __METHOD__ . ' with NULL is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Either pass an array or skip the call. See https://www.drupal.org/node/3534020.'); + return ''; + } + $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."); @@ -250,9 +257,10 @@ class Renderer implements RendererInterface { return $return; } - // 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. + // 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']; @@ -304,11 +312,9 @@ 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). + // 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'])) { @@ -615,12 +621,39 @@ class Renderer implements RendererInterface { * {@inheritdoc} */ public function executeInRenderContext(RenderContext $context, callable $callable) { - // Store the current render context. + // When executing in a render context, we need to isolate any bubbled + // context within this method. To allow for async rendering, it's necessary + // to detect if a fiber suspends within a render context. When this happens, + // we swap the previous render context in before suspending upwards, then + // back out again before resuming. $previous_context = $this->getCurrentRenderContext(); - // Set the provided context and call the callable, it will use that context. $this->setCurrentRenderContext($context); - $result = $callable(); + + $fiber = new \Fiber(static fn () => $callable()); + $fiber->start(); + while (!$fiber->isTerminated()) { + if ($fiber->isSuspended()) { + // When ::executeInRenderContext() is executed within a Fiber, which is + // always the case when rendering placeholders, if the callback results + // in this fiber being suspended, we need to suspend again up to the + // parent Fiber. Doing so allows other placeholders to be rendered + // before returning here. + if (\Fiber::getCurrent() !== NULL) { + $this->setCurrentRenderContext($previous_context); + \Fiber::suspend(); + $this->setCurrentRenderContext($context); + } + $fiber->resume(); + } + if (!$fiber->isTerminated()) { + // If we've reached this point, then the fiber has already been started + // and resumed at least once, so may be suspending repeatedly. Avoid + // a spin-lock by waiting for 0.5ms prior to continuing the while loop. + usleep(500); + } + } + $result = $fiber->getReturn(); assert($context->count() <= 1, 'Bubbling failed.'); // Restore the original render context. @@ -720,7 +753,7 @@ class Renderer implements RendererInterface { $message_placeholders[] = $placeholder; } else { - // Get the render array for the given placeholder + // Get the render array for the given placeholder. $fibers[$placeholder] = new \Fiber(function () use ($placeholder_element) { return [$this->doRenderPlaceholder($placeholder_element), $placeholder_element]; }); @@ -779,6 +812,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 020e594755f..081545bd79d 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -340,7 +340,7 @@ interface RendererInterface { * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments() * @see \Drupal\Core\Render\RendererInterface::renderRoot() */ - public function render(&$elements, $is_root_call = FALSE); + public function render(/* array */&$elements, $is_root_call = FALSE); /** * Checks whether a render context is active. diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index 0133cf17254..bf224d90880 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -184,7 +184,7 @@ class UrlGenerator implements UrlGeneratorInterface { $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); - // All params must be given + // All params must be given. if ($diff = array_diff_key($variables, $mergedParams)) { throw new MissingMandatoryParametersException($name, array_keys($diff)); } @@ -217,7 +217,7 @@ class UrlGenerator implements UrlGeneratorInterface { foreach ($tokens as $token) { if ('variable' === $token[0]) { if (!$optional || !array_key_exists($token[3], $defaults) || (isset($mergedParams[$token[3]]) && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]])) { - // Check requirement + // Check requirement. if (!preg_match('#^' . $token[2] . '$#', $mergedParams[$token[3]])) { $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]); throw new InvalidParameterException($message); @@ -228,7 +228,7 @@ class UrlGenerator implements UrlGeneratorInterface { } } else { - // Static text + // Static text. $url = $token[1] . $url; $optional = FALSE; } @@ -330,7 +330,7 @@ class UrlGenerator implements UrlGeneratorInterface { // http://tools.ietf.org/html/rfc3986#section-3.3 so we need to encode // them as they are not used for this purpose here otherwise we would // generate a URI that, when followed by a user agent (e.g. browser), does - // not match this route + // not match this route. $path = strtr($path, ['/../' => '/%2E%2E/', '/./' => '/%2E/']); if (str_ends_with($path, '/..')) { $path = substr($path, 0, -2) . '%2E%2E'; diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php index 0170626181c..6927ba2ebbe 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/StreamWrapper/LocalReadOnlyStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php index 29277393ff1..5fc9a4fca38 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php @@ -57,7 +57,7 @@ abstract class LocalReadOnlyStream extends LocalStream { * @see http://php.net/manual/streamwrapper.stream-lock.php */ public function stream_lock($operation) { - // Disallow exclusive lock or non-blocking lock requests + // Disallow exclusive lock or non-blocking lock requests. if (in_array($operation, [LOCK_EX, LOCK_EX | LOCK_NB])) { trigger_error('stream_lock() exclusive lock operations not supported for read-only stream wrappers', E_USER_WARNING); return FALSE; diff --git a/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php b/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php index c6327af7e3d..594b075694f 100644 --- a/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php @@ -99,6 +99,7 @@ abstract class ReadOnlyStream implements StreamWrapperInterface { * (optional) The stream wrapper URI to be converted to a canonical * absolute path. This may point to a directory or another type of file. * + * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn * @return string|bool * If $uri is not set, returns the canonical absolute path of the URI * previously set by the @@ -115,7 +116,6 @@ abstract class ReadOnlyStream implements StreamWrapperInterface { * prevent static analysis errors. In D11, consider changing it to an * abstract method. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidNoReturn protected function getLocalPath($uri = NULL) { throw new \BadMethodCallException(get_class($this) . '::getLocalPath() not implemented.'); } diff --git a/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php b/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php index 4467632eeb7..7052aab1d5d 100644 --- a/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php +++ b/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element\RenderElementBase; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Render\RendererInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -14,9 +15,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * Provides a link to break a tempstore lock. * * Properties: - * - #label: The label of the object that is locked. - * - #lock: \Drupal\Core\TempStore\Lock object. - * - #url: \Drupal\Core\Url object pointing to the break lock form. + * + * @property $label + * The label of the object that is locked. + * @property $lock + * \Drupal\Core\TempStore\Lock object. + * @property $url + * \Drupal\Core\Url object pointing to the break lock form. * * Usage example: * @code @@ -67,9 +72,19 @@ class BreakLockLink extends RenderElementBase implements ContainerFactoryPluginI * The entity type manager. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, DateFormatterInterface $date_formatter, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + DateFormatterInterface $date_formatter, + EntityTypeManagerInterface $entity_type_manager, + RendererInterface $renderer, + ElementInfoManagerInterface $elementInfoManager, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); $this->dateFormatter = $date_formatter; $this->entityTypeManager = $entity_type_manager; @@ -86,7 +101,8 @@ class BreakLockLink extends RenderElementBase implements ContainerFactoryPluginI $plugin_definition, $container->get('date.formatter'), $container->get('entity_type.manager'), - $container->get('renderer') + $container->get('renderer'), + $container->get('plugin.manager.element_info') ); } diff --git a/core/lib/Drupal/Core/Template/Loader/ComponentLoader.php b/core/lib/Drupal/Core/Template/Loader/ComponentLoader.php index d141d202ecb..e3669f8f145 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/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 5afa135d026..4b4fd068f89 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -100,7 +100,7 @@ class TwigExtension extends AbstractExtension { // This function will receive a renderable array, if an array is detected. new TwigFunction('render_var', [$this, 'renderVar']), // The URL and path function are defined in close parallel to those found - // in \Symfony\Bridge\Twig\Extension\RoutingExtension + // in \Symfony\Bridge\Twig\Extension\RoutingExtension. new TwigFunction('url', [$this, 'getUrl'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), new TwigFunction('path', [$this, 'getPath'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), new TwigFunction('link', [$this, 'getLink']), diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index 6a4dcfcf8b0..144cea386ca 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -85,7 +85,7 @@ trait FunctionalTestSetupTrait { // - The temporary directory is set and created by install_base_system(). // - The private file directory is created post install by // FunctionalTestSetupTrait::initConfig(). - // @see system_requirements() + // @see \Drupal\system\Install\SystemRequirements. // @see TestBase::prepareEnvironment() // @see install_base_system() // @see \Drupal\Core\Test\FunctionalTestSetupTrait::initConfig() @@ -197,8 +197,8 @@ trait FunctionalTestSetupTrait { protected function writeSettings(array $settings) { include_once DRUPAL_ROOT . '/core/includes/install.inc'; $filename = $this->siteDirectory . '/settings.php'; - // system_requirements() removes write permissions from settings.php - // whenever it is invoked. + // The system runtime_requirements hook removes write permissions from + // settings.php whenever it is invoked. // Not using File API; a potential error must trigger a PHP warning. chmod($filename, 0666); SettingsEditor::rewrite($filename, $settings); @@ -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/PhpUnitTestDiscovery.php b/core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php new file mode 100644 index 00000000000..a02894c1c74 --- /dev/null +++ b/core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php @@ -0,0 +1,407 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Test; + +use Drupal\Core\Test\Exception\MissingGroupException; +use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Event\EventFacadeIsSealedException; +use PHPUnit\Event\Facade as EventFacade; +use PHPUnit\Framework\DataProviderTestSuite; +use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestSuite; +use PHPUnit\TextUI\Configuration\Builder; +use PHPUnit\TextUI\Configuration\TestSuiteBuilder; + +/** + * Discovers available tests using the PHPUnit API. + * + * @internal + */ +class PhpUnitTestDiscovery { + + /** + * The singleton. + * + * @var \Drupal\Core\Test\PhpUnitTestDiscovery|null + */ + private static ?self $instance = NULL; + + /** + * The map of legacy test suite identifiers to phpunit.xml ones. + * + * @var array<string,string> + */ + private array $map = [ + 'PHPUnit-FunctionalJavascript' => 'functional-javascript', + 'PHPUnit-Functional' => 'functional', + 'PHPUnit-Kernel' => 'kernel', + 'PHPUnit-Unit' => 'unit', + 'PHPUnit-Unit-Component' => 'unit-component', + 'PHPUnit-Build' => 'build', + ]; + + /** + * The reverse map of legacy test suite identifiers to phpunit.xml ones. + * + * @var array<string,string> + */ + private array $reverseMap; + + /** + * Path to PHPUnit's configuration file. + */ + private string $configurationFilePath; + + /** + * The warnings generated during the discovery. + * + * @var list<string> + */ + private array $warnings = []; + + private function __construct() { + $this->reverseMap = array_flip($this->map); + try { + EventFacade::instance()->registerTracer(new PhpUnitTestDiscoveryTracer($this)); + EventFacade::instance()->seal(); + } + catch (EventFacadeIsSealedException) { + // Just continue. + } + } + + /** + * Returns the singleton instance. + */ + public static function instance(): self { + if (self::$instance === NULL) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Sets the configuration file path. + */ + public function setConfigurationFilePath(string $configurationFilePath): self { + $this->configurationFilePath = $configurationFilePath; + return $this; + } + + /** + * Discovers available tests. + * + * @param string|null $extension + * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * @param list<string> $testSuites + * (optional) An array of PHPUnit test suites to filter the discovery for. + * @param string|null $directory + * (optional) Limit discovered tests to a specific directory. + * + * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>> + * An array of test groups keyed by the group name. Each test group is an + * array of test class information arrays as returned by + * ::getTestClassInfo(), keyed by test class. If a test class belongs to + * multiple groups, it will appear under all group keys it belongs to. + */ + public function getTestClasses(?string $extension = NULL, array $testSuites = [], ?string $directory = NULL): array { + $this->warnings = []; + + $args = ['--configuration', $this->configurationFilePath]; + + if (!empty($testSuites)) { + // Convert $testSuites from Drupal's legacy syntax to the syntax used in + // phpunit.xml, that is necessary to PHPUnit to be able to apply the + // test suite filter. For example, 'PHPUnit-Unit' to 'unit'. + $tmp = []; + foreach ($testSuites as $i) { + if (!is_string($i)) { + throw new \InvalidArgumentException("Test suite must be a string"); + } + if (str_contains($i, ' ')) { + throw new \InvalidArgumentException("Test suite name '{$i}' is invalid"); + } + $tmp[] = $this->map[$i] ?? $i; + } + $args[] = '--testsuite=' . implode(',', $tmp); + } + + if ($directory !== NULL) { + $args[] = $directory; + } + + $phpUnitConfiguration = (new Builder())->build($args); + + // TestSuiteBuilder calls the test data providers during the discovery. + // Data providers may be changing the Drupal service container, which leads + // to potential issues. We save the current container before running the + // discovery, and in case a change is detected, reset it and raise + // warnings so that developers can tune their data provider code. + if (\Drupal::hasContainer()) { + $container = \Drupal::getContainer(); + $containerObjectId = spl_object_id($container); + } + $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration); + if (isset($containerObjectId) && $containerObjectId !== spl_object_id(\Drupal::getContainer())) { + $this->addWarning( + ">>> The service container was changed during the test discovery <<<\n" . + "Probably, a test data provider method called \\Drupal::setContainer().\n" . + "Ensure that all the data providers restore the original container before returning data." + ); + assert(isset($container)); + \Drupal::setContainer($container); + } + + $list = $directory === NULL ? + $this->getTestList($phpUnitTestSuite, $extension) : + $this->getTestListLimitedToDirectory($phpUnitTestSuite, $extension, $testSuites); + + // Sort the groups and tests within the groups by name. + uksort($list, 'strnatcasecmp'); + foreach ($list as &$tests) { + uksort($tests, 'strnatcasecmp'); + } + + return $list; + } + + /** + * Discovers all class files in all available extensions. + * + * @param string|null $extension + * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * @param string|null $directory + * (optional) Limit discovered tests to a specific directory. + * + * @return array + * A classmap containing all discovered class files; i.e., a map of + * fully-qualified classnames to path names. + */ + public function findAllClassFiles(?string $extension = NULL, ?string $directory = NULL): array { + $testClasses = $this->getTestClasses($extension, [], $directory); + $classMap = []; + foreach ($testClasses as $group) { + foreach ($group as $className => $info) { + $classMap[$className] = $info['file']; + } + } + return $classMap; + } + + /** + * Adds warning message generated during the discovery. + * + * @param string $message + * The warning message. + */ + public function addWarning(string $message): void { + $this->warnings[] = $message; + } + + /** + * Returns the warnings generated during the discovery. + * + * @return list<string> + * The warnings. + */ + public function getWarnings(): array { + return $this->warnings; + } + + /** + * Returns a list of tests from a TestSuite object. + * + * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite + * The TestSuite object returned by PHPUnit test discovery. + * @param string|null $extension + * The name of an extension to limit discovery to; e.g., 'node'. + * + * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>> + * An array of test groups keyed by the group name. Each test group is an + * array of test class information arrays as returned by + * ::getTestClassInfo(), keyed by test class. If a test class belongs to + * multiple groups, it will appear under all group keys it belongs to. + */ + private function getTestList(TestSuite $phpUnitTestSuite, ?string $extension): array { + $list = []; + foreach ($phpUnitTestSuite->tests() as $testSuite) { + foreach ($testSuite->tests() as $testClass) { + if ($testClass->isEmpty()) { + continue; + } + + if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) { + continue; + } + + $item = $this->getTestClassInfo( + $testClass, + $this->reverseMap[$testSuite->name()] ?? $testSuite->name(), + ); + + foreach ($item['groups'] as $group) { + $list[$group][$item['name']] = $item; + } + } + } + return $list; + } + + /** + * Returns a list of tests from a TestSuite object limited to a directory. + * + * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite + * The TestSuite object returned by PHPUnit test discovery. + * @param string|null $extension + * The name of an extension to limit discovery to; e.g., 'node'. + * @param list<string> $testSuites + * An array of PHPUnit test suites to filter the discovery for. + * + * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>> + * An array of test groups keyed by the group name. Each test group is an + * array of test class information arrays as returned by + * ::getTestClassInfo(), keyed by test class. If a test class belongs to + * multiple groups, it will appear under all group keys it belongs to. + */ + private function getTestListLimitedToDirectory(TestSuite $phpUnitTestSuite, ?string $extension, array $testSuites): array { + $list = []; + + // In this case, PHPUnit found a single test class to run tests for. + if ($phpUnitTestSuite->isForTestClass()) { + if ($phpUnitTestSuite->isEmpty()) { + return []; + } + + if ($extension !== NULL && !str_starts_with($phpUnitTestSuite->name(), "Drupal\\Tests\\{$extension}\\")) { + return []; + } + + // Take the test suite name from the class namespace. + $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($phpUnitTestSuite->name()); + if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) { + return []; + } + + $item = $this->getTestClassInfo($phpUnitTestSuite, $testSuite); + + foreach ($item['groups'] as $group) { + $list[$group][$item['name']] = $item; + } + return $list; + } + + // Multiple test classes were found. + $list = []; + foreach ($phpUnitTestSuite->tests() as $testClass) { + if ($testClass->isEmpty()) { + continue; + } + + if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) { + continue; + } + + // Take the test suite name from the class namespace. + $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($testClass->name()); + if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) { + continue; + } + + $item = $this->getTestClassInfo($testClass, $testSuite); + + foreach ($item['groups'] as $group) { + $list[$group][$item['name']] = $item; + } + } + return $list; + + } + + /** + * Returns the test class information. + * + * @param \PHPUnit\Framework\Test $testClass + * The test class. + * @param string $testSuite + * The test suite of this test class. + * + * @return array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int} + * The test class information. + */ + private function getTestClassInfo(Test $testClass, string $testSuite): array { + $reflection = new \ReflectionClass($testClass->name()); + + // Let PHPUnit API return the groups, as it will deal transparently with + // annotations or attributes, but skip groups generated by PHPUnit + // internally and starting with a double underscore prefix. + if (RunnerVersion::getMajor() < 11) { + $groups = array_filter($testClass->groups(), function (string $value): bool { + return !str_starts_with($value, '__phpunit'); + }); + } + else { + // In PHPUnit 11+, we need to coalesce the groups from individual tests + // as they may not be available from the test class level (when tests are + // backed by data providers). + $tmp = []; + foreach ($testClass as $test) { + if ($test instanceof DataProviderTestSuite) { + foreach ($test as $testWithData) { + $tmp = array_merge($tmp, $testWithData->groups()); + } + } + else { + $tmp = array_merge($tmp, $test->groups()); + } + } + $groups = array_filter(array_unique($tmp), function (string $value): bool { + return !str_starts_with($value, '__phpunit'); + }); + } + if (empty($groups)) { + throw new MissingGroupException(sprintf('Missing group metadata in test class %s', $testClass->name())); + } + + // Let PHPUnit API return the class coverage information. + $test = $testClass; + while (!$test instanceof TestCase) { + $test = $test->tests()[0]; + } + if (($metadata = $test->valueObjectForEvents()->metadata()->isCoversClass()) && $metadata->isNotEmpty()) { + $description = sprintf('Tests %s.', $metadata->asArray()[0]->className()); + } + elseif (($metadata = $test->valueObjectForEvents()->metadata()->isCoversDefaultClass()) && $metadata->isNotEmpty()) { + $description = sprintf('Tests %s.', $metadata->asArray()[0]->className()); + } + else { + $description = TestDiscovery::parseTestClassSummary($reflection->getDocComment()); + } + + // Find the test cases count. + $count = 0; + foreach ($testClass->tests() as $testCase) { + if ($testCase instanceof TestCase) { + // If it's a straight test method, counts 1. + $count++; + } + else { + // It's a data provider test suite, count 1 per data set provided. + $count += count($testCase->tests()); + } + } + + return [ + 'name' => $testClass->name(), + 'group' => $groups[0], + 'groups' => $groups, + 'type' => $testSuite, + 'description' => $description, + 'file' => $reflection->getFileName(), + 'tests_count' => $count, + ]; + } + +} diff --git a/core/lib/Drupal/Core/Test/PhpUnitTestDiscoveryTracer.php b/core/lib/Drupal/Core/Test/PhpUnitTestDiscoveryTracer.php new file mode 100644 index 00000000000..68170483096 --- /dev/null +++ b/core/lib/Drupal/Core/Test/PhpUnitTestDiscoveryTracer.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Test; + +use PHPUnit\Event\Event; +use PHPUnit\Event\Test\PhpunitErrorTriggered; +use PHPUnit\Event\Test\PhpunitWarningTriggered; +use PHPUnit\Event\TestRunner\WarningTriggered; +use PHPUnit\Event\Tracer\Tracer; + +/** + * Traces events dispatched by PHPUnit during the test discovery. + * + * @internal + */ +class PhpUnitTestDiscoveryTracer implements Tracer { + + public function __construct( + private readonly PHPUnitTestDiscovery $testDiscovery, + ) { + } + + /** + * {@inheritdoc} + */ + public function trace(Event $event): void { + if (in_array(get_class($event), [ + PhpunitErrorTriggered::class, + PhpunitWarningTriggered::class, + WarningTriggered::class, + ])) { + $this->testDiscovery->addWarning(sprintf('%s: %s', get_class($event), $event->message())); + } + } + +} diff --git a/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php index 12aa757e57e..8ab5260aa66 100644 --- a/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php +++ b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php @@ -4,9 +4,16 @@ namespace Drupal\Core\Test\RunTests; use PHPUnit\Framework\TestCase; +@trigger_error('Drupal\Core\Test\RunTests\TestFileParser is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); + /** * Parses class names from PHP files without loading them. * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3447698 + * * @internal */ class TestFileParser { diff --git a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php index cb754e1afaa..0c000d675c3 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/Test/TestDiscovery.php b/core/lib/Drupal/Core/Test/TestDiscovery.php index 1347d0c583f..468256779b3 100644 --- a/core/lib/Drupal/Core/Test/TestDiscovery.php +++ b/core/lib/Drupal/Core/Test/TestDiscovery.php @@ -7,6 +7,7 @@ use Drupal\Component\Annotation\Reflection\MockFileFinder; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Test\Exception\MissingGroupException; +use PHPUnit\Framework\Attributes\Group; /** * Discovers available tests. @@ -26,6 +27,11 @@ class TestDiscovery { * Statically cached list of test classes. * * @var array + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is + * no replacement. + * + * @see https://www.drupal.org/node/3447698 */ protected $testClasses; @@ -149,8 +155,14 @@ class TestDiscovery { * * @todo Remove singular grouping; retain list of groups in 'group' key. * @see https://www.drupal.org/node/2296615 + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * PhpUnitTestDiscovery::getTestClasses() instead. + * + * @see https://www.drupal.org/node/3447698 */ public function getTestClasses($extension = NULL, array $types = [], ?string $directory = NULL) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use PhpUnitTestDiscovery::getTestClasses() instead. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); if (!isset($extension) && empty($types)) { if (!empty($this->testClasses)) { return $this->testClasses; @@ -175,6 +187,15 @@ class TestDiscovery { catch (MissingGroupException $e) { // If the class name ends in Test and is not a migrate table dump. if (str_ends_with($classname, 'Test') && !str_contains($classname, 'migrate_drupal\Tests\Table')) { + $reflection = new \ReflectionClass($classname); + $groupAttributes = $reflection->getAttributes(Group::class, \ReflectionAttribute::IS_INSTANCEOF); + if (!empty($groupAttributes)) { + $group = '##no-group-annotations'; + $info['group'] = $group; + $info['groups'] = [$group]; + $list[$group][$classname] = $info; + continue; + } throw $e; } // If the class is @group annotation just skip it. Most likely it is an @@ -216,8 +237,14 @@ class TestDiscovery { * @return array * A classmap containing all discovered class files; i.e., a map of * fully-qualified classnames to path names. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * PhpUnitTestDiscovery::findAllClassFiles() instead. + * + * @see https://www.drupal.org/node/3447698 */ public function findAllClassFiles($extension = NULL, ?string $directory = NULL) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use PhpUnitTestDiscovery::findAllClassFiles() instead. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); $classmap = []; $namespaces = $this->registerTestNamespaces(); if (isset($extension)) { @@ -256,8 +283,14 @@ class TestDiscovery { * * @todo Limit to '*Test.php' files (~10% less files to reflect/introspect). * @see https://www.drupal.org/node/2296635 + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is + * no replacement. + * + * @see https://www.drupal.org/node/3447698 */ public static function scanDirectory($namespace_prefix, $path) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); if (!str_ends_with($namespace_prefix, '\\')) { throw new \InvalidArgumentException("Namespace prefix for $path must contain a trailing namespace separator."); } @@ -312,8 +345,14 @@ class TestDiscovery { * * @throws \Drupal\Core\Test\Exception\MissingGroupException * If the class does not have a @group annotation. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is + * no replacement. + * + * @see https://www.drupal.org/node/3447698 */ public static function getTestInfo($classname, $doc_comment = NULL) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); if ($doc_comment === NULL) { $reflection = new \ReflectionClass($classname); $doc_comment = $reflection->getDocComment(); @@ -350,7 +389,7 @@ class TestDiscovery { $info['type'] = 'PHPUnit-' . static::getPhpunitTestSuite($classname); if (!empty($annotations['coversDefaultClass'])) { - $info['description'] = 'Tests ' . $annotations['coversDefaultClass'] . '.'; + $info['description'] = 'Tests ' . ltrim($annotations['coversDefaultClass']) . '.'; } else { $info['description'] = static::parseTestClassSummary($doc_comment); diff --git a/core/lib/Drupal/Core/Test/TestRun.php b/core/lib/Drupal/Core/Test/TestRun.php index 5214093ce49..4d0e19e19f8 100644 --- a/core/lib/Drupal/Core/Test/TestRun.php +++ b/core/lib/Drupal/Core/Test/TestRun.php @@ -174,7 +174,7 @@ class TestRun { foreach (file($error_log_path) as $line) { if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) { // Parse PHP fatal errors for example: PHP Fatal error: Call to - // undefined function break_me() in /path/to/file.php on line 17 + // undefined function break_me() in /path/to/file.php on line 17. $this->insertLogEntry([ 'test_class' => $test_class, 'status' => 'fail', diff --git a/core/lib/Drupal/Core/Test/TestRunnerKernel.php b/core/lib/Drupal/Core/Test/TestRunnerKernel.php index e5ece5c07a2..242f725b260 100644 --- a/core/lib/Drupal/Core/Test/TestRunnerKernel.php +++ b/core/lib/Drupal/Core/Test/TestRunnerKernel.php @@ -5,7 +5,6 @@ namespace Drupal\Core\Test; use Drupal\Core\DrupalKernel; use Drupal\Core\Extension\Extension; use Drupal\Core\Site\Settings; -use Drupal\Core\Utility\Error; use Symfony\Component\HttpFoundation\Request; /** @@ -61,8 +60,7 @@ class TestRunnerKernel extends DrupalKernel { // Remove Drupal's error/exception handlers; they are designed for HTML // and there is no storage nor a (watchdog) logger here. - $currentErrorHandler = Error::currentErrorHandler(); - if (is_string($currentErrorHandler) && $currentErrorHandler === '_drupal_error_handler') { + if (get_error_handler() === '_drupal_error_handler') { restore_error_handler(); } restore_exception_handler(); diff --git a/core/lib/Drupal/Core/Test/TestSetupTrait.php b/core/lib/Drupal/Core/Test/TestSetupTrait.php index e68c9dba532..b25af6f622b 100644 --- a/core/lib/Drupal/Core/Test/TestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/TestSetupTrait.php @@ -134,7 +134,7 @@ trait TestSetupTrait { * @see \Drupal\Tests\BrowserTestBase::prepareEnvironment() * @see drupal_valid_test_ua() */ - protected function prepareDatabasePrefix() { + protected function prepareDatabasePrefix(): void { $test_db = new TestDatabase(); $this->siteDirectory = $test_db->getTestSitePath(); $this->databasePrefix = $test_db->getDatabasePrefix(); @@ -143,7 +143,7 @@ trait TestSetupTrait { /** * Changes the database connection to the prefixed one. */ - protected function changeDatabasePrefix() { + protected function changeDatabasePrefix(): void { if (empty($this->databasePrefix)) { $this->prepareDatabasePrefix(); } @@ -154,7 +154,7 @@ trait TestSetupTrait { // Ensure no existing database gets in the way. If a default database // exists already it must be removed. Database::removeConnection('default'); - $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root ?? DRUPAL_ROOT, TRUE); + $database = Database::convertDbUrlToConnectionInfo($db_url, TRUE); Database::addConnectionInfo('default', 'default', $database); } diff --git a/core/lib/Drupal/Core/Test/TestStatus.php b/core/lib/Drupal/Core/Test/TestStatus.php index 91689eb0733..09dbdb5c21b 100644 --- a/core/lib/Drupal/Core/Test/TestStatus.php +++ b/core/lib/Drupal/Core/Test/TestStatus.php @@ -55,7 +55,7 @@ class TestStatus { static::ERROR => 'error', static::SYSTEM => 'exception', ]; - // For status 3 and higher, we want 'exception.' + // For status 3 and higher, we want 'exception'. $label = $statusMap[$status > static::SYSTEM ? static::SYSTEM : $status]; return $label; } diff --git a/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php b/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php index e19e759f173..753529b6324 100644 --- a/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php +++ b/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php @@ -5,6 +5,7 @@ namespace Drupal\Core\Theme\Component; use Drupal\Core\Extension\ExtensionLifecycle; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Render\Component\Exception\InvalidComponentException; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Component metadata. @@ -14,6 +15,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 +121,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 +156,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; } @@ -162,10 +169,28 @@ class ComponentMetadata { throw new InvalidComponentException('The schema for the %s in the component metadata is invalid. Arbitrary additional properties are not allowed.'); } $schema['additionalProperties'] = FALSE; - // All props should also support "object" this allows deferring rendering - // in Twig to the render pipeline. - $schema_props = $metadata_info['props']; - foreach ($schema_props['properties'] ?? [] as $name => $prop_schema) { + foreach ($schema['properties'] ?? [] as $name => $prop_schema) { + if (isset($prop_schema['enum'])) { + // Ensure all enum values are also in meta:enum. + $enum = array_combine($prop_schema['enum'], $prop_schema['enum']); + $prop_schema['meta:enum'] = array_replace($enum, $prop_schema['meta:enum'] ?? []); + + // Remove meta:enum values which are not in enum. + $prop_schema['meta:enum'] = array_intersect_key($prop_schema['meta:enum'], $enum); + + // Make meta:enum label translatable. + $translation_context = $prop_schema['x-translation-context'] ?? ''; + $prop_schema['meta:enum'] = array_map( + // @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + fn($label) => new TranslatableMarkup((string) $label, [], ['context' => $translation_context]), + $prop_schema['meta:enum'] + ); + + $schema['properties'][$name] = $prop_schema; + } + + // All props should also support "object" this allows deferring + // rendering in Twig to the render pipeline. $type = $prop_schema['type'] ?? ''; $schema['properties'][$name]['type'] = array_unique([ ...(array) $type, diff --git a/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php b/core/lib/Drupal/Core/Theme/Component/ComponentValidator.php index 246f143d4e2..ff102b5170a 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 a0c93317699..5a3b62773ea 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; } /** diff --git a/core/lib/Drupal/Core/Theme/ImagePreprocess.php b/core/lib/Drupal/Core/Theme/ImagePreprocess.php new file mode 100644 index 00000000000..2f43ffe4c08 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ImagePreprocess.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\Core\Theme; + +use Drupal\Core\File\FileUrlGeneratorInterface; +use Drupal\Core\Template\AttributeHelper; + +/** + * Image theme preprocess. + * + * @internal + */ +class ImagePreprocess { + + public function __construct(protected FileUrlGeneratorInterface $fileUrlGenerator) { + } + + /** + * Prepares variables for image templates. + * + * Default template: image.html.twig. + * + * @param array $variables + * An associative array containing: + * - uri: Either the path of the image file (relative to base_path()) or a + * full URL. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0 + * always require an alt attribute. The HTML 5 draft allows the alt + * attribute to be omitted in some cases. Therefore, this variable + * defaults to an empty string, but can be set to NULL for the attribute + * to be omitted. Usually, neither omission nor an empty string satisfies + * accessibility requirements, so it is strongly encouraged for code + * building variables for image.html.twig templates to pass a meaningful + * value for this variable. + * - https://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 + * - https://www.w3.org/TR/xhtml1/dtds.html + * - http://dev.w3.org/html5/spec/Overview.html#alt + * - title: The title text is displayed when the image is hovered in some + * popular browsers. + * - attributes: Associative array of attributes to be placed in the img + * tag. + * - srcset: Array of multiple URIs and sizes/multipliers. + * - sizes: The sizes attribute for viewport-based selection of images. + * phpcs:ignore + * - http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#introduction-3:viewport-based-selection-2 + */ + public function preprocessImage(array &$variables): void { + if (!empty($variables['uri'])) { + $variables['attributes']['src'] = $this->fileUrlGenerator->generateString($variables['uri']); + } + // Generate a srcset attribute conforming to the spec at + // https://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-srcset + if (!empty($variables['srcset'])) { + $srcset = []; + foreach ($variables['srcset'] as $src) { + // URI is mandatory. + $source = $this->fileUrlGenerator->generateString($src['uri']); + if (isset($src['width']) && !empty($src['width'])) { + $source .= ' ' . $src['width']; + } + elseif (isset($src['multiplier']) && !empty($src['multiplier'])) { + $source .= ' ' . $src['multiplier']; + } + $srcset[] = $source; + } + $variables['attributes']['srcset'] = implode(', ', $srcset); + } + + foreach (['width', 'height', 'alt', 'title', 'sizes'] as $key) { + if (isset($variables[$key])) { + // If the property has already been defined in the attributes, + // do not override, including NULL. + if (AttributeHelper::attributeExists($key, $variables['attributes'])) { + continue; + } + $variables['attributes'][$key] = $variables[$key]; + } + } + + // Without dimensions specified, layout shifts can occur, + // which are more noticeable on pages that take some time to load. + // As a result, only mark images as lazy load that have dimensions. + if (isset($variables['width'], $variables['height']) && !isset($variables['attributes']['loading'])) { + $variables['attributes']['loading'] = 'lazy'; + } + } + +} diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 383776b65be..0ad83cff281 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -40,6 +40,11 @@ class Registry implements DestructableInterface { private const string PREPROCESS_INVOKES = 'preprocess invokes'; /** + * A common key for storing preprocess callbacks used by every theme hook. + */ + private const string GLOBAL_PREPROCESS = 'global preprocess'; + + /** * The theme object representing the active theme for this registry. * * @var \Drupal\Core\Theme\ActiveTheme @@ -410,10 +415,6 @@ class Registry implements DestructableInterface { * @see hook_theme_registry_alter() */ protected function build() { - $cache = [ - static::PREPROCESS_INVOKES => [], - ]; - $fixed_preprocess_functions = $this->collectModulePreprocess($cache, 'preprocess'); // First, preprocess the theme hooks advertised by modules. This will // serve as the basic registry. Since the list of enabled modules is the // same regardless of the theme used, this is cached in its own entry to @@ -422,6 +423,10 @@ class Registry implements DestructableInterface { $cache = $cached->data; } else { + $cache = [ + self::PREPROCESS_INVOKES => [], + ]; + $cache[self::GLOBAL_PREPROCESS] = $this->collectModulePreprocess($cache, 'preprocess'); if (defined('MAINTENANCE_MODE') && constant('MAINTENANCE_MODE') === 'install') { // System is still set here so preprocess can be updated in install. $this->processExtension($cache, 'system', 'install', 'system', $this->moduleList->getPath('system')); @@ -431,7 +436,6 @@ class Registry implements DestructableInterface { $this->processExtension($cache, $module, 'module', $module, $this->moduleList->getPath($module)); }); } - $this->addFixedPreprocessFunctions($cache, $fixed_preprocess_functions); // Only cache this registry if all modules are loaded. if ($this->moduleHandler->isLoaded()) { @@ -439,8 +443,6 @@ class Registry implements DestructableInterface { } } - $old_cache = $cache; - // Process each base theme. // Ensure that we start with the root of the parents, so that both CSS files // and preprocess functions comes first. @@ -461,10 +463,6 @@ class Registry implements DestructableInterface { // Hooks provided by the theme itself. $this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath()); - // Add the fixed preprocess functions to hooks defined by themes. They - // were already added to hooks defined by modules and potentially cached. - $this->addFixedPreprocessFunctions($cache, $fixed_preprocess_functions, $old_cache); - // Discover and add all preprocess functions for theme hook suggestions. $this->postProcessExtension($cache, $this->theme); @@ -674,7 +672,7 @@ class Registry implements DestructableInterface { // template. if ($type == 'theme' || $type == 'base_theme') { foreach ($cache as $hook => $info) { - if ($hook == static::PREPROCESS_INVOKES) { + if ($hook == self::PREPROCESS_INVOKES) { continue; } // Check only if not registered by the theme or engine. @@ -826,7 +824,7 @@ class Registry implements DestructableInterface { // Add missing preprocessor to existing hook. $cache[$hook]['preprocess functions'][] = $preprocessor; if (isset($invokes[$preprocessor])) { - $cache[static::PREPROCESS_INVOKES][$preprocessor] = $invokes[$preprocessor]; + $cache[self::PREPROCESS_INVOKES][$preprocessor] = $invokes[$preprocessor]; } } elseif (!isset($cache[$hook]) && strpos($hook, '__')) { @@ -836,7 +834,7 @@ class Registry implements DestructableInterface { $this->completeSuggestion($hook, $cache); $cache[$hook]['preprocess functions'][] = $preprocessor; if (isset($invokes[$preprocessor])) { - $cache[static::PREPROCESS_INVOKES][$preprocessor] = $invokes[$preprocessor]; + $cache[self::PREPROCESS_INVOKES][$preprocessor] = $invokes[$preprocessor]; } } } @@ -845,7 +843,7 @@ class Registry implements DestructableInterface { // hooks. This ensures that derivative hooks have a complete set of variable // preprocess functions. foreach ($cache as $hook => $info) { - if ($hook == static::PREPROCESS_INVOKES) { + if ($hook == self::PREPROCESS_INVOKES) { continue; } // The 'base hook' is only applied to derivative hooks already registered @@ -941,35 +939,6 @@ class Registry implements DestructableInterface { } /** - * Adds $prefix_preprocess functions to every hook. - * - * @param array $cache - * The theme registry, as documented in - * \Drupal\Core\Theme\Registry::processExtension(). - * @param array $fixed_preprocess_functions - * A list of preprocess functions. - * @param array $old_cache - * An already processed theme registry. - */ - protected function addFixedPreprocessFunctions(array &$cache, array $fixed_preprocess_functions, array $old_cache = []): void { - foreach (array_keys(array_diff_key($cache, $old_cache)) as $hook) { - if ($hook == static::PREPROCESS_INVOKES) { - continue; - } - if (!isset($cache[$hook]['preprocess functions'])) { - $cache[$hook]['preprocess functions'] = $fixed_preprocess_functions; - } - else { - $offset = 0; - while (isset($cache[$hook]['preprocess functions'][$offset]) && is_string($cache[$hook]['preprocess functions'][$offset]) && str_starts_with($cache[$hook]['preprocess functions'][$offset], 'template_')) { - $offset++; - } - array_splice($cache[$hook]['preprocess functions'], $offset, 0, $fixed_preprocess_functions); - } - } - } - - /** * Collect module implementations of a single hook. * * @param array $cache @@ -990,7 +959,7 @@ class Registry implements DestructableInterface { // implementations are not executed. $this->moduleHandler->invokeAllWith($hook, function (callable $callable, string $module) use ($hook, &$cache, &$preprocess_functions) { $function = $module . '_' . $hook; - $cache[static::PREPROCESS_INVOKES][$function] = ['module' => $module, 'hook' => $hook]; + $cache[self::PREPROCESS_INVOKES][$function] = ['module' => $module, 'hook' => $hook]; $preprocess_functions[] = $function; }); return $preprocess_functions; diff --git a/core/lib/Drupal/Core/Theme/ThemeCommonElements.php b/core/lib/Drupal/Core/Theme/ThemeCommonElements.php index 5ddf58fd6f2..50755c302a7 100644 --- a/core/lib/Drupal/Core/Theme/ThemeCommonElements.php +++ b/core/lib/Drupal/Core/Theme/ThemeCommonElements.php @@ -4,7 +4,11 @@ declare(strict_types=1); namespace Drupal\Core\Theme; +use Drupal\Core\Breadcrumb\BreadcrumbPreprocess; use Drupal\Core\Datetime\DatePreprocess; +use Drupal\Core\Field\FieldPreprocess; +use Drupal\Core\Menu\MenuPreprocess; +use Drupal\Core\Pager\PagerPreprocess; /** * Provide common theme render elements. @@ -34,6 +38,7 @@ class ThemeCommonElements { ], 'region' => [ 'render element' => 'elements', + 'initial preprocess' => ThemePreprocess::class . ':preprocessRegion', ], 'time' => [ 'variables' => [ @@ -98,11 +103,13 @@ class ThemeCommonElements { 'srcset' => [], 'style_name' => NULL, ], + 'initial preprocess' => ImagePreprocess::class . ':preprocessImage', ], 'breadcrumb' => [ 'variables' => [ 'links' => [], ], + 'initial preprocess' => BreadcrumbPreprocess::class . ':preprocessBreadcrumb', ], 'table' => [ 'variables' => [ @@ -116,11 +123,13 @@ class ThemeCommonElements { 'responsive' => TRUE, 'empty' => '', ], + 'initial preprocess' => ThemePreprocess::class . ':preprocessTable', ], 'tablesort_indicator' => [ 'variables' => [ 'style' => NULL, ], + 'initial preprocess' => ThemePreprocess::class . ':preprocessTablesortIndicator', ], 'mark' => [ 'variables' => [ @@ -137,6 +146,7 @@ class ThemeCommonElements { 'empty' => NULL, 'context' => [], ], + 'initial preprocess' => ThemePreprocess::class . ':preprocessItemList', ], 'feed_icon' => [ 'variables' => [ @@ -155,12 +165,13 @@ class ThemeCommonElements { 'indentation' => [ 'variables' => ['size' => 1], ], - // From theme.maintenance.inc. 'maintenance_page' => [ 'render element' => 'page', + 'initial preprocess' => ThemePreprocess::class . ':preprocessMaintenancePage', ], 'install_page' => [ 'render element' => 'page', + 'initial preprocess' => ThemePreprocess::class . ':preprocessInstallPage', ], 'maintenance_task_list' => [ 'variables' => [ @@ -168,6 +179,7 @@ class ThemeCommonElements { 'active' => NULL, 'variant' => NULL, ], + 'initial preprocess' => ThemePreprocess::class . ':preprocessMaintenanceTaskList', ], 'authorize_report' => [ 'variables' => [ @@ -180,6 +192,7 @@ class ThemeCommonElements { ], 'pager' => [ 'render element' => 'pager', + 'initial preprocess' => PagerPreprocess::class . ':preprocessPager', ], 'menu' => [ 'variables' => [ @@ -190,9 +203,11 @@ class ThemeCommonElements { ], 'menu_local_task' => [ 'render element' => 'element', + 'initial preprocess' => MenuPreprocess::class . ':preprocessMenuLocalTask', ], 'menu_local_action' => [ 'render element' => 'element', + 'initial preprocess' => MenuPreprocess::class . ':preprocessMenuLocalAction', ], 'menu_local_tasks' => [ 'variables' => [ @@ -241,9 +256,11 @@ class ThemeCommonElements { // From field system. 'field' => [ 'render element' => 'element', + 'initial preprocess' => FieldPreprocess::class . ':preprocessField', ], 'field_multiple_value_form' => [ 'render element' => 'element', + 'initial preprocess' => FieldPreprocess::class . ':preprocessFieldMultipleValueForm', ], 'off_canvas_page_wrapper' => [ 'variables' => [ diff --git a/core/lib/Drupal/Core/Theme/ThemeInitialization.php b/core/lib/Drupal/Core/Theme/ThemeInitialization.php index 10f173a15e7..7ea13f67ade 100644 --- a/core/lib/Drupal/Core/Theme/ThemeInitialization.php +++ b/core/lib/Drupal/Core/Theme/ThemeInitialization.php @@ -137,7 +137,7 @@ class ThemeInitialization implements ThemeInitializationInterface { $active_theme->getExtension()->load(); } else { - // Include non-engine theme files + // Include non-engine theme files. foreach (array_reverse($active_theme->getBaseThemeExtensions()) as $base) { // Include the theme file or the engine. if ($base->owner) { @@ -222,10 +222,10 @@ class ThemeInitialization implements ThemeInitializationInterface { } } - // Do basically the same as the above for libraries + // Do basically the same as the above for libraries. $values['libraries'] = []; - // Grab libraries from base theme + // Grab libraries from base theme. foreach ($base_themes as $base) { if (!empty($base->libraries)) { foreach ($base->libraries as $library) { diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index 3724ea6e156..c7c5f209070 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -288,27 +288,52 @@ class ThemeManager implements ThemeManagerInterface { } } + $invoke_preprocess_callback = function (mixed $preprocessor_function) use ($invoke_map, &$variables, $hook, $info): mixed { + // Preprocess hooks are stored as strings resembling functions. + // This is for backwards compatibility and may represent OOP + // implementations as well. + if (is_string($preprocessor_function) && isset($invoke_map[$preprocessor_function])) { + // Invoke module preprocess functions. + $this->moduleHandler->invoke(... $invoke_map[$preprocessor_function], args: [&$variables, $hook, $info]); + } + // Invoke preprocess callbacks that are not in the invoke map, such as + // those from themes or an alter hook. + elseif (is_callable($preprocessor_function)) { + call_user_func_array($preprocessor_function, [&$variables, $hook, $info]); + } + return $variables; + }; + + // Global preprocess functions are always called, after initial and + // template preprocess and before regular module and theme preprocess + // callbacks. template preprocess callbacks are deprecated but still + // supported, so they need to be called before the first non-template + // preprocess callback, and if that doesn't happen, after the loop. + $global_preprocess = $theme_registry->getGlobalPreprocess(); + $global_preprocess_called = FALSE; + // Invoke preprocess hooks. - // By default $info['preprocess functions'] should always be set, but it's - // good to check it if default Registry service implementation is - // overridden. See \Drupal\Core\Theme\Registry. if (isset($info['preprocess functions'])) { foreach ($info['preprocess functions'] as $preprocessor_function) { - // Preprocess hooks are stored as strings resembling functions. - // This is for backwards compatibility and may represent OOP - // implementations as well. - if (is_string($preprocessor_function) && isset($invoke_map[$preprocessor_function])) { - // While themes are not modules, ModuleHandlerInterface::invoke calls - // a legacy invoke which can can call any extension, not just - // modules. - $this->moduleHandler->invoke(... $invoke_map[$preprocessor_function], args: [&$variables, $hook, $info]); - } - // Check if hook_theme_registry_alter added a manual callback. - elseif (is_callable($preprocessor_function)) { - call_user_func_array($preprocessor_function, [&$variables, $hook, $info]); + // If global preprocess functions have not been called yet and this is + // not a template preprocess function, invoke them now. + if (!$global_preprocess_called && is_string($preprocessor_function) && !str_starts_with($preprocessor_function, 'template_')) { + $global_preprocess_called = TRUE; + foreach ($global_preprocess as $global_preprocess_callback) { + $invoke_preprocess_callback($global_preprocess_callback); + } } + $invoke_preprocess_callback($preprocessor_function); } } + + // If global process hasn't been invoked yet, do that now. + if (!$global_preprocess_called) { + foreach ($global_preprocess as $global_preprocess_callback) { + $invoke_preprocess_callback($global_preprocess_callback); + } + } + // Allow theme preprocess functions to set $variables['#attached'] and // $variables['#cache'] and use them like the corresponding element // properties on render arrays. This is the officially supported diff --git a/core/lib/Drupal/Core/Theme/ThemePreprocess.php b/core/lib/Drupal/Core/Theme/ThemePreprocess.php index 2a93d5b06c9..f712e3d9ff9 100644 --- a/core/lib/Drupal/Core/Theme/ThemePreprocess.php +++ b/core/lib/Drupal/Core/Theme/ThemePreprocess.php @@ -5,15 +5,18 @@ namespace Drupal\Core\Theme; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Crypt; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\Path\PathMatcherInterface; +use Drupal\Core\Render\Element; use Drupal\Core\Render\Markup; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Template\Attribute; use Drupal\Core\Url; +use Drupal\Core\Utility\TableSort; /** * Preprocess for common/core theme templates. @@ -356,4 +359,445 @@ class ThemePreprocess { } } + /** + * Prepares variables for maintenance page templates. + * + * Default template: maintenance-page.html.twig. + * + * @param array $variables + * An associative array containing: + * - content - An array of page content. + * + * @see system_page_attachments() + */ + public function preprocessMaintenancePage(array &$variables): void { + // @todo Rename the templates to page--maintenance + page--install. + $this->preprocessPage($variables); + + // @see system_page_attachments() + $variables['#attached']['library'][] = 'system/maintenance'; + + // Maintenance page and install page need branding info in variables because + // there is no blocks. + $site_config = $this->configFactory->get('system.site'); + $variables['logo'] = theme_get_setting('logo.url'); + $variables['site_name'] = $site_config->get('name'); + $variables['site_slogan'] = $site_config->get('slogan'); + + // Maintenance page and install page need page title in variable because + // there are no blocks. + $variables['title'] = $variables['page']['#title']; + } + + /** + * Prepares variables for install page templates. + * + * Default template: install-page.html.twig. + * + * @param array $variables + * An associative array containing: + * - content - An array of page content. + * + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessMaintenancePage() + */ + public function preprocessInstallPage(array &$variables): void { + $installer_active_task = NULL; + if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE === 'install' && InstallerKernel::installationAttempted()) { + $installer_active_task = $GLOBALS['install_state']['active_task']; + } + + $this->preprocessMaintenancePage($variables); + + // Override the site name that is displayed on the page, since Drupal is + // still in the process of being installed. + $distribution_name = drupal_install_profile_distribution_name(); + $variables['site_name'] = $distribution_name; + $variables['site_version'] = $installer_active_task ? drupal_install_profile_distribution_version() : ''; + } + + /** + * Prepares variables for region templates. + * + * Default template: region.html.twig. + * + * Prepares the values passed to the theme_region function to be passed into a + * pluggable template engine. Uses the region name to generate a template file + * suggestions. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing properties of the region. + */ + public function preprocessRegion(array &$variables): void { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['region'] = $variables['elements']['#region']; + } + + /** + * Prepares variables for table templates. + * + * Default template: table.html.twig. + * + * @param array $variables + * An associative array containing: + * - header: An array containing the table headers. Each element of the + * array can be either a localized string or an associative array with the + * following keys: + * - data: The localized title of the table column, as a string or render + * array. + * - field: The database field represented in the table column (required + * if user is to be able to sort on this column). + * - sort: A default sort order for this column ("asc" or "desc"). Only + * one column should be given a default sort order because table sorting + * only applies to one column at a time. + * - initial_click_sort: Set the initial sort of the column when clicked. + * Defaults to "asc". + * - class: An array of values for the 'class' attribute. In particular, + * the least important columns that can be hidden on narrow and medium + * width screens should have a 'priority-low' class, referenced with the + * RESPONSIVE_PRIORITY_LOW constant. Columns that should be shown on + * medium+ wide screens should be marked up with a class of + * 'priority-medium', referenced by with the RESPONSIVE_PRIORITY_MEDIUM + * constant. Themes may hide columns with one of these two classes on + * narrow viewports to save horizontal space. + * - Any HTML attributes, such as "colspan", to apply to the column header + * cell. + * - rows: An array of table rows. Every row is an array of cells, or an + * associative array with the following keys: + * - data: An array of cells. + * - Any HTML attributes, such as "class", to apply to the table row. + * - no_striping: A Boolean indicating that the row should receive no + * 'even / odd' styling. Defaults to FALSE. + * Each cell can be either a string or an associative array with the + * following keys: + * - data: The string or render array to display in the table cell. + * - header: Indicates this cell is a header. + * - Any HTML attributes, such as "colspan", to apply to the table cell. + * Here's an example for $rows: + * @code + * $rows = [ + * // Simple row + * [ + * 'Cell 1', 'Cell 2', 'Cell 3' + * ], + * // Row with attributes on the row and some of its cells. + * [ + * 'data' => ['Cell 1', ['data' => 'Cell 2', 'colspan' => 2]], 'class' => ['funky'] + * ], + * ]; + * @endcode + * - footer: An array of table rows which will be printed within a <tfoot> + * tag, in the same format as the rows element (see above). + * - attributes: An array of HTML attributes to apply to the table tag. + * - caption: A localized string to use for the <caption> tag. + * - colgroups: An array of column groups. Each element of the array can be + * either: + * - An array of columns, each of which is an associative array of HTML + * attributes applied to the <col> element. + * - An array of attributes applied to the <colgroup> element, which must + * include a "data" attribute. To add attributes to <col> elements, + * set the "data" attribute with an array of columns, each of which is + * an associative array of HTML attributes. + * Here's an example for $colgroup: + * @code + * $colgroup = [ + * // <colgroup> with one <col> element. + * [ + * [ + * 'class' => ['funky'], // Attribute for the <col> element. + * ], + * ], + * // <colgroup> with attributes and inner <col> elements. + * [ + * 'data' => [ + * [ + * 'class' => ['funky'], // Attribute for the <col> element. + * ], + * ], + * 'class' => ['jazzy'], // Attribute for the <colgroup> element. + * ], + * ]; + * @endcode + * These optional tags are used to group and set properties on columns + * within a table. For example, one may easily group three columns and + * apply same background style to all. + * - sticky: Use a "sticky" table header. + * - empty: The message to display in an extra row if table does not have + * any rows. + */ + public function preprocessTable(array &$variables): void { + // Format the table columns: + if (!empty($variables['colgroups'])) { + foreach ($variables['colgroups'] as &$colgroup) { + // Check if we're dealing with a simple or complex column + if (isset($colgroup['data'])) { + $cols = $colgroup['data']; + unset($colgroup['data']); + $colgroup_attributes = $colgroup; + } + else { + $cols = $colgroup; + $colgroup_attributes = []; + } + $colgroup = []; + $colgroup['attributes'] = new Attribute($colgroup_attributes); + $colgroup['cols'] = []; + + // Build columns. + if (is_array($cols) && !empty($cols)) { + foreach ($cols as $col_key => $col) { + $colgroup['cols'][$col_key]['attributes'] = new Attribute($col); + } + } + } + } + + // Build an associative array of responsive classes keyed by column. + $responsive_classes = []; + + // Format the table header: + $ts = []; + $header_columns = 0; + if (!empty($variables['header'])) { + $ts = TableSort::getContextFromRequest($variables['header'], \Drupal::request()); + + // Use a separate index with responsive classes as headers + // may be associative. + $responsive_index = -1; + foreach ($variables['header'] as $col_key => $cell) { + // Increase the responsive index. + $responsive_index++; + + if (!is_array($cell)) { + $header_columns++; + $cell_content = $cell; + $cell_attributes = new Attribute(); + $is_header = TRUE; + } + else { + if (isset($cell['colspan'])) { + $header_columns += $cell['colspan']; + } + else { + $header_columns++; + } + $cell_content = ''; + if (isset($cell['data'])) { + $cell_content = $cell['data']; + unset($cell['data']); + } + // Flag the cell as a header or not and remove the flag. + $is_header = $cell['header'] ?? TRUE; + unset($cell['header']); + + // Track responsive classes for each column as needed. Only the header + // cells for a column are marked up with the responsive classes by a + // module developer or themer. The responsive classes on the header + // cells must be transferred to the content cells. + if (!empty($cell['class']) && is_array($cell['class'])) { + if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) { + $responsive_classes[$responsive_index] = RESPONSIVE_PRIORITY_MEDIUM; + } + elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) { + $responsive_classes[$responsive_index] = RESPONSIVE_PRIORITY_LOW; + } + } + + TableSort::header($cell_content, $cell, $variables['header'], $ts); + + // TableSort::header() removes the 'sort', 'initial_click_sort' and + // 'field' keys. + $cell_attributes = new Attribute($cell); + } + $variables['header'][$col_key] = []; + $variables['header'][$col_key]['tag'] = $is_header ? 'th' : 'td'; + $variables['header'][$col_key]['attributes'] = $cell_attributes; + $variables['header'][$col_key]['content'] = $cell_content; + } + } + $variables['header_columns'] = $header_columns; + + // Rows and footer have the same structure. + $sections = ['rows' , 'footer']; + foreach ($sections as $section) { + if (!empty($variables[$section])) { + foreach ($variables[$section] as $row_key => $row) { + $cells = $row; + $row_attributes = []; + + // Check if we're dealing with a simple or complex row + if (isset($row['data'])) { + $cells = $row['data']; + $variables['no_striping'] = $row['no_striping'] ?? FALSE; + + // Set the attributes array and exclude 'data' and 'no_striping'. + $row_attributes = $row; + unset($row_attributes['data']); + unset($row_attributes['no_striping']); + } + + // Build row. + $variables[$section][$row_key] = []; + $variables[$section][$row_key]['attributes'] = new Attribute($row_attributes); + $variables[$section][$row_key]['cells'] = []; + if (!empty($cells)) { + // Reset the responsive index. + $responsive_index = -1; + foreach ($cells as $col_key => $cell) { + // Increase the responsive index. + $responsive_index++; + + if (!is_array($cell)) { + $cell_content = $cell; + $cell_attributes = []; + $is_header = FALSE; + } + else { + $cell_content = ''; + if (isset($cell['data'])) { + $cell_content = $cell['data']; + unset($cell['data']); + } + + // Flag the cell as a header or not and remove the flag. + $is_header = !empty($cell['header']); + unset($cell['header']); + + $cell_attributes = $cell; + } + // Active table sort information. + if (isset($variables['header'][$col_key]['data']) && $variables['header'][$col_key]['data'] == $ts['name'] && !empty($variables['header'][$col_key]['field'])) { + $variables[$section][$row_key]['cells'][$col_key]['active_table_sort'] = TRUE; + } + // Copy RESPONSIVE_PRIORITY_LOW/RESPONSIVE_PRIORITY_MEDIUM + // class from header to cell as needed. + if (isset($responsive_classes[$responsive_index])) { + $cell_attributes['class'][] = $responsive_classes[$responsive_index]; + } + $variables[$section][$row_key]['cells'][$col_key]['tag'] = $is_header ? 'th' : 'td'; + $variables[$section][$row_key]['cells'][$col_key]['attributes'] = new Attribute($cell_attributes); + $variables[$section][$row_key]['cells'][$col_key]['content'] = $cell_content; + } + } + } + } + } + if (empty($variables['no_striping'])) { + $variables['attributes']['data-striping'] = 1; + } + } + + /** + * Prepares variables for tablesort indicators. + * + * Default template: tablesort-indicator.html.twig. + */ + public function preprocessTablesortIndicator(array &$variables): void { + $variables['#attached']['library'][] = 'core/drupal.tablesort'; + } + + /** + * Prepares variables for item list templates. + * + * Default template: item-list.html.twig. + * + * @param array $variables + * An associative array containing: + * - items: An array of items to be displayed in the list. Each item can be + * either a string or a render array. If #type, #theme, or #markup + * properties are not specified for child render arrays, they will be + * inherited from the parent list, allowing callers to specify larger + * nested lists without having to explicitly specify and repeat the + * render properties for all nested child lists. + * - title: A title to be prepended to the list. + * - list_type: The type of list to return (e.g. "ul", "ol"). + * - wrapper_attributes: HTML attributes to be applied to the list wrapper. + * + * @see https://www.drupal.org/node/1842756 + */ + public function preprocessItemList(array &$variables): void { + $variables['wrapper_attributes'] = new Attribute($variables['wrapper_attributes']); + $variables['#attached']['library'][] = 'core/drupal.item-list'; + foreach ($variables['items'] as &$item) { + $attributes = []; + // If the item value is an array, then it is a render array. + if (is_array($item)) { + // List items support attributes via the '#wrapper_attributes' property. + if (isset($item['#wrapper_attributes'])) { + $attributes = $item['#wrapper_attributes']; + } + // Determine whether there are any child elements in the item that are + // not fully-specified render arrays. If there are any, then the child + // elements present nested lists and we automatically inherit the render + // array properties of the current list to them. + foreach (Element::children($item) as $key) { + $child = &$item[$key]; + // If this child element does not specify how it can be rendered, then + // we need to inherit the render properties of the current list. + if (!isset($child['#type']) && !isset($child['#theme']) && !isset($child['#markup'])) { + // Since item-list.html.twig supports both strings and render arrays + // as items, the items of the nested list may have been specified as + // the child elements of the nested list, instead of #items. For + // convenience, we automatically move them into #items. + if (!isset($child['#items'])) { + // This is the same condition as in + // \Drupal\Core\Render\Element::children(), which cannot be used + // here, since it triggers an error on string values. + foreach ($child as $child_key => $child_value) { + if (is_int($child_key) || $child_key === '' || $child_key[0] !== '#') { + $child['#items'][$child_key] = $child_value; + unset($child[$child_key]); + } + } + } + // Lastly, inherit the original theme variables of the current list. + $child['#theme'] = $variables['theme_hook_original']; + $child['#list_type'] = $variables['list_type']; + } + } + } + + // Set the item's value and attributes for the template. + $item = [ + 'value' => $item, + 'attributes' => new Attribute($attributes), + ]; + } + } + + /** + * Prepares variables for maintenance task list templates. + * + * Default template: maintenance-task-list.html.twig. + * + * @param array $variables + * An associative array containing: + * - items: An associative array of maintenance tasks. + * It's the caller's responsibility to ensure this array's items contain + * no dangerous HTML such as <script> tags. + * - active: The key for the currently active maintenance task. + */ + public function preprocessMaintenanceTaskList(array &$variables): void { + $items = $variables['items']; + $active = $variables['active']; + + $done = isset($items[$active]) || $active == NULL; + foreach ($items as $k => $item) { + $variables['tasks'][$k]['item'] = $item; + $variables['tasks'][$k]['attributes'] = new Attribute(); + if ($active == $k) { + $variables['tasks'][$k]['attributes']->addClass('is-active'); + $variables['tasks'][$k]['status'] = $this->t('active'); + $done = FALSE; + } + else { + if ($done) { + $variables['tasks'][$k]['attributes']->addClass('done'); + $variables['tasks'][$k]['status'] = $this->t('done'); + } + } + } + } + } diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Language.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Language.php index 40d74a8853a..05ef3dc1e26 100644 --- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Language.php +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Language.php @@ -36,8 +36,8 @@ class Language extends TypedData { * {@inheritdoc} * * @return \Drupal\Core\Language\LanguageInterface|null + * The language object, or NULL if the language is not set. */ - // phpcs:ignore Drupal.Commenting.FunctionComment.MissingReturnComment public function getValue() { if (!isset($this->language) && $this->id) { $this->language = \Drupal::languageManager()->getLanguage($this->id); diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php index 29efd63f7e4..71f939e220e 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php @@ -184,7 +184,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface { protected function validateConstraints($value, $cache_key, $constraints) { foreach ($constraints as $constraint) { // Prevent duplicate validation of constraints, in the case - // that constraints belong to multiple validated groups + // that constraints belong to multiple validated groups. if (isset($cache_key)) { $constraint_hash = spl_object_hash($constraint); diff --git a/core/lib/Drupal/Core/Update/UpdateHookRegistry.php b/core/lib/Drupal/Core/Update/UpdateHookRegistry.php index 19e64d98780..fa4ed8807b3 100644 --- a/core/lib/Drupal/Core/Update/UpdateHookRegistry.php +++ b/core/lib/Drupal/Core/Update/UpdateHookRegistry.php @@ -112,7 +112,7 @@ class UpdateHookRegistry { // possible functions which match '_update_'. We use preg_grep() here // since looping through all PHP functions can take significant page // execution time and this function is called on every administrative page - // via system_requirements(). + // via the system runtime_requirements hook. foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { // If this function is a module update function, add it to the list of // module updates. diff --git a/core/lib/Drupal/Core/Utility/Error.php b/core/lib/Drupal/Core/Utility/Error.php index 459af44d8c5..e460179b6f4 100644 --- a/core/lib/Drupal/Core/Utility/Error.php +++ b/core/lib/Drupal/Core/Utility/Error.php @@ -211,11 +211,15 @@ class Error { * * @return callable|null * The current error handler as a callable, or NULL if none is set. + * + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use + * get_error_handler() instead. + * + * @see https://www.drupal.org/node/3529500 */ public static function currentErrorHandler(): ?callable { - $currentHandler = set_error_handler('var_dump'); - restore_error_handler(); - return $currentHandler; + @trigger_error(__METHOD__ . ' is deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use get_error_handler() instead. See https://www.drupal.org/node/3529500', E_USER_DEPRECATED); + return get_error_handler(); } } diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php index a6846ba263c..bdc8188b5e4 100644 --- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php +++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php @@ -176,8 +176,21 @@ class ThemeRegistry extends CacheCollector implements DestructableInterface { * * @internal */ - public function getPreprocessInvokes() { + public function getPreprocessInvokes(): array { return $this->get('preprocess invokes'); } + /** + * Gets global preprocess callbacks. + * + * @return array + * An array of preprocess callbacks that should be called for every theme + * hook. + * + * @internal + */ + public function getGlobalPreprocess(): array { + return $this->get('global preprocess'); + } + } diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php index c139cbf05c3..6e44238c8fa 100644 --- a/core/lib/Drupal/Core/Utility/Token.php +++ b/core/lib/Drupal/Core/Utility/Token.php @@ -305,7 +305,8 @@ class Token { // Iterate through the matches, building an associative array containing // $tokens grouped by $types, pointing to the version of the token found in - // the source text. For example, $results['node']['title'] = '[node:title]'; + // the source text. For example, + // "$results['node']['title'] = '[node:title]'". $results = []; for ($i = 0; $i < count($tokens); $i++) { $results[$types[$i]][$tokens[$i]] = $matches[0][$i]; diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php index 00f3c862af4..56f55832dc5 100644 --- a/core/lib/Drupal/Core/Validation/ConstraintManager.php +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -15,6 +15,7 @@ use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\IdenticalTo; use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\PositiveOrZero; /** * Constraint plugin manager. @@ -123,6 +124,11 @@ class ConstraintManager extends DefaultPluginManager { 'class' => Image::class, 'type' => ['string'], ]); + $this->getDiscovery()->setDefinition('PositiveOrZero', [ + 'label' => new TranslatableMarkup('Positive or zero'), + 'class' => PositiveOrZero::class, + 'type' => ['integer'], + ]); $this->getDiscovery()->setDefinition('IdenticalTo', [ 'label' => new TranslatableMarkup('IdenticalTo'), 'class' => IdenticalTo::class, diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php index 857e2c1b099..1e69f1bfc7f 100644 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php @@ -158,7 +158,7 @@ class UniqueFieldValueValidator extends ConstraintValidator implements Container private function extractDuplicates(array $item_values): array { $value_frequency = array_count_values($item_values); - // Filter out item values which are not duplicates while preserving deltas + // Filter out item values which are not duplicates while preserving deltas. $duplicate_values = array_intersect($item_values, array_keys(array_filter( $value_frequency, function ($value) { return $value > 1; |