summaryrefslogtreecommitdiffstatshomepage
path: root/core/lib
diff options
context:
space:
mode:
Diffstat (limited to 'core/lib')
-rw-r--r--core/lib/Drupal.php2
-rw-r--r--core/lib/Drupal/Component/Annotation/Doctrine/DocParser.php34
-rw-r--r--core/lib/Drupal/Component/Annotation/composer.json2
-rw-r--r--core/lib/Drupal/Component/Datetime/DateTimePlus.php56
-rw-r--r--core/lib/Drupal/Component/DependencyInjection/composer.json4
-rw-r--r--core/lib/Drupal/Component/EventDispatcher/composer.json6
-rw-r--r--core/lib/Drupal/Component/Gettext/PoItem.php2
-rw-r--r--core/lib/Drupal/Component/HttpFoundation/composer.json2
-rw-r--r--core/lib/Drupal/Component/Plugin/LazyPluginCollection.php6
-rw-r--r--core/lib/Drupal/Component/Plugin/PluginManagerBase.php2
-rw-r--r--core/lib/Drupal/Component/Plugin/composer.json2
-rw-r--r--core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php4
-rw-r--r--core/lib/Drupal/Component/Render/FormattableMarkup.php8
-rw-r--r--core/lib/Drupal/Component/Serialization/composer.json2
-rw-r--r--core/lib/Drupal/Component/Utility/DeprecationHelper.php4
-rw-r--r--core/lib/Drupal/Component/Utility/Tags.php2
-rw-r--r--core/lib/Drupal/Component/Utility/UserAgent.php2
-rw-r--r--core/lib/Drupal/Core/Access/AccessGroupAnd.php55
-rw-r--r--core/lib/Drupal/Core/Access/DependentAccessInterface.php35
-rw-r--r--core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php46
-rw-r--r--core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php50
-rw-r--r--core/lib/Drupal/Core/Action/ActionPluginCollection.php2
-rw-r--r--core/lib/Drupal/Core/Action/ConfigurableActionBase.php24
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxResponse.php17
-rw-r--r--core/lib/Drupal/Core/Asset/AssetResolver.php45
-rw-r--r--core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php9
-rw-r--r--core/lib/Drupal/Core/Asset/CssOptimizer.php3
-rw-r--r--core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php9
-rw-r--r--core/lib/Drupal/Core/Breadcrumb/BreadcrumbPreprocess.php32
-rw-r--r--core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php13
-rw-r--r--core/lib/Drupal/Core/Cache/CacheTagsPurgeInterface.php24
-rw-r--r--core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php18
-rw-r--r--core/lib/Drupal/Core/Command/DbCommandBase.php2
-rw-r--r--core/lib/Drupal/Core/Command/GenerateProxyClassApplication.php2
-rw-r--r--core/lib/Drupal/Core/Command/GenerateProxyClassCommand.php2
-rw-r--r--core/lib/Drupal/Core/Condition/ConditionPluginCollection.php2
-rw-r--r--core/lib/Drupal/Core/Config/Config.php2
-rw-r--r--core/lib/Drupal/Core/Config/ConfigBase.php2
-rw-r--r--core/lib/Drupal/Core/Config/DatabaseStorage.php2
-rw-r--r--core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php3
-rw-r--r--core/lib/Drupal/Core/Config/Schema/Mapping.php6
-rw-r--r--core/lib/Drupal/Core/Config/Schema/TypeResolver.php4
-rw-r--r--core/lib/Drupal/Core/Config/TypedConfigManager.php7
-rw-r--r--core/lib/Drupal/Core/Cron.php2
-rw-r--r--core/lib/Drupal/Core/Database/Connection.php6
-rw-r--r--core/lib/Drupal/Core/Database/Database.php17
-rw-r--r--core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php12
-rw-r--r--core/lib/Drupal/Core/Database/Query/Condition.php12
-rw-r--r--core/lib/Drupal/Core/Database/Query/Select.php12
-rw-r--r--core/lib/Drupal/Core/Database/Schema.php6
-rw-r--r--core/lib/Drupal/Core/Database/Statement/PdoResult.php12
-rw-r--r--core/lib/Drupal/Core/Database/Statement/PdoTrait.php25
-rw-r--r--core/lib/Drupal/Core/Database/Statement/ResultBase.php8
-rw-r--r--core/lib/Drupal/Core/Database/Statement/StatementBase.php38
-rw-r--r--core/lib/Drupal/Core/Database/StatementInterface.php8
-rw-r--r--core/lib/Drupal/Core/Database/StatementPrefetchIterator.php21
-rw-r--r--core/lib/Drupal/Core/Database/StatementWrapperIterator.php23
-rw-r--r--core/lib/Drupal/Core/Database/Transaction.php30
-rw-r--r--core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php134
-rw-r--r--core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php4
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php4
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/Compiler/BackendCompilerPass.php5
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php34
-rw-r--r--core/lib/Drupal/Core/Display/VariantBase.php21
-rw-r--r--core/lib/Drupal/Core/DrupalKernel.php7
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityBase.php11
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityForm.php3
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityInterface.php9
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Controller/EntityController.php9
-rw-r--r--core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php25
-rw-r--r--core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php39
-rw-r--r--core/lib/Drupal/Core/Entity/EntityType.php5
-rw-r--r--core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php6
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Query/QueryInterface.php6
-rw-r--r--core/lib/Drupal/Core/Entity/entity.api.php2
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php2
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php10
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php4
-rw-r--r--core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php13
-rw-r--r--core/lib/Drupal/Core/Extension/ModuleHandler.php32
-rw-r--r--core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php12
-rw-r--r--core/lib/Drupal/Core/Extension/Requirement/RequirementSeverity.php120
-rw-r--r--core/lib/Drupal/Core/Extension/ThemeInstaller.php120
-rw-r--r--core/lib/Drupal/Core/Extension/module.api.php84
-rw-r--r--core/lib/Drupal/Core/Field/FieldItemInterface.php8
-rw-r--r--core/lib/Drupal/Core/Field/FieldPreprocess.php205
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php4
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php5
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php22
-rw-r--r--core/lib/Drupal/Core/Field/WidgetBase.php44
-rw-r--r--core/lib/Drupal/Core/Field/WidgetInterface.php51
-rw-r--r--core/lib/Drupal/Core/File/HtaccessWriter.php39
-rw-r--r--core/lib/Drupal/Core/FileTransfer/FTP.php1
-rw-r--r--core/lib/Drupal/Core/FileTransfer/FTPExtension.php2
-rw-r--r--core/lib/Drupal/Core/FileTransfer/FileTransfer.php2
-rw-r--r--core/lib/Drupal/Core/FileTransfer/Local.php2
-rw-r--r--core/lib/Drupal/Core/FileTransfer/SSH.php1
-rw-r--r--core/lib/Drupal/Core/Form/FormBase.php31
-rw-r--r--core/lib/Drupal/Core/Form/FormBuilder.php24
-rw-r--r--core/lib/Drupal/Core/Form/FormState.php6
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/FormAlter.php54
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/Hook.php26
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/Preprocess.php23
-rw-r--r--core/lib/Drupal/Core/Hook/HookCollectorPass.php8
-rw-r--r--core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php2
-rw-r--r--core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php2
-rw-r--r--core/lib/Drupal/Core/Layout/LayoutDefault.php29
-rw-r--r--core/lib/Drupal/Core/Layout/LayoutInterface.php2
-rw-r--r--core/lib/Drupal/Core/Layout/LayoutPluginManager.php5
-rw-r--r--core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php13
-rw-r--r--core/lib/Drupal/Core/Mailer/Transport/SendmailCommandValidationTransportFactory.php52
-rw-r--r--core/lib/Drupal/Core/Mailer/TransportServiceFactory.php44
-rw-r--r--core/lib/Drupal/Core/Mailer/TransportServiceFactoryInterface.php28
-rw-r--r--core/lib/Drupal/Core/Mailer/TransportServiceFactoryTrait.php42
-rw-r--r--core/lib/Drupal/Core/Menu/MenuPreprocess.php73
-rw-r--r--core/lib/Drupal/Core/Pager/PagerPreprocess.php171
-rw-r--r--core/lib/Drupal/Core/Plugin/ConfigurablePluginBase.php31
-rw-r--r--core/lib/Drupal/Core/Plugin/ConfigurableTrait.php81
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextInterface.php2
-rw-r--r--core/lib/Drupal/Core/Plugin/ContextAwarePluginTrait.php4
-rw-r--r--core/lib/Drupal/Core/ProxyClass/Cron.php80
-rw-r--r--core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php13
-rw-r--r--core/lib/Drupal/Core/Recipe/InputConfigurator.php28
-rw-r--r--core/lib/Drupal/Core/Recipe/Recipe.php8
-rw-r--r--core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php3
-rw-r--r--core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php3
-rw-r--r--core/lib/Drupal/Core/Render/Element/Button.php10
-rw-r--r--core/lib/Drupal/Core/Render/Element/Checkbox.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Checkboxes.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Color.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/ComponentElement.php13
-rw-r--r--core/lib/Drupal/Core/Render/Element/Container.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Date.php13
-rw-r--r--core/lib/Drupal/Core/Render/Element/Details.php10
-rw-r--r--core/lib/Drupal/Core/Render/Element/Dropbutton.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/ElementInterface.php113
-rw-r--r--core/lib/Drupal/Core/Render/Element/Email.php10
-rw-r--r--core/lib/Drupal/Core/Render/Element/File.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/FormElement.php3
-rw-r--r--core/lib/Drupal/Core/Render/Element/FormElementBase.php141
-rw-r--r--core/lib/Drupal/Core/Render/Element/Generic.php31
-rw-r--r--core/lib/Drupal/Core/Render/Element/Hidden.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/HtmlTag.php13
-rw-r--r--core/lib/Drupal/Core/Render/Element/Icon.php10
-rw-r--r--core/lib/Drupal/Core/Render/Element/InlineTemplate.php9
-rw-r--r--core/lib/Drupal/Core/Render/Element/Link.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/MachineName.php12
-rw-r--r--core/lib/Drupal/Core/Render/Element/MoreLink.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Number.php13
-rw-r--r--core/lib/Drupal/Core/Render/Element/Page.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/Pager.php30
-rw-r--r--core/lib/Drupal/Core/Render/Element/Password.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/PasswordConfirm.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Radios.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Range.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/RenderElement.php3
-rw-r--r--core/lib/Drupal/Core/Render/Element/RenderElementBase.php409
-rw-r--r--core/lib/Drupal/Core/Render/Element/Select.php28
-rw-r--r--core/lib/Drupal/Core/Render/Element/StatusReport.php31
-rw-r--r--core/lib/Drupal/Core/Render/Element/Submit.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/Table.php24
-rw-r--r--core/lib/Drupal/Core/Render/Element/Tableselect.php16
-rw-r--r--core/lib/Drupal/Core/Render/Element/Tel.php7
-rw-r--r--core/lib/Drupal/Core/Render/Element/Textarea.php13
-rw-r--r--core/lib/Drupal/Core/Render/Element/Textfield.php18
-rw-r--r--core/lib/Drupal/Core/Render/Element/TitleDisplay.php22
-rw-r--r--core/lib/Drupal/Core/Render/Element/Url.php10
-rw-r--r--core/lib/Drupal/Core/Render/Element/Value.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/VerticalTabs.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Weight.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Widget.php26
-rw-r--r--core/lib/Drupal/Core/Render/ElementInfoManager.php142
-rw-r--r--core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php62
-rw-r--r--core/lib/Drupal/Core/Render/Renderer.php64
-rw-r--r--core/lib/Drupal/Core/Render/RendererInterface.php2
-rw-r--r--core/lib/Drupal/Core/Routing/UrlGenerator.php8
-rw-r--r--core/lib/Drupal/Core/Session/SessionManager.php11
-rw-r--r--core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php2
-rw-r--r--core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php2
-rw-r--r--core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php28
-rw-r--r--core/lib/Drupal/Core/Template/Loader/ComponentLoader.php7
-rw-r--r--core/lib/Drupal/Core/Template/TwigExtension.php2
-rw-r--r--core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php31
-rw-r--r--core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php407
-rw-r--r--core/lib/Drupal/Core/Test/PhpUnitTestDiscoveryTracer.php38
-rw-r--r--core/lib/Drupal/Core/Test/RunTests/TestFileParser.php7
-rw-r--r--core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php6
-rw-r--r--core/lib/Drupal/Core/Test/TestDiscovery.php41
-rw-r--r--core/lib/Drupal/Core/Test/TestRun.php2
-rw-r--r--core/lib/Drupal/Core/Test/TestRunnerKernel.php4
-rw-r--r--core/lib/Drupal/Core/Test/TestSetupTrait.php6
-rw-r--r--core/lib/Drupal/Core/Test/TestStatus.php2
-rw-r--r--core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php35
-rw-r--r--core/lib/Drupal/Core/Theme/Component/ComponentValidator.php4
-rw-r--r--core/lib/Drupal/Core/Theme/ComponentPluginManager.php4
-rw-r--r--core/lib/Drupal/Core/Theme/ImagePreprocess.php90
-rw-r--r--core/lib/Drupal/Core/Theme/Registry.php59
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeCommonElements.php19
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeInitialization.php6
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeManager.php55
-rw-r--r--core/lib/Drupal/Core/Theme/ThemePreprocess.php444
-rw-r--r--core/lib/Drupal/Core/TypedData/Plugin/DataType/Language.php2
-rw-r--r--core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php2
-rw-r--r--core/lib/Drupal/Core/Update/UpdateHookRegistry.php2
-rw-r--r--core/lib/Drupal/Core/Utility/Error.php10
-rw-r--r--core/lib/Drupal/Core/Utility/ThemeRegistry.php15
-rw-r--r--core/lib/Drupal/Core/Utility/Token.php3
-rw-r--r--core/lib/Drupal/Core/Validation/ConstraintManager.php6
-rw-r--r--core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php2
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="&lt;front&gt;">
+ // '<a href="/" data-drupal-link-system-path="&lt;front&gt;">'.
$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;