summaryrefslogtreecommitdiffstatshomepage
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-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/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/Render/Renderer.php15
-rw-r--r--core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php6
-rw-r--r--core/modules/block/migrations/d6_block.yml2
-rw-r--r--core/modules/block/migrations/d7_block.yml2
-rw-r--r--core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php4
-rw-r--r--core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php2
-rw-r--r--core/modules/block_content/src/Controller/BlockContentController.php5
-rw-r--r--core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php6
-rw-r--r--core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php6
-rw-r--r--core/modules/block_content/tests/src/Functional/LocalActionTest.php53
-rw-r--r--core/modules/comment/src/Hook/CommentThemeHooks.php4
-rw-r--r--core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php4
-rw-r--r--core/modules/config_translation/migrations/d6_block_translation.yml2
-rw-r--r--core/modules/config_translation/migrations/d7_block_translation.yml2
-rw-r--r--core/modules/contact/src/Hook/ContactFormHooks.php6
-rw-r--r--core/modules/contextual/src/Hook/ContextualThemeHooks.php4
-rw-r--r--core/modules/locale/src/Hook/LocaleThemeHooks.php4
-rw-r--r--core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php6
-rw-r--r--core/modules/migrate/tests/src/Unit/MigrateSourceTest.php27
-rw-r--r--core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php1
-rw-r--r--core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php1
-rw-r--r--core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php2
-rw-r--r--core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php2
-rw-r--r--core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php6
-rw-r--r--core/modules/node/src/Hook/NodeHooks.php9
-rw-r--r--core/modules/node/src/Hook/NodeThemeHooks.php4
-rw-r--r--core/modules/node/src/Plugin/Block/SyndicateBlock.php6
-rw-r--r--core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php102
-rw-r--r--core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php2
-rw-r--r--core/modules/node/tests/src/Functional/NodeTranslationUITest.php24
-rw-r--r--core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php1
-rw-r--r--core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php2
-rw-r--r--core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gzbin162581 -> 162168 bytes
-rw-r--r--core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gzbin596855 -> 596593 bytes
-rw-r--r--core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php38
-rw-r--r--core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php6
-rw-r--r--core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php4
-rw-r--r--core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php1
-rw-r--r--core/modules/views/tests/src/Functional/GlossaryTest.php1
-rw-r--r--core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php34
-rw-r--r--core/modules/views/views.theme.inc14
-rw-r--r--core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php24
-rw-r--r--core/recipes/standard/recipe.yml1
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php51
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php6
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php1278
-rw-r--r--core/themes/olivero/config/optional/block.block.olivero_syndicate.yml20
53 files changed, 1761 insertions, 310 deletions
diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php
index dcecc44e17c6..b8693e4bb761 100644
--- a/core/lib/Drupal/Core/Database/Transaction.php
+++ b/core/lib/Drupal/Core/Database/Transaction.php
@@ -30,12 +30,12 @@ class Transaction {
/**
* Destructs the object.
*
- * Depending on the nesting level of the object, this leads to a COMMIT (for
- * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on
- * the database.
+ * If the transaction is still active at this stage, and depending on the
+ * state of the transaction stack, this leads to a COMMIT (for a root item)
+ * or to a RELEASE SAVEPOINT (for a savepoint item) executed on the database.
*/
public function __destruct() {
- $this->connection->transactionManager()->unpile($this->name, $this->id);
+ $this->connection->transactionManager()->purge($this->name, $this->id);
}
/**
@@ -46,16 +46,22 @@ class Transaction {
}
/**
- * Rolls back the current transaction.
+ * Returns the transaction to the parent nesting level.
*
- * This is just a wrapper method to rollback whatever transaction stack we are
- * currently in, which is managed by the TransactionManager. Note that logging
- * needs to happen after a transaction has been rolled back or the log
- * messages will be rolled back too.
+ * Depending on the state of the transaction stack, this leads to a COMMIT
+ * operation (for a root item), or to a RELEASE SAVEPOINT operation (for a
+ * savepoint item) executed on the database.
+ */
+ public function commitOrRelease(): void {
+ $this->connection->transactionManager()->unpile($this->name, $this->id);
+ }
+
+ /**
+ * Rolls back the transaction.
*
- * Depending on the nesting level of the object, this leads to a ROLLBACK (for
- * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed
- * on the database.
+ * Depending on the state of the transaction stack, this leads to a ROLLBACK
+ * operation (for a root item), or to a ROLLBACK TO SAVEPOINT + a RELEASE
+ * SAVEPOINT operations (for a savepoint item) executed on the database.
*/
public function rollBack() {
$this->connection->transactionManager()->rollback($this->name, $this->id);
diff --git a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php
index aa663d942265..fa1a309a7678 100644
--- a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php
+++ b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php
@@ -102,6 +102,16 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
private ClientConnectionTransactionState $connectionTransactionState;
/**
+ * Whether to trigger warnings when unpiling a void transaction.
+ *
+ * Normally FALSE, is set to TRUE by specific tests checking the internal
+ * state of the transaction stack.
+ *
+ * @internal
+ */
+ public bool $triggerWarningWhenUnpilingOnVoidTransaction = FALSE;
+
+ /**
* Constructor.
*
* @param \Drupal\Core\Database\Connection $connection
@@ -202,7 +212,9 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
protected function voidStackItem(string $id): void {
// The item should be removed from $stack and added to $voidedItems for
// later processing.
- $this->voidedItems[$id] = $this->stack[$id];
+ if (isset($this->stack[$id])) {
+ $this->voidedItems[$id] = $this->stack[$id];
+ }
$this->removeStackItem($id);
}
@@ -285,14 +297,29 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
}
/**
- * {@inheritdoc}
+ * Purges a Drupal transaction from the manager.
+ *
+ * This is only called by a Transaction object's ::__destruct() method and
+ * should only be called internally by a database driver.
+ *
+ * @param string $name
+ * The name of the transaction.
+ * @param string $id
+ * The id of the transaction.
+ *
+ * @throws \Drupal\Core\Database\TransactionOutOfOrderException
+ * If a Drupal Transaction with the specified name does not exist.
+ * @throws \Drupal\Core\Database\TransactionCommitFailedException
+ * If the commit of the root transaction failed.
+ *
+ * @internal
*/
- public function unpile(string $name, string $id): void {
+ public function purge(string $name, string $id): void {
// If this is a 'root' transaction, and it is voided (that is, no longer in
// the stack), then the transaction on the database is no longer active. An
- // action such as a rollback, or a DDL statement, was executed that
- // terminated the database transaction. So, we can process the post
- // transaction callbacks.
+ // action such as a commit, a release savepoint, a rollback, or a DDL
+ // statement, was executed that terminated the database transaction. So, we
+ // can process the post transaction callbacks.
if (!isset($this->stack()[$id]) && isset($this->voidedItems[$id]) && $this->rootId === $id) {
$this->processPostTransactionCallbacks();
$this->rootId = NULL;
@@ -309,6 +336,62 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
return;
}
+ // When we get here, the transaction (or savepoint) is still active on the
+ // database. We can unpile it, and if we are left with no more items in the
+ // stack, we can also process the post transaction callbacks.
+ $this->commit($name, $id);
+ $this->removeStackItem($id);
+ if ($this->rootId === $id) {
+ $this->processPostTransactionCallbacks();
+ $this->rootId = NULL;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function unpile(string $name, string $id): void {
+ // If the transaction was voided, we cannot unpile. Skip but trigger a user
+ // warning if requested.
+ if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) {
+ if ($this->triggerWarningWhenUnpilingOnVoidTransaction) {
+ trigger_error('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', E_USER_WARNING);
+ }
+ return;
+ }
+
+ // If there is no $id to commit, or if $id does not correspond to the one
+ // in the stack for that $name, the commit is out of order.
+ if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) {
+ throw new TransactionOutOfOrderException("Error attempting commit of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString());
+ }
+
+ // Commit the transaction.
+ $this->commit($name, $id);
+
+ // Void the transaction stack item.
+ $this->voidStackItem($id);
+ }
+
+ /**
+ * Commits a Drupal transaction.
+ *
+ * @param string $name
+ * The name of the transaction.
+ * @param string $id
+ * The id of the transaction.
+ *
+ * @throws \Drupal\Core\Database\TransactionOutOfOrderException
+ * If a Drupal Transaction with the specified name does not exist.
+ * @throws \Drupal\Core\Database\TransactionCommitFailedException
+ * If the commit of the root transaction failed.
+ */
+ protected function commit(string $name, string $id): void {
+ if ($this->getConnectionTransactionState() !== ClientConnectionTransactionState::Active) {
+ // The stack got corrupted.
+ throw new TransactionOutOfOrderException("Transaction {$id}\\{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
+ }
+
// If we are not releasing the last savepoint but an earlier one, or
// committing a root transaction while savepoints are active, all
// subsequent savepoints will be released as well. The stack must be
@@ -317,33 +400,20 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
$this->voidStackItem((string) $i);
}
- if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) {
- if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) {
- // Release the client transaction savepoint in case the Drupal
- // transaction is not a root one.
- $this->releaseClientSavepoint($name);
- }
- elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) {
- // If this was the root Drupal transaction, we can commit the client
- // transaction.
- $this->processRootCommit();
- if ($this->rootId === $id) {
- $this->processPostTransactionCallbacks();
- $this->rootId = NULL;
- }
- }
- else {
- // The stack got corrupted.
- throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
- }
-
- // Remove the transaction from the stack.
- $this->removeStackItem($id);
- return;
+ if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) {
+ // Release the client transaction savepoint in case the Drupal
+ // transaction is not a root one.
+ $this->releaseClientSavepoint($name);
+ }
+ elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) {
+ // If this was the root Drupal transaction, we can commit the client
+ // transaction.
+ $this->processRootCommit();
+ }
+ else {
+ // The stack got corrupted.
+ throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
}
-
- // The stack got corrupted.
- throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
}
/**
diff --git a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php
index 11af511f14bc..a9aa2c770525 100644
--- a/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php
+++ b/core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php
@@ -53,8 +53,8 @@ interface TransactionManagerInterface {
* Removes a Drupal transaction from the stack.
*
* The unpiled item does not necessarily need to be the last on the stack.
- * This method should only be called by a Transaction object going out of
- * scope.
+ * This method should only be called by a Transaction object's
+ * ::commitOrRelease() method.
*
* This method should only be called internally by a database driver.
*
diff --git a/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php b/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php
deleted file mode 100644
index 158010463d2a..000000000000
--- a/core/lib/Drupal/Core/Hook/Attribute/FormAlter.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Core\Hook\Attribute;
-
-use Drupal\Core\Hook\Order\OrderInterface;
-
-/**
- * Hook attribute for FormAlter.
- *
- * @see hook_form_alter().
- */
-#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
-class FormAlter extends Hook {
-
- /**
- * {@inheritdoc}
- */
- public const string PREFIX = 'form';
-
- /**
- * {@inheritdoc}
- */
- public const string SUFFIX = 'alter';
-
- /**
- * Constructs a FormAlter attribute object.
- *
- * @param string $form_id
- * (optional) The ID of the form that this implementation alters.
- * If this is left blank then `form_alter` is the hook that is registered.
- * @param string $method
- * (optional) The method name. If this attribute is on a method, this
- * parameter is not required. If this attribute is on a class and this
- * parameter is omitted, the class must have an __invoke() method, which is
- * taken as the hook implementation.
- * @param string|null $module
- * (optional) The module this implementation is for. This allows one module
- * to implement a hook on behalf of another module. Defaults to the module
- * the implementation is in.
- * @param \Drupal\Core\Hook\Order\OrderInterface|null $order
- * (optional) Set the order of the implementation.
- */
- public function __construct(
- string $form_id = '',
- public string $method = '',
- public ?string $module = NULL,
- public ?OrderInterface $order = NULL,
- ) {
- parent::__construct($form_id, $method, $module, $order);
- }
-
-}
diff --git a/core/lib/Drupal/Core/Hook/Attribute/Hook.php b/core/lib/Drupal/Core/Hook/Attribute/Hook.php
index 34dbc8ebf916..0084e651180d 100644
--- a/core/lib/Drupal/Core/Hook/Attribute/Hook.php
+++ b/core/lib/Drupal/Core/Hook/Attribute/Hook.php
@@ -98,27 +98,10 @@ use Drupal\Core\Hook\Order\OrderInterface;
class Hook implements HookAttributeInterface {
/**
- * The hook prefix such as `form`.
- *
- * @var string
- */
- public const string PREFIX = '';
-
- /**
- * The hook suffix such as `alter`.
- *
- * @var string
- */
- public const string SUFFIX = '';
-
- /**
* Constructs a Hook attribute object.
*
* @param string $hook
* The short hook name, without the 'hook_' prefix.
- * $hook is only optional when Hook is extended and a PREFIX or SUFFIX is
- * defined. When using the [#Hook] attribute directly $hook is required.
- * See Drupal\Core\Hook\Attribute\Preprocess.
* @param string $method
* (optional) The method name. If this attribute is on a method, this
* parameter is not required. If this attribute is on a class and this
@@ -132,15 +115,10 @@ class Hook implements HookAttributeInterface {
* (optional) Set the order of the implementation.
*/
public function __construct(
- public string $hook = '',
+ public string $hook,
public string $method = '',
public ?string $module = NULL,
public ?OrderInterface $order = NULL,
- ) {
- $this->hook = implode('_', array_filter([static::PREFIX, $hook, static::SUFFIX]));
- if ($this->hook === '') {
- throw new \LogicException('The Hook attribute or an attribute extending the Hook attribute must provide the $hook parameter, a PREFIX or a SUFFIX.');
- }
- }
+ ) {}
}
diff --git a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php b/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php
deleted file mode 100644
index 47642859a20b..000000000000
--- a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Core\Hook\Attribute;
-
-/**
- * Attribute for defining a class method as a preprocess function.
- *
- * Pass no arguments for hook_preprocess `#[Preprocess]`.
- * For `hook_preprocess_HOOK` pass the `HOOK` without the `hook_preprocess`
- * portion `#[Preprocess('HOOK')]`.
- *
- * See \Drupal\Core\Hook\Attribute\Hook for additional information.
- */
-#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
-class Preprocess extends Hook {
- /**
- * {@inheritdoc}
- */
- public const string PREFIX = 'preprocess';
-
-}
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index d0150fe0127a..fe3f29ea6968 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -250,9 +250,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 +305,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'])) {
diff --git a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php
index cb754e1afaa4..0c000d675c3c 100644
--- a/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php
+++ b/core/lib/Drupal/Core/Test/SimpletestTestRunResultsStorage.php
@@ -95,13 +95,14 @@ class SimpletestTestRunResultsStorage implements TestRunResultsStorageInterface
* {@inheritdoc}
*/
public function removeResults(TestRun $test_run): int {
- $this->connection->startTransaction('delete_test_run');
+ $transaction = $this->connection->startTransaction('delete_test_run');
$this->connection->delete('simpletest')
->condition('test_id', $test_run->id())
->execute();
$count = $this->connection->delete('simpletest_test_id')
->condition('test_id', $test_run->id())
->execute();
+ $transaction->commitOrRelease();
return $count;
}
@@ -169,9 +170,10 @@ class SimpletestTestRunResultsStorage implements TestRunResultsStorageInterface
*/
public function cleanUp(): int {
// Clear test results.
- $this->connection->startTransaction('delete_simpletest');
+ $transaction = $this->connection->startTransaction('delete_simpletest');
$this->connection->delete('simpletest')->execute();
$count = $this->connection->delete('simpletest_test_id')->execute();
+ $transaction->commitOrRelease();
return $count;
}
diff --git a/core/modules/block/migrations/d6_block.yml b/core/modules/block/migrations/d6_block.yml
index 74922444e8df..853ce28a47b8 100644
--- a/core/modules/block/migrations/d6_block.yml
+++ b/core/modules/block/migrations/d6_block.yml
@@ -56,8 +56,6 @@ process:
1: forum_new_block
locale:
0: language_block
- node:
- 0: node_syndicate_block
search:
0: search_form_block
statistics:
diff --git a/core/modules/block/migrations/d7_block.yml b/core/modules/block/migrations/d7_block.yml
index 9b031b7daa79..35c6f23d86f4 100644
--- a/core/modules/block/migrations/d7_block.yml
+++ b/core/modules/block/migrations/d7_block.yml
@@ -59,8 +59,6 @@ process:
new: forum_new_block
# locale:
# 0: language_block
- node:
- syndicate: node_syndicate_block
search:
form: search_form_block
statistics:
diff --git a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
index 8b2ead48edaf..6305ab7f8415 100644
--- a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
+++ b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
@@ -65,6 +65,10 @@ class BlockConfigSchemaTest extends KernelTestBase {
*/
public function testBlockConfigSchema(): void {
foreach ($this->blockManager->getDefinitions() as $block_id => $definition) {
+ // Skip the syndicate block as it is deprecated.
+ if ($block_id === 'node_syndicate_block') {
+ continue;
+ }
$id = $this->randomMachineName();
$block = Block::create([
'id' => $id,
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
index 3f20b2148b8d..dc96d95e6996 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
@@ -100,7 +100,7 @@ class MigrateBlockTest extends MigrateDrupal6TestBase {
*/
public function testBlockMigration(): void {
$blocks = Block::loadMultiple();
- $this->assertCount(25, $blocks);
+ $this->assertCount(24, $blocks);
// Check user blocks.
$visibility = [
diff --git a/core/modules/block_content/src/Controller/BlockContentController.php b/core/modules/block_content/src/Controller/BlockContentController.php
index b2776f51d7d8..77f8eee7939d 100644
--- a/core/modules/block_content/src/Controller/BlockContentController.php
+++ b/core/modules/block_content/src/Controller/BlockContentController.php
@@ -2,9 +2,9 @@
namespace Drupal\block_content\Controller;
+use Drupal\block_content\BlockContentTypeInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\block_content\BlockContentTypeInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -88,7 +88,8 @@ class BlockContentController extends ControllerBase {
uasort($types, [$this->blockContentTypeStorage->getEntityType()->getClass(), 'sort']);
if ($types && count($types) == 1) {
$type = reset($types);
- return $this->addForm($type, $request);
+ $query = $request->query->all();
+ return $this->redirect('block_content.add_form', ['block_content_type' => $type->id()], ['query' => $query]);
}
if (count($types) === 0) {
return [
diff --git a/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php b/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php
index 844e06895cc5..4e6c3b141e70 100644
--- a/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php
+++ b/core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php
@@ -5,7 +5,6 @@ namespace Drupal\block_content\Plugin\Menu\LocalAction;
use Drupal\Core\Menu\LocalActionDefault;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
-use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -54,11 +53,6 @@ class BlockContentAddLocalAction extends LocalActionDefault {
if ($region = $this->requestStack->getCurrentRequest()->query->getString('region')) {
$options['query']['region'] = $region;
}
-
- // Adds a destination on content block listing.
- if ($route_match->getRouteName() == 'entity.block_content.collection') {
- $options['query']['destination'] = Url::fromRoute('<current>')->toString();
- }
return $options;
}
diff --git a/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php b/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php
index bca42cd3e328..364b5f4524d6 100644
--- a/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php
+++ b/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php
@@ -155,11 +155,7 @@ class BlockContentCreationTest extends BlockContentTestBase {
// Create a block and place in block layout.
$this->drupalGet('/admin/content/block');
$this->clickLink('Add content block');
- // Verify destination URL, when clicking "Save and configure" this
- // destination will be ignored.
- $base = base_path();
- $url = 'block/add?destination=' . $base . 'admin/content/block';
- $this->assertSession()->addressEquals($url);
+ $this->assertSession()->addressEquals('/block/add/basic');
$edit = [];
$edit['info[0][value]'] = 'Test Block';
$edit['body[0][value]'] = $this->randomMachineName(16);
diff --git a/core/modules/block_content/tests/src/Functional/LocalActionTest.php b/core/modules/block_content/tests/src/Functional/LocalActionTest.php
new file mode 100644
index 000000000000..bb1a20df880c
--- /dev/null
+++ b/core/modules/block_content/tests/src/Functional/LocalActionTest.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\block_content\Functional;
+
+/**
+ * Tests block_content local action links.
+ *
+ * @group block_content
+ */
+class LocalActionTest extends BlockContentTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $defaultTheme = 'stark';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests the block_content_add_action link.
+ */
+ public function testAddContentBlockLink(): void {
+ // Verify that the link takes you straight to the block form if there's only
+ // one type.
+ $this->drupalGet('/admin/content/block');
+ $this->clickLink('Add content block');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->addressEquals('/block/add/basic');
+
+ $type = $this->randomMachineName();
+ $this->createBlockContentType([
+ 'id' => $type,
+ 'label' => $type,
+ ]);
+
+ // Verify that the link takes you to the block add page if there's more than
+ // one type.
+ $this->drupalGet('/admin/content/block');
+ $this->clickLink('Add content block');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->addressEquals('/block/add');
+ }
+
+}
diff --git a/core/modules/comment/src/Hook/CommentThemeHooks.php b/core/modules/comment/src/Hook/CommentThemeHooks.php
index e789af6dab10..c137d586d416 100644
--- a/core/modules/comment/src/Hook/CommentThemeHooks.php
+++ b/core/modules/comment/src/Hook/CommentThemeHooks.php
@@ -2,7 +2,7 @@
namespace Drupal\comment\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for comment.
@@ -12,7 +12,7 @@ class CommentThemeHooks {
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
- #[Preprocess('block')]
+ #[Hook('preprocess_block')]
public function preprocessBlock(&$variables): void {
if ($variables['configuration']['provider'] == 'comment') {
$variables['attributes']['role'] = 'navigation';
diff --git a/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php b/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php
index db1ffae5a6d0..01a40394b409 100644
--- a/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php
+++ b/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Drupal\comment_empty_title_test\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for comment_empty_title_test.
@@ -14,7 +14,7 @@ class CommentEmptyTitleTestThemeHooks {
/**
* Implements hook_preprocess_comment().
*/
- #[Preprocess('comment')]
+ #[Hook('preprocess_comment')]
public function preprocessComment(&$variables): void {
$variables['title'] = '';
}
diff --git a/core/modules/config_translation/migrations/d6_block_translation.yml b/core/modules/config_translation/migrations/d6_block_translation.yml
index 6d57fdae1be4..7925c49626f2 100644
--- a/core/modules/config_translation/migrations/d6_block_translation.yml
+++ b/core/modules/config_translation/migrations/d6_block_translation.yml
@@ -39,8 +39,6 @@ process:
1: forum_new_block
locale:
0: language_block
- node:
- 0: node_syndicate_block
search:
0: search_form_block
statistics:
diff --git a/core/modules/config_translation/migrations/d7_block_translation.yml b/core/modules/config_translation/migrations/d7_block_translation.yml
index 9c82ee6b6786..d2530e3b50a8 100644
--- a/core/modules/config_translation/migrations/d7_block_translation.yml
+++ b/core/modules/config_translation/migrations/d7_block_translation.yml
@@ -44,8 +44,6 @@ process:
new: forum_new_block
# locale:
# 0: language_block
- node:
- syndicate: node_syndicate_block
search:
form: search_form_block
statistics:
diff --git a/core/modules/contact/src/Hook/ContactFormHooks.php b/core/modules/contact/src/Hook/ContactFormHooks.php
index ad8223c3ec62..b31b929bddf2 100644
--- a/core/modules/contact/src/Hook/ContactFormHooks.php
+++ b/core/modules/contact/src/Hook/ContactFormHooks.php
@@ -4,7 +4,7 @@ namespace Drupal\contact\Hook;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Hook\Attribute\FormAlter;
+use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\user\UserDataInterface;
@@ -29,7 +29,7 @@ class ContactFormHooks {
*
* @see \Drupal\user\ProfileForm::form()
*/
- #[FormAlter('user_form')]
+ #[Hook('form_user_form_alter')]
public function formUserFormAlter(&$form, FormStateInterface $form_state) : void {
$form['contact'] = [
'#type' => 'details',
@@ -55,7 +55,7 @@ class ContactFormHooks {
*
* Adds the default personal contact setting on the user settings page.
*/
- #[FormAlter('user_admin_settings')]
+ #[Hook('form_user_admin_settings_alter')]
public function formUserAdminSettingsAlter(&$form, FormStateInterface $form_state) : void {
$form['contact'] = [
'#type' => 'details',
diff --git a/core/modules/contextual/src/Hook/ContextualThemeHooks.php b/core/modules/contextual/src/Hook/ContextualThemeHooks.php
index 760a42c97854..7d873196b431 100644
--- a/core/modules/contextual/src/Hook/ContextualThemeHooks.php
+++ b/core/modules/contextual/src/Hook/ContextualThemeHooks.php
@@ -2,7 +2,7 @@
namespace Drupal\contextual\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountInterface;
/**
@@ -21,7 +21,7 @@ class ContextualThemeHooks {
* @see contextual_page_attachments()
* @see \Drupal\contextual\ContextualController::render()
*/
- #[Preprocess]
+ #[Hook('preprocess')]
public function preprocess(&$variables, $hook, $info): void {
// Determine the primary theme function argument.
if (!empty($info['variables'])) {
diff --git a/core/modules/locale/src/Hook/LocaleThemeHooks.php b/core/modules/locale/src/Hook/LocaleThemeHooks.php
index d1e438f50ace..4ef5ca0b4989 100644
--- a/core/modules/locale/src/Hook/LocaleThemeHooks.php
+++ b/core/modules/locale/src/Hook/LocaleThemeHooks.php
@@ -2,7 +2,7 @@
namespace Drupal\locale\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
@@ -18,7 +18,7 @@ class LocaleThemeHooks {
/**
* Implements hook_preprocess_HOOK() for node templates.
*/
- #[Preprocess('node')]
+ #[Hook('preprocess_node')]
public function preprocessNode(&$variables): void {
/** @var \Drupal\node\NodeInterface $node */
$node = $variables['node'];
diff --git a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
index 3a1cb8a1b695..77c8b45d00f8 100644
--- a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
+++ b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
@@ -609,15 +609,15 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
* {@inheritdoc}
*/
public function preRollback(MigrateRollbackEvent $event) {
- // Nothing to do in this implementation.
+ // Reset the high-water mark.
+ $this->saveHighWater(NULL);
}
/**
* {@inheritdoc}
*/
public function postRollback(MigrateRollbackEvent $event) {
- // Reset the high-water mark.
- $this->saveHighWater(NULL);
+ // Nothing to do in this implementation.
}
/**
diff --git a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php
index 2f0b85ffbc47..e344e3e23e84 100644
--- a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php
+++ b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php
@@ -9,6 +9,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateSkipRowException;
@@ -448,6 +449,32 @@ class MigrateSourceTest extends MigrateTestCase {
return new MigrateExecutable($migration, $message, $event_dispatcher);
}
+ /**
+ * @covers ::preRollback
+ */
+ public function testPreRollback(): void {
+ $this->migrationConfiguration['id'] = 'test_migration';
+ $plugin_id = 'test_migration';
+ $migration = $this->getMigration();
+
+ // Verify that preRollback() sets the high water mark to NULL.
+ $key_value = $this->createMock(KeyValueStoreInterface::class);
+ $key_value->expects($this->once())
+ ->method('set')
+ ->with($plugin_id, NULL);
+ $key_value_factory = $this->createMock(KeyValueFactoryInterface::class);
+ $key_value_factory->expects($this->once())
+ ->method('get')
+ ->with('migrate:high_water')
+ ->willReturn($key_value);
+ $container = new ContainerBuilder();
+ $container->set('keyvalue', $key_value_factory);
+ \Drupal::setContainer($container);
+
+ $source = new StubSourceGeneratorPlugin([], $plugin_id, [], $migration);
+ $source->preRollback(new MigrateRollbackEvent($migration));
+ }
+
}
/**
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php
index 1f54f94848ec..efe2b1509282 100644
--- a/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php
@@ -18,6 +18,7 @@ use Drupal\field_discovery_test\FieldDiscoveryTestClass;
* Test FieldDiscovery Service against Drupal 7.
*
* @group migrate_drupal
+ * @group #slow
* @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
*/
class FieldDiscoveryTest extends MigrateDrupal7TestBase {
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php
index ca8a9a0d06b3..27ab60bc0c0c 100644
--- a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php
@@ -16,6 +16,7 @@ use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
* Tests the migration auditor for ID conflicts.
*
* @group migrate_drupal
+ * @group #slow
*/
class MigrateDrupal7AuditIdsTest extends MigrateDrupal7TestBase {
diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php
index 64dc7a1ea865..daf06a65468a 100644
--- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php
+++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php
@@ -73,7 +73,7 @@ class Upgrade6Test extends MigrateUpgradeExecuteTestBase {
*/
protected function getEntityCounts(): array {
return [
- 'block' => 37,
+ 'block' => 36,
'block_content' => 2,
'block_content_type' => 1,
'comment' => 8,
diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php
index 46b3447e159d..f9b702d22e3a 100644
--- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php
+++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php
@@ -76,7 +76,7 @@ class Upgrade7Test extends MigrateUpgradeExecuteTestBase {
*/
protected function getEntityCounts(): array {
return [
- 'block' => 27,
+ 'block' => 26,
'block_content' => 1,
'block_content_type' => 1,
'comment' => 4,
diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
index ae9ae566f8dd..5bf9d2477f09 100644
--- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
+++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
@@ -73,14 +73,14 @@ class PerformanceTest extends PerformanceTestBase {
$expected = [
'QueryCount' => 4,
- 'CacheGetCount' => 48,
+ 'CacheGetCount' => 47,
'CacheGetCountByBin' => [
'config' => 11,
'data' => 4,
'discovery' => 10,
'bootstrap' => 6,
'dynamic_page_cache' => 1,
- 'render' => 15,
+ 'render' => 14,
'menu' => 1,
],
'CacheSetCount' => 2,
@@ -89,7 +89,7 @@ class PerformanceTest extends PerformanceTestBase {
],
'CacheDeleteCount' => 0,
'CacheTagInvalidationCount' => 0,
- 'CacheTagLookupQueryCount' => 14,
+ 'CacheTagLookupQueryCount' => 13,
'ScriptCount' => 3,
'ScriptBytes' => 167569,
'StylesheetCount' => 2,
diff --git a/core/modules/node/src/Hook/NodeHooks.php b/core/modules/node/src/Hook/NodeHooks.php
index d5f84e0359ba..8a6b4d887c89 100644
--- a/core/modules/node/src/Hook/NodeHooks.php
+++ b/core/modules/node/src/Hook/NodeHooks.php
@@ -66,4 +66,13 @@ class NodeHooks {
}
}
+ /**
+ * Implements hook_block_alter().
+ */
+ #[Hook('block_alter')]
+ public function blockAlter(&$definitions): void {
+ // Hide the deprecated Syndicate block from the UI.
+ $definitions['node_syndicate_block']['_block_ui_hidden'] = TRUE;
+ }
+
}
diff --git a/core/modules/node/src/Hook/NodeThemeHooks.php b/core/modules/node/src/Hook/NodeThemeHooks.php
index 7ee443c458f8..7ed0ef91f5fd 100644
--- a/core/modules/node/src/Hook/NodeThemeHooks.php
+++ b/core/modules/node/src/Hook/NodeThemeHooks.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Drupal\node\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for the node module.
@@ -14,7 +14,7 @@ class NodeThemeHooks {
/**
* Implements hook_preprocess_HOOK() for node field templates.
*/
- #[Preprocess('field__node')]
+ #[Hook('preprocess_field__node')]
public function preprocessFieldNode(&$variables): void {
// Set a variable 'is_inline' in cases where inline markup is required,
// without any block elements such as <div>.
diff --git a/core/modules/node/src/Plugin/Block/SyndicateBlock.php b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
index b10c63527e5b..45cfe1eb45c6 100644
--- a/core/modules/node/src/Plugin/Block/SyndicateBlock.php
+++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
@@ -14,6 +14,11 @@ use Drupal\Core\Url;
/**
* Provides a 'Syndicate' block that links to the site's RSS feed.
+ *
+ * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no
+ * replacement.
+ *
+ * @see https://www.drupal.org/node/3519248
*/
#[Block(
id: "node_syndicate_block",
@@ -43,6 +48,7 @@ class SyndicateBlock extends BlockBase implements ContainerFactoryPluginInterfac
* The config factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $configFactory) {
+ @trigger_error('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248', E_USER_DEPRECATED);
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $configFactory;
}
diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php
new file mode 100644
index 000000000000..5a930df3e2df
--- /dev/null
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\node\Functional;
+
+use Drupal\Core\Url;
+
+/**
+ * Tests reverting node revisions correctly sets authorship information.
+ *
+ * @group node
+ */
+class NodeRevisionsAuthorTest extends NodeTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $defaultTheme = 'stark';
+
+ /**
+ * Tests node authorship is retained after reverting revisions.
+ */
+ public function testNodeRevisionRevertAuthors(): void {
+ // Create and log in user.
+ $initialUser = $this->drupalCreateUser([
+ 'view page revisions',
+ 'revert page revisions',
+ 'edit any page content',
+ ]);
+ $initialRevisionUser = $this->drupalCreateUser();
+ // Third user is an author only and needs no permissions
+ $initialRevisionAuthor = $this->drupalCreateUser();
+
+ // Create initial node (author: $user1).
+ $this->drupalLogin($initialUser);
+ $node = $this->drupalCreateNode();
+ $originalRevisionId = $node->getRevisionId();
+ $originalBody = $node->body->value;
+ $originalTitle = $node->getTitle();
+
+ // Create a revision (as $initialUser) showing $initialRevisionAuthor
+ // as author.
+ $node->setRevisionLogMessage('Changed author');
+ $revisedTitle = $this->randomMachineName();
+ $node->setTitle($revisedTitle);
+ $revisedBody = $this->randomMachineName(32);
+ $node->set('body', [
+ 'value' => $revisedBody,
+ 'format' => filter_default_format(),
+ ]);
+ $node->setOwnerId($initialRevisionAuthor->id());
+ $node->setRevisionUserId($initialRevisionUser->id());
+ $node->setNewRevision();
+ $node->save();
+ $revisedRevisionId = $node->getRevisionId();
+
+ $nodeStorage = \Drupal::entityTypeManager()->getStorage('node');
+
+ self::assertEquals($node->getOwnerId(), $initialRevisionAuthor->id());
+ self::assertEquals($node->getRevisionUserId(), $initialRevisionUser->id());
+
+ // Revert to the original node revision.
+ $this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [
+ 'node' => $node->id(),
+ 'node_revision' => $originalRevisionId,
+ ]));
+ $this->submitForm([], 'Revert');
+ $this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $originalTitle));
+
+ // With the revert done, reload the node and verify that the authorship
+ // fields have reverted correctly.
+ $nodeStorage->resetCache([$node->id()]);
+ /** @var \Drupal\node\NodeInterface $revertedNode */
+ $revertedNode = $nodeStorage->load($node->id());
+ self::assertEquals($originalBody, $revertedNode->body->value);
+ self::assertEquals($initialUser->id(), $revertedNode->getOwnerId());
+ self::assertEquals($initialUser->id(), $revertedNode->getRevisionUserId());
+
+ // Revert again to the revised version and check that node author and
+ // revision author fields are correct.
+ // Revert to the original node.
+ $this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [
+ 'node' => $revertedNode->id(),
+ 'node_revision' => $revisedRevisionId,
+ ]));
+ $this->submitForm([], 'Revert');
+ $this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $revisedTitle));
+
+ // With the reversion done, reload the node and verify that the
+ // authorship fields have reverted correctly.
+ $nodeStorage->resetCache([$revertedNode->id()]);
+ /** @var \Drupal\node\NodeInterface $re_reverted_node */
+ $re_reverted_node = $nodeStorage->load($revertedNode->id());
+ self::assertEquals($revisedBody, $re_reverted_node->body->value);
+ self::assertEquals($initialRevisionAuthor->id(), $re_reverted_node->getOwnerId());
+ // The new revision user will be the current logged in user as set in
+ // NodeRevisionRevertForm.
+ self::assertEquals($initialUser->id(), $re_reverted_node->getRevisionUserId());
+ }
+
+}
diff --git a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
index c3a3d46b4960..f8d52b06ecb3 100644
--- a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
+++ b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
@@ -8,6 +8,7 @@ namespace Drupal\Tests\node\Functional;
* Tests if the syndicate block is available.
*
* @group node
+ * @group legacy
*/
class NodeSyndicateBlockTest extends NodeTestBase {
@@ -40,6 +41,7 @@ class NodeSyndicateBlockTest extends NodeTestBase {
$this->drupalPlaceBlock('node_syndicate_block', ['id' => 'test_syndicate_block', 'label' => 'Subscribe to RSS Feed']);
$this->drupalGet('');
$this->assertSession()->elementExists('xpath', '//div[@id="block-test-syndicate-block"]/*');
+ $this->expectDeprecation('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248');
// Verify syndicate block title.
$this->assertSession()->pageTextContains('Subscribe to RSS Feed');
diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
index 2bb252f7c6e1..ac1e8664badf 100644
--- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
+++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
@@ -242,21 +242,19 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
// Set up the default admin theme and use it for node editing.
$this->container->get('theme_installer')->install(['claro']);
- $edit = [];
- $edit['admin_theme'] = 'claro';
- $edit['use_admin_theme'] = TRUE;
- $this->drupalGet('admin/appearance');
- $this->submitForm($edit, 'Save configuration');
- $this->drupalGet('node/' . $article->id() . '/translations');
+ $this->config('system.theme')->set('admin', 'claro')->save();
+
// Verify that translation uses the admin theme if edit is admin.
+ $this->drupalGet('node/' . $article->id() . '/translations');
$this->assertSession()->responseContains('core/themes/claro/css/base/elements.css');
// Turn off admin theme for editing, assert inheritance to translations.
- $edit['use_admin_theme'] = FALSE;
- $this->drupalGet('admin/appearance');
- $this->submitForm($edit, 'Save configuration');
- $this->drupalGet('node/' . $article->id() . '/translations');
+ $this->config('node.settings')->set('use_admin_theme', FALSE)->save();
+ // Changing node.settings:use_admin_theme requires a route rebuild.
+ $this->container->get('router.builder')->rebuild();
+
// Verify that translation uses the frontend theme if edit is frontend.
+ $this->drupalGet('node/' . $article->id() . '/translations');
$this->assertSession()->responseNotContains('core/themes/claro/css/base/elements.css');
// Assert presence of translation page itself (vs. DisabledBundle below).
@@ -561,12 +559,10 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
'translatable' => TRUE,
])->save();
- $this->drupalLogin($this->administrator);
// Make the image field a multi-value field in order to display a
// details form element.
- $edit = ['field_storage[subform][cardinality_number]' => 2];
- $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image');
- $this->submitForm($edit, 'Save');
+ $fieldStorage = FieldStorageConfig::loadByName('node', 'field_image');
+ $fieldStorage->setCardinality(2)->save();
// Enable the display of the image field.
EntityFormDisplay::load('node.article.default')
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
index cbe9b346623e..ac47588d5ec4 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
@@ -18,6 +18,7 @@ use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
* Test class for a complete node migration for Drupal 7.
*
* @group migrate_drupal_7
+ * @group #slow
*/
class MigrateNodeCompleteTest extends MigrateDrupal7TestBase {
diff --git a/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php
index 41f2e8b8e4ff..da6d22bfb055 100644
--- a/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/tests/src/Functional/PageCacheTagsIntegrationTest.php
@@ -155,7 +155,6 @@ class PageCacheTagsIntegrationTest extends BrowserTestBase {
'config:block.block.olivero_messages',
'config:block.block.olivero_primary_local_tasks',
'config:block.block.olivero_secondary_local_tasks',
- 'config:block.block.olivero_syndicate',
'config:block.block.olivero_primary_admin_actions',
'config:block.block.olivero_page_title',
'node_view',
@@ -195,7 +194,6 @@ class PageCacheTagsIntegrationTest extends BrowserTestBase {
'config:block.block.olivero_messages',
'config:block.block.olivero_primary_local_tasks',
'config:block.block.olivero_secondary_local_tasks',
- 'config:block.block.olivero_syndicate',
'config:block.block.olivero_primary_admin_actions',
'config:block.block.olivero_page_title',
'node_view',
diff --git a/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz
index 5d8c99744690..077d0645ddc7 100644
--- a/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz
+++ b/core/modules/system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz
Binary files differ
diff --git a/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz
index 423f49a1d409..5db0b3a5aaeb 100644
--- a/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz
+++ b/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz
Binary files differ
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php
index 542c4e162e2d..78328f9f8e4d 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestClickedButtonForm.php
@@ -35,32 +35,28 @@ class FormTestClickedButtonForm extends FormBase {
'#type' => 'textfield',
];
+ // Get button configurations, filter out NULL values.
+ $args = array_filter([$first, $second, $third]);
+
+ // Define button types for each argument.
+ $button_types = [
+ 's' => 'submit',
+ 'i' => 'image_button',
+ 'b' => 'button',
+ ];
+
// Loop through each path argument, adding buttons based on the information
// in the argument. For example, if the path is
// form-test/clicked-button/s/i/rb, then 3 buttons are added: a 'submit', an
// 'image_button', and a 'button' with #access=FALSE. This enables form.test
// to test a variety of combinations.
- $i = 0;
- $args = [$first, $second, $third];
- foreach ($args as $arg) {
- $name = 'button' . ++$i;
- // 's', 'b', or 'i' in the argument define the button type wanted.
- if (!is_string($arg)) {
- $type = NULL;
- }
- elseif (str_contains($arg, 's')) {
- $type = 'submit';
- }
- elseif (str_contains($arg, 'b')) {
- $type = 'button';
- }
- elseif (str_contains($arg, 'i')) {
- $type = 'image_button';
- }
- else {
- $type = NULL;
- }
- if (isset($type)) {
+ foreach ($args as $index => $arg) {
+ // Get the button type based on the index of the argument.
+ $type = $button_types[$arg] ?? NULL;
+ $name = 'button' . ($index + 1);
+
+ if ($type) {
+ // Define the button.
$form[$name] = [
'#type' => $type,
'#name' => $name,
diff --git a/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php b/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php
index 1cbb9e6b422b..db923382a21b 100644
--- a/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php
+++ b/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php
@@ -4,19 +4,19 @@ declare(strict_types=1);
namespace Drupal\module_test_oop_preprocess\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for module_test_oop_preprocess.
*/
class ModuleTestOopPreprocessThemeHooks {
- #[Preprocess]
+ #[Hook('preprocess')]
public function rootPreprocess($arg): mixed {
return $arg;
}
- #[Preprocess('test')]
+ #[Hook('preprocess_test')]
public function preprocessTest($arg): mixed {
return $arg;
}
diff --git a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php
index fc48756de51a..7bfc10ef0ef9 100644
--- a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php
+++ b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Drupal\theme_test\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for theme_test.
@@ -14,7 +14,7 @@ class ThemeTestThemeHooks {
/**
* Implements hook_preprocess_HOOK().
*/
- #[Preprocess('theme_test_preprocess_suggestions__monkey')]
+ #[Hook('preprocess_theme_test_preprocess_suggestions__monkey')]
public function preprocessTestSuggestions(&$variables): void {
$variables['foo'] = 'Monkey';
}
diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php
index 8d9465f61a3f..7fcb764eac3e 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTranslationTest.php
@@ -11,6 +11,7 @@ use Drupal\node\Entity\Node;
* Upgrade taxonomy term node associations.
*
* @group migrate_drupal_6
+ * @group #slow
*/
class MigrateTermNodeTranslationTest extends MigrateDrupal6TestBase {
diff --git a/core/modules/views/tests/src/Functional/GlossaryTest.php b/core/modules/views/tests/src/Functional/GlossaryTest.php
index 292f91767715..25c08d5f1590 100644
--- a/core/modules/views/tests/src/Functional/GlossaryTest.php
+++ b/core/modules/views/tests/src/Functional/GlossaryTest.php
@@ -83,7 +83,6 @@ class GlossaryTest extends ViewTestBase {
'url',
'user.node_grants:view',
'user.permissions',
- 'route',
],
[
'config:views.view.glossary',
diff --git a/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php b/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php
index 03488125064a..e19f1414615f 100644
--- a/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php
+++ b/core/modules/views/tests/src/Kernel/Handler/ArgumentSummaryTest.php
@@ -150,4 +150,38 @@ class ArgumentSummaryTest extends ViewsKernelTestBase {
$this->assertStringContainsString($tags[1]->label() . ' (2)', $output);
}
+ /**
+ * Tests that the active link is set correctly.
+ */
+ public function testActiveLink(): void {
+ require_once $this->root . '/core/modules/views/views.theme.inc';
+
+ // We need at least one node.
+ Node::create([
+ 'type' => $this->nodeType->id(),
+ 'title' => $this->randomMachineName(),
+ ])->save();
+
+ $view = Views::getView('test_argument_summary');
+ $view->execute();
+ $view->build();
+ $variables = [
+ 'view' => $view,
+ 'rows' => $view->result,
+ ];
+
+ template_preprocess_views_view_summary_unformatted($variables);
+ $this->assertFalse($variables['rows'][0]->active);
+
+ template_preprocess_views_view_summary($variables);
+ $this->assertFalse($variables['rows'][0]->active);
+
+ // Checks that the row with the current path is active.
+ \Drupal::service('path.current')->setPath('/test-argument-summary');
+ template_preprocess_views_view_summary_unformatted($variables);
+ $this->assertTrue($variables['rows'][0]->active);
+ template_preprocess_views_view_summary($variables);
+ $this->assertTrue($variables['rows'][0]->active);
+ }
+
}
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index 10c29c5dbf30..04c5de5a535f 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -253,15 +253,12 @@ function template_preprocess_views_view_summary(&$variables): void {
$url_options['query'] = $view->exposed_raw_input;
}
+ $currentPath = \Drupal::service('path.current')->getPath();
$active_urls = [
// Force system path.
- Url::fromRoute('<current>', [], ['alias' => TRUE])->toString(),
- // Force system path.
- Url::fromRouteMatch(\Drupal::routeMatch())->setOption('alias', TRUE)->toString(),
- // Could be an alias.
- Url::fromRoute('<current>')->toString(),
+ Url::fromUserInput($currentPath, ['alias' => TRUE])->toString(),
// Could be an alias.
- Url::fromRouteMatch(\Drupal::routeMatch())->toString(),
+ Url::fromUserInput($currentPath)->toString(),
];
$active_urls = array_combine($active_urls, $active_urls);
@@ -342,11 +339,12 @@ function template_preprocess_views_view_summary_unformatted(&$variables): void {
}
$count = 0;
+ $currentPath = \Drupal::service('path.current')->getPath();
$active_urls = [
// Force system path.
- Url::fromRoute('<current>', [], ['alias' => TRUE])->toString(),
+ Url::fromUserInput($currentPath, ['alias' => TRUE])->toString(),
// Could be an alias.
- Url::fromRoute('<current>')->toString(),
+ Url::fromUserInput($currentPath)->toString(),
];
$active_urls = array_combine($active_urls, $active_urls);
diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
index 856c5fb25b20..f3a3196fab6e 100644
--- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
+++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
@@ -90,6 +90,7 @@ class StandardPerformanceTest extends PerformanceTestBase {
$expected_queries = [
'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/node" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC',
'SELECT "name", "route", "fit" FROM "router" WHERE "pattern_outline" IN ( "/node" ) AND "number_parts" >= 1',
+ 'SELECT 1 AS "expression" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."path" LIKE "/rss.xml%" ESCAPE ' . "'\\\\'" . ') LIMIT 1 OFFSET 0',
'SELECT COUNT(*) AS "expression" FROM (SELECT 1 AS "expression" FROM "node_field_data" "node_field_data" WHERE ("node_field_data"."promote" = 1) AND ("node_field_data"."status" = 1)) "subquery"',
'SELECT "node_field_data"."sticky" AS "node_field_data_sticky", "node_field_data"."created" AS "node_field_data_created", "node_field_data"."nid" AS "nid" FROM "node_field_data" "node_field_data" WHERE ("node_field_data"."promote" = 1) AND ("node_field_data"."status" = 1) ORDER BY "node_field_data_sticky" DESC, "node_field_data_created" DESC LIMIT 10 OFFSET 0',
'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."vid" = "base"."vid" WHERE "base"."nid" IN (1)',
@@ -133,8 +134,8 @@ class StandardPerformanceTest extends PerformanceTestBase {
$recorded_queries = $performance_data->getQueries();
$this->assertSame($expected_queries, $recorded_queries);
$expected = [
- 'QueryCount' => 41,
- 'CacheGetCount' => 101,
+ 'QueryCount' => 42,
+ 'CacheGetCount' => 100,
'CacheGetCountByBin' => [
'page' => 1,
'config' => 21,
@@ -142,7 +143,7 @@ class StandardPerformanceTest extends PerformanceTestBase {
'discovery' => 38,
'bootstrap' => 8,
'dynamic_page_cache' => 1,
- 'render' => 14,
+ 'render' => 13,
'default' => 5,
'entity' => 2,
'menu' => 3,
@@ -150,7 +151,7 @@ class StandardPerformanceTest extends PerformanceTestBase {
'CacheSetCount' => 47,
'CacheDeleteCount' => 0,
'CacheTagInvalidationCount' => 0,
- 'CacheTagLookupQueryCount' => 17,
+ 'CacheTagLookupQueryCount' => 16,
'CacheTagGroupedLookups' => [
[
'route_match',
@@ -182,7 +183,6 @@ class StandardPerformanceTest extends PerformanceTestBase {
['config:block.block.stark_messages'],
['config:block.block.stark_help'],
['config:block.block.stark_powered'],
- ['config:block.block.stark_syndicate'],
[
'config:block.block.stark_account_menu',
'config:block.block.stark_breadcrumbs',
@@ -234,11 +234,11 @@ class StandardPerformanceTest extends PerformanceTestBase {
$this->assertSame($expected_queries, $recorded_queries);
$expected = [
'QueryCount' => 10,
- 'CacheGetCount' => 72,
+ 'CacheGetCount' => 71,
'CacheSetCount' => 16,
'CacheDeleteCount' => 0,
'CacheTagInvalidationCount' => 0,
- 'CacheTagLookupQueryCount' => 14,
+ 'CacheTagLookupQueryCount' => 13,
'CacheTagGroupedLookups' => [
[
'route_match',
@@ -267,7 +267,6 @@ class StandardPerformanceTest extends PerformanceTestBase {
['config:block.block.stark_messages'],
['config:block.block.stark_help'],
['config:block.block.stark_powered'],
- ['config:block.block.stark_syndicate'],
[
'config:block.block.stark_account_menu',
'config:block.block.stark_breadcrumbs',
@@ -316,11 +315,11 @@ class StandardPerformanceTest extends PerformanceTestBase {
$this->assertSame($expected_queries, $recorded_queries);
$expected = [
'QueryCount' => 14,
- 'CacheGetCount' => 57,
+ 'CacheGetCount' => 56,
'CacheSetCount' => 17,
'CacheDeleteCount' => 0,
'CacheTagInvalidationCount' => 0,
- 'CacheTagLookupQueryCount' => 13,
+ 'CacheTagLookupQueryCount' => 12,
'StylesheetCount' => 1,
'StylesheetBytes' => 1800,
];
@@ -375,11 +374,11 @@ class StandardPerformanceTest extends PerformanceTestBase {
'StylesheetBytes' => 1429,
'StylesheetCount' => 1,
'QueryCount' => 17,
- 'CacheGetCount' => 69,
+ 'CacheGetCount' => 68,
'CacheSetCount' => 1,
'CacheDeleteCount' => 1,
'CacheTagInvalidationCount' => 0,
- 'CacheTagLookupQueryCount' => 14,
+ 'CacheTagLookupQueryCount' => 13,
'CacheTagGroupedLookups' => [
// Form submission and login.
[
@@ -421,7 +420,6 @@ class StandardPerformanceTest extends PerformanceTestBase {
['config:block.block.stark_messages'],
['config:block.block.stark_help'],
['config:block.block.stark_powered'],
- ['config:block.block.stark_syndicate'],
['config:block.block.stark_main_menu'],
[
'config:block.block.stark_account_menu',
diff --git a/core/recipes/standard/recipe.yml b/core/recipes/standard/recipe.yml
index 4a88f8a9b695..12e764f0ff5d 100644
--- a/core/recipes/standard/recipe.yml
+++ b/core/recipes/standard/recipe.yml
@@ -59,7 +59,6 @@ config:
- block.block.olivero_help
- block.block.olivero_search_form_narrow
- block.block.olivero_search_form_wide
- - block.block.olivero_syndicate
user:
- core.entity_view_mode.user.compact
- search.page.user_search
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
index 3bd0fe48af73..d33d7c4942ab 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
@@ -63,25 +63,20 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
// Assert that in the settings.php the database connection array has the
// correct values set.
- $contents = file_get_contents($this->container->getParameter('app.root') . '/' . $this->siteDirectory . '/settings.php');
- $this->assertStringContainsString("'namespace' => 'Drupal\\\\driver_test\\\\Driver\\\\Database\\\\{$this->testDriverName}',", $contents);
- $this->assertStringContainsString("'driver' => '{$this->testDriverName}',", $contents);
- $this->assertStringContainsString("'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/',", $contents);
-
- $dependencies = "'dependencies' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'mysql' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'namespace' => 'Drupal\\\\mysql'," . PHP_EOL .
- " 'autoload' => 'core/modules/mysql/src/'," . PHP_EOL .
- " )," . PHP_EOL .
- " 'pgsql' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'namespace' => 'Drupal\\\\pgsql'," . PHP_EOL .
- " 'autoload' => 'core/modules/pgsql/src/'," . PHP_EOL .
- " )," . PHP_EOL .
- " )," . PHP_EOL;
- $this->assertStringContainsString($dependencies, $contents);
+ $installedDatabaseSettings = $this->getInstalledDatabaseSettings();
+ $this->assertSame("Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}", $installedDatabaseSettings['default']['default']['namespace']);
+ $this->assertSame($this->testDriverName, $installedDatabaseSettings['default']['default']['driver']);
+ $this->assertSame("core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/", $installedDatabaseSettings['default']['default']['autoload']);
+ $this->assertEquals([
+ 'mysql' => [
+ 'namespace' => 'Drupal\\mysql',
+ 'autoload' => 'core/modules/mysql/src/',
+ ],
+ 'pgsql' => [
+ 'namespace' => 'Drupal\\pgsql',
+ 'autoload' => 'core/modules/pgsql/src/',
+ ],
+ ], $installedDatabaseSettings['default']['default']['dependencies']);
// Assert that the module "driver_test" and its dependencies have been
// installed.
@@ -99,4 +94,22 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
$this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test");
}
+ /**
+ * Returns the databases setup from the SUT's settings.php.
+ *
+ * @return array<string,mixed>
+ * The value of the $databases variable.
+ */
+ protected function getInstalledDatabaseSettings(): array {
+ // The $app_root and $site_path variables are required by the settings.php
+ // file to be parsed correctly. The $databases variable is set in the
+ // included file, we need to inform PHPStan about that since PHPStan itself
+ // is unable to determine it.
+ $app_root = $this->container->getParameter('app.root');
+ $site_path = $this->siteDirectory;
+ include $app_root . '/' . $site_path . '/settings.php';
+ assert(isset($databases));
+ return $databases;
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
index c361c7af9596..cbda6e3d7f72 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
@@ -432,9 +432,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
/**
* Tests rollback after a DDL statement when no transactional DDL supported.
- *
- * @todo In drupal:12.0.0, rollBack will throw a
- * TransactionOutOfOrderException. Adjust the test accordingly.
*/
public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
if ($this->connection->supportsTransactionalDDL()) {
@@ -919,9 +916,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* transaction including DDL statements is not possible, since a commit
* happened already. We cannot decide what should be the status of the
* callback, an exception is thrown.
- *
- * @todo In drupal:12.0.0, rollBack will throw a
- * TransactionOutOfOrderException. Adjust the test accordingly.
*/
public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
if ($this->connection->supportsTransactionalDDL()) {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
new file mode 100644
index 000000000000..f40a977f6f49
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
@@ -0,0 +1,1278 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Transaction;
+use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
+use Drupal\Core\Database\Transaction\StackItem;
+use Drupal\Core\Database\Transaction\StackItemType;
+use Drupal\Core\Database\Transaction\TransactionManagerBase;
+use Drupal\Core\Database\TransactionNameNonUniqueException;
+use Drupal\Core\Database\TransactionOutOfOrderException;
+
+// cspell:ignore Tinky Winky Dipsy
+
+/**
+ * Tests the transactions, using the explicit ::commitOrRelease method.
+ *
+ * We test nesting by having two transaction layers, an outer and inner. The
+ * outer layer encapsulates the inner layer. Our transaction nesting abstraction
+ * should allow the outer layer function to call any function it wants,
+ * especially the inner layer that starts its own transaction, and be
+ * confident that, when the function it calls returns, its own transaction
+ * is still "alive."
+ *
+ * Call structure:
+ * transactionOuterLayer()
+ * Start transaction "A"
+ * transactionInnerLayer()
+ * Start transaction "B" (does nothing in database)
+ * [Maybe decide to roll back "B"]
+ * Do more stuff
+ * Should still be in transaction "A"
+ *
+ * These method can be overridden by non-core database driver if their
+ * transaction behavior is different from core. For example, both oci8 (Oracle)
+ * and mysqli (MySql) clients do not have a solution to check if a transaction
+ * is active, and mysqli does not fail when rolling back and no transaction
+ * active.
+ *
+ * @group Database
+ */
+class TransactionTest extends DatabaseTestBase {
+
+ /**
+ * Keeps track of the post-transaction callback action executed.
+ */
+ protected ?string $postTransactionCallbackAction = NULL;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Set the transaction manager to trigger warnings when appropriate.
+ $this->connection->transactionManager()->triggerWarningWhenUnpilingOnVoidTransaction = TRUE;
+ }
+
+ /**
+ * Create a root Drupal transaction.
+ */
+ protected function createRootTransaction(string $name = '', bool $insertRow = TRUE): Transaction {
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+
+ // Start root transaction. Corresponds to 'BEGIN TRANSACTION' on the
+ // database.
+ $transaction = $this->connection->startTransaction($name);
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a single row into the testing table.
+ if ($insertRow) {
+ $this->insertRow('David');
+ $this->assertRowPresent('David');
+ }
+
+ return $transaction;
+ }
+
+ /**
+ * Create a Drupal savepoint transaction after root.
+ */
+ protected function createFirstSavepointTransaction(string $name = '', bool $insertRow = TRUE): Transaction {
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_1'
+ // on the database. The name can be changed by the $name argument.
+ $savepoint = $this->connection->startTransaction($name);
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a single row into the testing table.
+ if ($insertRow) {
+ $this->insertRow('Roger');
+ $this->assertRowPresent('Roger');
+ }
+
+ return $savepoint;
+ }
+
+ /**
+ * Encapsulates a transaction's "inner layer" with an "outer layer".
+ *
+ * This "outer layer" transaction starts and then encapsulates the "inner
+ * layer" transaction. This nesting is used to evaluate whether the database
+ * transaction API properly supports nesting. By "properly supports," we mean
+ * the outer transaction continues to exist regardless of what functions are
+ * called and whether those functions start their own transactions.
+ *
+ * In contrast, a typical database would commit the outer transaction, start
+ * a new transaction for the inner layer, commit the inner layer transaction,
+ * and then be confused when the outer layer transaction tries to commit its
+ * transaction (which was already committed when the inner transaction
+ * started).
+ *
+ * @param string $suffix
+ * Suffix to add to field values to differentiate tests.
+ */
+ protected function transactionOuterLayer(string $suffix): void {
+ $txn = $this->connection->startTransaction();
+
+ // Insert a single row into the testing table.
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David' . $suffix,
+ 'age' => '24',
+ ])
+ ->execute();
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction before calling nested transaction.');
+
+ // We're already in a transaction, but we call ->transactionInnerLayer
+ // to nest another transaction inside the current one.
+ $this->transactionInnerLayer($suffix);
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction after calling nested transaction.');
+
+ $txn->commitOrRelease();
+ }
+
+ /**
+ * Creates an "inner layer" transaction.
+ *
+ * This "inner layer" transaction is either used alone or nested inside of the
+ * "outer layer" transaction.
+ *
+ * @param string $suffix
+ * Suffix to add to field values to differentiate tests.
+ */
+ protected function transactionInnerLayer(string $suffix): void {
+ $depth = $this->connection->transactionManager()->stackDepth();
+ // Start a transaction. If we're being called from ->transactionOuterLayer,
+ // then we're already in a transaction. Normally, that would make starting
+ // a transaction here dangerous, but the database API handles this problem
+ // for us by tracking the nesting and avoiding the danger.
+ $txn = $this->connection->startTransaction();
+
+ $depth2 = $this->connection->transactionManager()->stackDepth();
+ $this->assertSame($depth + 1, $depth2, 'Transaction depth has increased with new transaction.');
+
+ // Insert a single row into the testing table.
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'Daniel' . $suffix,
+ 'age' => '19',
+ ])
+ ->execute();
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction inside nested transaction.');
+
+ $txn->commitOrRelease();
+ }
+
+ /**
+ * Tests root transaction rollback.
+ */
+ public function testRollbackRoot(): void {
+ $transaction = $this->createRootTransaction();
+
+ // Rollback. Since we are at the root, the transaction is closed.
+ // Corresponds to 'ROLLBACK' on the database.
+ $transaction->rollBack();
+ $this->assertRowAbsent('David');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests root transaction rollback after savepoint rollback.
+ */
+ public function testRollbackRootAfterSavepointRollback(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Try to rollback root. No savepoint is active, this should succeed.
+ $transaction->rollBack();
+ $this->assertRowAbsent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests root transaction rollback failure when savepoint is open.
+ */
+ public function testRollbackRootWithActiveSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Try to rollback root. Since a savepoint is active, this should fail.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\drupal_transaction\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1/");
+ $transaction->rollBack();
+ }
+
+ /**
+ * Tests savepoint transaction rollback.
+ */
+ public function testRollbackSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction commit after rollback.
+ */
+ public function testCommitAfterRollbackSameSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Try releasing savepoint. Should fail since it was released already.
+ try {
+ $savepoint->commitOrRelease();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting commit of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction rollback after commit.
+ */
+ public function testRollbackAfterCommitSameSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Release savepoint. Corresponds to 'RELEASE savepoint_1' on the database.
+ $savepoint->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Try rolling back savepoint. Should fail since it was released already.
+ try {
+ $savepoint->rollback();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction duplicated rollback.
+ */
+ public function testRollbackTwiceSameSavepoint(): void {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Rollback savepoint again. Should fail since it was released already.
+ try {
+ $savepoint->rollBack();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction rollback failure when later savepoints exist.
+ */
+ public function testRollbackSavepointWithLaterSavepoint(): void {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $transaction = $this->createRootTransaction();
+ $savepoint1 = $this->createFirstSavepointTransaction();
+
+ // Starts another savepoint transaction. Corresponds to 'SAVEPOINT
+ // savepoint_2' on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+
+ // Try to rollback to savepoint 1. Out of order.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1 > .*\\\\savepoint_2/");
+ $savepoint1->rollBack();
+ }
+
+ /**
+ * Tests commit does not fail when committing after DDL.
+ *
+ * In core, SQLite and PostgreSql databases support transactional DDL, MySql
+ * does not.
+ */
+ public function testCommitAfterDdl(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ $this->executeDDLStatement();
+
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+ }
+ else {
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $savepoint->commitOrRelease();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $savepoint->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ /**
+ * Tests a committed transaction.
+ *
+ * The behavior of this test should be identical for connections that support
+ * transactions and those that do not.
+ */
+ public function testCommittedTransaction(): void {
+ // Create two nested transactions. The changes should be committed.
+ $this->transactionOuterLayer('A');
+
+ // Because we committed, both of the inserted rows should be present.
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DavidA'])->fetchField();
+ $this->assertSame('24', $saved_age, 'Can retrieve DavidA row after commit.');
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DanielA'])->fetchField();
+ $this->assertSame('19', $saved_age, 'Can retrieve DanielA row after commit.');
+ }
+
+ /**
+ * Tests the compatibility of transactions with DDL statements.
+ */
+ public function testTransactionWithDdlStatement(): void {
+ // First, test that a commit works normally, even with DDL statements.
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // Even in different order.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->executeDDLStatement();
+ $this->insertRow('row');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // Even with stacking.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction2->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction2->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->commitOrRelease();
+
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, this is out or order since $transaction3 started a
+ // new root.
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // A transaction after a DDL statement should still work the same.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction2->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction2->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->rollBack();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, this is out or order since $transaction3 started a
+ // new root.
+ }
+ }
+ $this->assertRowAbsent('row');
+
+ // The behavior of a rollback depends on the type of database server.
+ if ($this->connection->supportsTransactionalDDL()) {
+ // For database servers that support transactional DDL, a rollback
+ // of a transaction including DDL statements should be possible.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ $transaction->rollBack();
+ $this->assertRowAbsent('row');
+
+ // Including with stacking.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ $transaction2->commitOrRelease();
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->commitOrRelease();
+ $this->assertRowPresent('row');
+ $transaction->rollBack();
+ $this->assertRowAbsent('row');
+ }
+ }
+
+ /**
+ * Tests rollback after a DDL statement when no transactional DDL supported.
+ */
+ public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database that do not support transactional DDL.');
+ }
+
+ // For database servers that do not support transactional DDL,
+ // the DDL statement should commit the transaction stack.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $reflectionMethod = new \ReflectionMethod(get_class($this->connection->transactionManager()), 'getConnectionTransactionState');
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ $this->assertEquals(ClientConnectionTransactionState::Active, $reflectionMethod->invoke($this->connection->transactionManager()));
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ $this->assertEquals(ClientConnectionTransactionState::Voided, $reflectionMethod->invoke($this->connection->transactionManager()));
+
+ // Try to rollback the root transaction. Since the DDL already committed
+ // it, it should fail.
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->rollBack();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, the attempted rollback made the overall state to
+ // ClientConnectionTransactionState::RollbackFailed.
+ }
+
+ $manager = $this->connection->transactionManager();
+ $this->assertSame(0, $manager->stackDepth());
+ $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState');
+ $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager));
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Inserts a single row into the testing table.
+ */
+ protected function insertRow(string $name): void {
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => $name,
+ ])
+ ->execute();
+ }
+
+ /**
+ * Executes a DDL statement.
+ */
+ protected function executeDDLStatement(): void {
+ static $count = 0;
+ $table = [
+ 'fields' => [
+ 'id' => [
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ ],
+ 'primary key' => ['id'],
+ ];
+ $this->connection->schema()->createTable('database_test_' . ++$count, $table);
+ }
+
+ /**
+ * Starts over for a new test.
+ */
+ protected function cleanUp(): void {
+ $this->connection->truncate('test')
+ ->execute();
+ $this->postTransactionCallbackAction = NULL;
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Asserts that a given row is present in the test table.
+ *
+ * @param string $name
+ * The name of the row.
+ * @param string $message
+ * The message to log for the assertion.
+ *
+ * @internal
+ */
+ public function assertRowPresent(string $name, ?string $message = NULL): void {
+ $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
+ $this->assertTrue($present, $message ?? "Row '{$name}' should be present, but it actually does not exist.");
+ }
+
+ /**
+ * Asserts that a given row is absent from the test table.
+ *
+ * @param string $name
+ * The name of the row.
+ * @param string $message
+ * The message to log for the assertion.
+ *
+ * @internal
+ */
+ public function assertRowAbsent(string $name, ?string $message = NULL): void {
+ $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
+ $this->assertFalse($present, $message ?? "Row '{$name}' should be absent, but it actually exists.");
+ }
+
+ /**
+ * Tests transaction stacking, commit, and rollback.
+ */
+ public function testTransactionStacking(): void {
+ // Standard case: pop the inner transaction before the outer transaction.
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('outer');
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->insertRow('inner');
+ // Pop the inner transaction.
+ $transaction2->commitOrRelease();
+ $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the inner transaction');
+ // Pop the outer transaction.
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the outer transaction');
+ $this->assertRowPresent('outer');
+ $this->assertRowPresent('inner');
+
+ // Rollback the inner transaction.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('outer');
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->insertRow('inner');
+ // Now rollback the inner transaction.
+ $transaction2->rollBack();
+ $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the outer transaction');
+ // Pop the outer transaction, it should commit.
+ $this->insertRow('outer-after-inner-rollback');
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the inner transaction');
+ $this->assertRowPresent('outer');
+ $this->assertRowAbsent('inner');
+ $this->assertRowPresent('outer-after-inner-rollback');
+ }
+
+ /**
+ * Tests that transactions can continue to be used if a query fails.
+ */
+ public function testQueryFailureInTransaction(): void {
+ $transaction = $this->createRootTransaction('test_transaction', FALSE);
+ $this->connection->schema()->dropTable('test');
+
+ // Test a failed query using the query() method.
+ try {
+ $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField();
+ $this->fail('Using the query method should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed select query.
+ try {
+ $this->connection->select('test')
+ ->fields('test', ['name'])
+ ->execute();
+
+ $this->fail('Select query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed insert query.
+ try {
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David',
+ 'age' => '24',
+ ])
+ ->execute();
+
+ $this->fail('Insert query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed update query.
+ try {
+ $this->connection->update('test')
+ ->fields(['name' => 'Tiffany'])
+ ->condition('id', 1)
+ ->execute();
+
+ $this->fail('Update query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed delete query.
+ try {
+ $this->connection->delete('test')
+ ->condition('id', 1)
+ ->execute();
+
+ $this->fail('Delete query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed merge query.
+ try {
+ $this->connection->merge('test')
+ ->key('job', 'Presenter')
+ ->fields([
+ 'age' => '31',
+ 'name' => 'Tiffany',
+ ])
+ ->execute();
+
+ $this->fail('Merge query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed upsert query.
+ try {
+ $this->connection->upsert('test')
+ ->key('job')
+ ->fields(['job', 'age', 'name'])
+ ->values([
+ 'job' => 'Presenter',
+ 'age' => 31,
+ 'name' => 'Tiffany',
+ ])
+ ->execute();
+
+ $this->fail('Upsert query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Create the missing schema and insert a row.
+ $this->installSchema('database_test', ['test']);
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David',
+ 'age' => '24',
+ ])
+ ->execute();
+
+ // Commit the transaction.
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField();
+ $this->assertEquals('24', $saved_age);
+ }
+
+ /**
+ * Tests releasing a savepoint before last is safe.
+ */
+ public function testReleaseIntermediateSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2'
+ // on the database.
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_3'
+ // on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint3 = $this->connection->startTransaction();
+ $this->assertSame(4, $this->connection->transactionManager()->stackDepth());
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_4'
+ // on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint4 = $this->connection->startTransaction();
+ $this->assertSame(5, $this->connection->transactionManager()->stackDepth());
+
+ $this->insertRow('row');
+
+ // Release savepoint transaction. Corresponds to 'RELEASE SAVEPOINT
+ // savepoint_2' on the database.
+ $savepoint2->commitOrRelease();
+ // Since we have committed an intermediate savepoint Transaction object,
+ // the savepoints created later have been dropped by the database already.
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+ $this->assertRowPresent('row');
+
+ // Commit the remaining Transaction objects. The client transaction is
+ // eventually committed.
+ $savepoint1->commitOrRelease();
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Tests committing a transaction while savepoints are active.
+ */
+ public function testCommitWithActiveSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2'
+ // on the database.
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+
+ $this->insertRow('row');
+
+ // Commit the root transaction.
+ $transaction->commitOrRelease();
+ // Since we have committed the outer (root) Transaction object, the inner
+ // (savepoint) ones have been dropped by the database already, and we are
+ // no longer in an active transaction state.
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertRowPresent('row');
+ // Trying to release the inner (savepoint) Transaction object, throws an
+ // exception since it was dropped by the database already, and removed from
+ // our transaction stack.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting commit of .*\\\\savepoint_2\\. Active stack: .* empty/");
+ $savepoint2->commitOrRelease();
+ }
+
+ /**
+ * Tests for transaction names.
+ */
+ public function testTransactionName(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->assertSame('drupal_transaction', $transaction->name());
+
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->assertSame('savepoint_1', $savepoint1->name());
+
+ $this->expectException(TransactionNameNonUniqueException::class);
+ $this->expectExceptionMessage("savepoint_1 is already in use.");
+ $this->connection->startTransaction('savepoint_1');
+ }
+
+ /**
+ * Tests for arbitrary transaction names.
+ */
+ public function testArbitraryTransactionNames(): void {
+ $transaction = $this->createRootTransaction('TinkyWinky', FALSE);
+ // Despite setting a name, the root transaction is always named
+ // 'drupal_transaction'.
+ $this->assertSame('drupal_transaction', $transaction->name());
+
+ $savepoint1 = $this->createFirstSavepointTransaction('Dipsy', FALSE);
+ $this->assertSame('Dipsy', $savepoint1->name());
+
+ $this->expectException(TransactionNameNonUniqueException::class);
+ $this->expectExceptionMessage("Dipsy is already in use.");
+ $this->connection->startTransaction('Dipsy');
+ }
+
+ /**
+ * Tests that adding a post-transaction callback fails with no transaction.
+ */
+ public function testRootTransactionEndCallbackAddedWithoutTransaction(): void {
+ $this->expectException(\LogicException::class);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ }
+
+ /**
+ * Tests post-transaction callback executes after transaction commit.
+ */
+ public function testRootTransactionEndCallbackCalledOnCommit(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a commit is not sufficient by itself.
+ $transaction->commitOrRelease();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowPresent('row');
+ $this->assertRowAbsent('rtcCommit');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcCommit'
+ // row.
+ $this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
+ $this->assertRowPresent('row');
+ $this->assertRowPresent('rtcCommit');
+ }
+
+ /**
+ * Tests post-transaction callback executes after transaction rollback.
+ */
+ public function testRootTransactionEndCallbackCalledAfterRollbackAndDestruction(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a rollback is not sufficient by itself.
+ $transaction->rollBack();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowAbsent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $this->assertRowAbsent('row');
+ }
+
+ /**
+ * Tests post-transaction callback executes after a DDL statement.
+ */
+ public function testRootTransactionEndCallbackCalledAfterDdlAndDestruction(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcCommit'
+ // row.
+ $this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
+ $this->assertRowPresent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Tests post-transaction rollback executes after a DDL statement.
+ *
+ * For database servers that support transactional DDL, a rollback of a
+ * transaction including DDL statements is possible.
+ */
+ public function testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabase(): void {
+ if (!$this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database supporting transactional DDL.');
+ }
+
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing the rollback is not sufficient by itself.
+ $transaction->rollBack();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowAbsent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $this->assertRowAbsent('row');
+ }
+
+ /**
+ * Tests post-transaction rollback failure after a DDL statement.
+ *
+ * For database servers that support transactional DDL, a rollback of a
+ * transaction including DDL statements is not possible, since a commit
+ * happened already. We cannot decide what should be the status of the
+ * callback, an exception is thrown.
+ */
+ public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database that do not support transactional DDL.');
+ }
+
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->rollBack();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $manager = $this->connection->transactionManager();
+ $this->assertSame(0, $manager->stackDepth());
+ $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState');
+ $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager));
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * A post-transaction callback for testing purposes.
+ */
+ public function rootTransactionCallback(bool $success): void {
+ $this->postTransactionCallbackAction = $success ? 'rtcCommit' : 'rtcRollback';
+ $this->insertRow($this->postTransactionCallbackAction);
+ }
+
+ /**
+ * Tests TransactionManager failure.
+ */
+ public function testTransactionManagerFailureOnPendingStackItems(): void {
+ $connectionInfo = Database::getConnectionInfo();
+ Database::addConnectionInfo('default', 'test_fail', $connectionInfo['default']);
+ $testConnection = Database::getConnection('test_fail');
+
+ // Add a fake item to the stack.
+ $manager = $testConnection->transactionManager();
+ $reflectionMethod = new \ReflectionMethod($manager, 'addStackItem');
+ $reflectionMethod->invoke($manager, 'bar', new StackItem('qux', StackItemType::Root));
+ // Ensure transaction state can be determined during object destruction.
+ // This is necessary for the test to pass when xdebug.mode has the 'develop'
+ // option enabled.
+ $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'connectionTransactionState');
+ $reflectionProperty->setValue($manager, ClientConnectionTransactionState::Active);
+
+ // Ensure that __destruct() results in an assertion error. Note that this
+ // will normally be called by PHP during the object's destruction but Drupal
+ // will commit all transactions when a database is closed thereby making
+ // this impossible to test unless it is called directly.
+ try {
+ $manager->__destruct();
+ $this->fail("Expected AssertionError error not thrown");
+ }
+ catch (\AssertionError $e) {
+ $this->assertStringStartsWith('Transaction $stack was not empty. Active stack: bar\\qux', $e->getMessage());
+ }
+
+ // Clean up.
+ $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'stack');
+ $reflectionProperty->setValue($manager, []);
+ unset($testConnection);
+ Database::closeConnection('test_fail');
+ }
+
+}
diff --git a/core/themes/olivero/config/optional/block.block.olivero_syndicate.yml b/core/themes/olivero/config/optional/block.block.olivero_syndicate.yml
deleted file mode 100644
index 343b8d3256a5..000000000000
--- a/core/themes/olivero/config/optional/block.block.olivero_syndicate.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-langcode: en
-status: true
-dependencies:
- module:
- - node
- theme:
- - olivero
-id: olivero_syndicate
-theme: olivero
-region: social
-weight: 0
-provider: null
-plugin: node_syndicate_block
-settings:
- id: node_syndicate_block
- label: 'RSS feed'
- label_display: '0'
- provider: node
- block_count: 10
-visibility: { }