Loading core/core.services.yml +3 −0 Original line number Diff line number Diff line Loading @@ -415,6 +415,9 @@ services: tags: - { name: event_subscriber } Drupal\Core\Database\ReplicaKillSwitch: '@database.replica_kill_switch' Drupal\Core\Database\EventSubscriber\StatementExecutionSubscriber: tags: - { name: event_subscriber } datetime.time: class: Drupal\Component\Datetime\Time arguments: ['@request_stack'] Loading core/lib/Drupal/Core/Database/Connection.php +158 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ namespace Drupal\Core\Database; use Drupal\Component\Assertion\Inspector; use Drupal\Core\Database\Event\DatabaseEvent; use Drupal\Core\Database\Exception\EventException; use Drupal\Core\Database\Query\Condition; use Drupal\Core\Database\Query\Delete; use Drupal\Core\Database\Query\Insert; Loading Loading @@ -216,6 +218,14 @@ abstract class Connection { */ protected $identifierQuotes; /** * Tracks the database API events to be dispatched. * * For performance reasons, database API events are not yielded by default. * Call ::enableEvents() to enable them. */ private array $enabledEvents = []; /** * Constructs a Connection object. * Loading Loading @@ -1867,4 +1877,152 @@ public function hasJson(): bool { } } /** * Returns the status of a database API event toggle. * * @param string $eventName * The name of the event to check. * * @return bool * TRUE if the event is going to be fired by the database API, FALSE * otherwise. */ public function isEventEnabled(string $eventName): bool { return $this->enabledEvents[$eventName] ?? FALSE; } /** * Enables database API events dispatching. * * @param string[] $eventNames * A list of database events to be enabled. * * @return static */ public function enableEvents(array $eventNames): static { foreach ($eventNames as $eventName) { assert(class_exists($eventName), "Event class {$eventName} does not exist"); $this->enabledEvents[$eventName] = TRUE; } return $this; } /** * Disables database API events dispatching. * * @param string[] $eventNames * A list of database events to be disabled. * * @return static */ public function disableEvents(array $eventNames): static { foreach ($eventNames as $eventName) { assert(class_exists($eventName), "Event class {$eventName} does not exist"); $this->enabledEvents[$eventName] = FALSE; } return $this; } /** * Dispatches a database API event via the container dispatcher. * * @param \Drupal\Core\Database\Event\DatabaseEvent $event * The database event. * @param string|null $eventName * (Optional) the name of the event to dispatch. * * @return \Drupal\Core\Database\Event\DatabaseEvent * The database event. * * @throws \Drupal\Core\Database\Exception\EventException * If the container is not initialized. */ public function dispatchEvent(DatabaseEvent $event, ?string $eventName = NULL): DatabaseEvent { if (\Drupal::hasService('event_dispatcher')) { return \Drupal::service('event_dispatcher')->dispatch($event, $eventName); } throw new EventException('The event dispatcher service is not available. Database API events can only be fired if the container is initialized'); } /** * Determine the last non-database method that called the database API. * * Traversing the call stack from the very first call made during the * request, we define "the routine that called this query" as the last entry * in the call stack that is not any method called from the namespace of the * database driver, is not inside the Drupal\Core\Database namespace and does * have a file (which excludes call_user_func_array(), anonymous functions * and similar). That makes the climbing logic very simple, and handles the * variable stack depth caused by the query builders. * * See the @link http://php.net/debug_backtrace debug_backtrace() @endlink * function. * * @return array * This method returns a stack trace entry similar to that generated by * debug_backtrace(). However, it flattens the trace entry and the trace * entry before it so that we get the function and args of the function that * called into the database system, not the function and args of the * database call itself. */ public function findCallerFromDebugBacktrace(): array { $stack = $this->removeDatabaseEntriesFromDebugBacktrace($this->getDebugBacktrace(), $this->getConnectionOptions()['namespace']); // Return the first function call whose stack entry has a 'file' key, that // is, it is not a callback or a closure. for ($i = 0; $i < count($stack); $i++) { if (!empty($stack[$i]['file'])) { return [ 'file' => $stack[$i]['file'], 'line' => $stack[$i]['line'], 'function' => $stack[$i + 1]['function'], 'class' => $stack[$i + 1]['class'] ?? NULL, 'type' => $stack[$i + 1]['type'] ?? NULL, 'args' => $stack[$i + 1]['args'] ?? [], ]; } } return []; } /** * Removes database related calls from a backtrace array. * * @param array $backtrace * A standard PHP backtrace. Passed by reference. * @param string $driver_namespace * The PHP namespace of the database driver. * * @return array * The cleaned backtrace array. */ public static function removeDatabaseEntriesFromDebugBacktrace(array $backtrace, string $driver_namespace): array { // Starting from the very first entry processed during the request, find // the first function call that can be identified as a call to a // method/function in the database layer. for ($n = count($backtrace) - 1; $n >= 0; $n--) { // If the call was made from a function, 'class' will be empty. We give // it a default empty string value in that case. $class = $backtrace[$n]['class'] ?? ''; if (str_starts_with($class, __NAMESPACE__) || str_starts_with($class, $driver_namespace)) { break; } } return array_values(array_slice($backtrace, $n)); } /** * Gets the debug backtrace. * * Wraps the debug_backtrace function to allow mocking results in PHPUnit * tests. * * @return array[] * The debug backtrace. */ protected function getDebugBacktrace(): array { return debug_backtrace(); } } core/lib/Drupal/Core/Database/Database.php +10 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ namespace Drupal\Core\Database; use Composer\Autoload\ClassLoader; use Drupal\Core\Database\Event\StatementExecutionEndEvent; use Drupal\Core\Database\Event\StatementExecutionStartEvent; use Drupal\Core\Extension\ExtensionDiscovery; /** Loading Loading @@ -122,6 +124,10 @@ final public static function startLog($logging_key, $key = 'default') { // logging object associated with it. if (!empty(self::$connections[$key])) { foreach (self::$connections[$key] as $connection) { $connection->enableEvents([ StatementExecutionStartEvent::class, StatementExecutionEndEvent::class, ]); $connection->setLogger(self::$logs[$key]); } } Loading Loading @@ -450,6 +456,10 @@ final protected static function openConnection($key, $target) { // If we have any active logging objects for this connection key, we need // to associate them with the connection we just opened. if (!empty(self::$logs[$key])) { $new_connection->enableEvents([ StatementExecutionStartEvent::class, StatementExecutionEndEvent::class, ]); $new_connection->setLogger(self::$logs[$key]); } Loading core/lib/Drupal/Core/Database/Event/DatabaseEvent.php 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Database\Event; use Drupal\Component\EventDispatcher\Event; /** * Represents a database event. */ abstract class DatabaseEvent extends Event { /** * The time of the event. */ public readonly float $time; /** * Constructs a DatabaseEvent object. */ public function __construct() { $this->time = microtime(TRUE); } } core/lib/Drupal/Core/Database/Event/StatementExecutionEndEvent.php 0 → 100644 +54 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Database\Event; /** * Represents the end of a statement execution as an event. */ class StatementExecutionEndEvent extends DatabaseEvent { /** * Constructs a StatementExecutionEndEvent object. * * See 'Customizing database settings' in settings.php for an explanation of * the $key and $target connection values. * * @param int $statementObjectId * The id of the StatementInterface object as returned by spl_object_id(). * @param string $key * The database connection key. * @param string $target * The database connection target. * @param string $queryString * The SQL statement string being executed, with placeholders. * @param array $args * The placeholders' replacement values. * @param array $caller * A normalized debug backtrace entry representing the last non-db method * called. * @param float $startTime * The time of the statement execution start. */ public function __construct( public readonly int $statementObjectId, public readonly string $key, public readonly string $target, public readonly string $queryString, public readonly array $args, public readonly array $caller, public readonly float $startTime, ) { parent::__construct(); } /** * Gets the query execution elapsed time. * * @return float * The elapsed time. */ public function getElapsedTime(): float { return $this->time - $this->startTime; } } Loading
core/core.services.yml +3 −0 Original line number Diff line number Diff line Loading @@ -415,6 +415,9 @@ services: tags: - { name: event_subscriber } Drupal\Core\Database\ReplicaKillSwitch: '@database.replica_kill_switch' Drupal\Core\Database\EventSubscriber\StatementExecutionSubscriber: tags: - { name: event_subscriber } datetime.time: class: Drupal\Component\Datetime\Time arguments: ['@request_stack'] Loading
core/lib/Drupal/Core/Database/Connection.php +158 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ namespace Drupal\Core\Database; use Drupal\Component\Assertion\Inspector; use Drupal\Core\Database\Event\DatabaseEvent; use Drupal\Core\Database\Exception\EventException; use Drupal\Core\Database\Query\Condition; use Drupal\Core\Database\Query\Delete; use Drupal\Core\Database\Query\Insert; Loading Loading @@ -216,6 +218,14 @@ abstract class Connection { */ protected $identifierQuotes; /** * Tracks the database API events to be dispatched. * * For performance reasons, database API events are not yielded by default. * Call ::enableEvents() to enable them. */ private array $enabledEvents = []; /** * Constructs a Connection object. * Loading Loading @@ -1867,4 +1877,152 @@ public function hasJson(): bool { } } /** * Returns the status of a database API event toggle. * * @param string $eventName * The name of the event to check. * * @return bool * TRUE if the event is going to be fired by the database API, FALSE * otherwise. */ public function isEventEnabled(string $eventName): bool { return $this->enabledEvents[$eventName] ?? FALSE; } /** * Enables database API events dispatching. * * @param string[] $eventNames * A list of database events to be enabled. * * @return static */ public function enableEvents(array $eventNames): static { foreach ($eventNames as $eventName) { assert(class_exists($eventName), "Event class {$eventName} does not exist"); $this->enabledEvents[$eventName] = TRUE; } return $this; } /** * Disables database API events dispatching. * * @param string[] $eventNames * A list of database events to be disabled. * * @return static */ public function disableEvents(array $eventNames): static { foreach ($eventNames as $eventName) { assert(class_exists($eventName), "Event class {$eventName} does not exist"); $this->enabledEvents[$eventName] = FALSE; } return $this; } /** * Dispatches a database API event via the container dispatcher. * * @param \Drupal\Core\Database\Event\DatabaseEvent $event * The database event. * @param string|null $eventName * (Optional) the name of the event to dispatch. * * @return \Drupal\Core\Database\Event\DatabaseEvent * The database event. * * @throws \Drupal\Core\Database\Exception\EventException * If the container is not initialized. */ public function dispatchEvent(DatabaseEvent $event, ?string $eventName = NULL): DatabaseEvent { if (\Drupal::hasService('event_dispatcher')) { return \Drupal::service('event_dispatcher')->dispatch($event, $eventName); } throw new EventException('The event dispatcher service is not available. Database API events can only be fired if the container is initialized'); } /** * Determine the last non-database method that called the database API. * * Traversing the call stack from the very first call made during the * request, we define "the routine that called this query" as the last entry * in the call stack that is not any method called from the namespace of the * database driver, is not inside the Drupal\Core\Database namespace and does * have a file (which excludes call_user_func_array(), anonymous functions * and similar). That makes the climbing logic very simple, and handles the * variable stack depth caused by the query builders. * * See the @link http://php.net/debug_backtrace debug_backtrace() @endlink * function. * * @return array * This method returns a stack trace entry similar to that generated by * debug_backtrace(). However, it flattens the trace entry and the trace * entry before it so that we get the function and args of the function that * called into the database system, not the function and args of the * database call itself. */ public function findCallerFromDebugBacktrace(): array { $stack = $this->removeDatabaseEntriesFromDebugBacktrace($this->getDebugBacktrace(), $this->getConnectionOptions()['namespace']); // Return the first function call whose stack entry has a 'file' key, that // is, it is not a callback or a closure. for ($i = 0; $i < count($stack); $i++) { if (!empty($stack[$i]['file'])) { return [ 'file' => $stack[$i]['file'], 'line' => $stack[$i]['line'], 'function' => $stack[$i + 1]['function'], 'class' => $stack[$i + 1]['class'] ?? NULL, 'type' => $stack[$i + 1]['type'] ?? NULL, 'args' => $stack[$i + 1]['args'] ?? [], ]; } } return []; } /** * Removes database related calls from a backtrace array. * * @param array $backtrace * A standard PHP backtrace. Passed by reference. * @param string $driver_namespace * The PHP namespace of the database driver. * * @return array * The cleaned backtrace array. */ public static function removeDatabaseEntriesFromDebugBacktrace(array $backtrace, string $driver_namespace): array { // Starting from the very first entry processed during the request, find // the first function call that can be identified as a call to a // method/function in the database layer. for ($n = count($backtrace) - 1; $n >= 0; $n--) { // If the call was made from a function, 'class' will be empty. We give // it a default empty string value in that case. $class = $backtrace[$n]['class'] ?? ''; if (str_starts_with($class, __NAMESPACE__) || str_starts_with($class, $driver_namespace)) { break; } } return array_values(array_slice($backtrace, $n)); } /** * Gets the debug backtrace. * * Wraps the debug_backtrace function to allow mocking results in PHPUnit * tests. * * @return array[] * The debug backtrace. */ protected function getDebugBacktrace(): array { return debug_backtrace(); } }
core/lib/Drupal/Core/Database/Database.php +10 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ namespace Drupal\Core\Database; use Composer\Autoload\ClassLoader; use Drupal\Core\Database\Event\StatementExecutionEndEvent; use Drupal\Core\Database\Event\StatementExecutionStartEvent; use Drupal\Core\Extension\ExtensionDiscovery; /** Loading Loading @@ -122,6 +124,10 @@ final public static function startLog($logging_key, $key = 'default') { // logging object associated with it. if (!empty(self::$connections[$key])) { foreach (self::$connections[$key] as $connection) { $connection->enableEvents([ StatementExecutionStartEvent::class, StatementExecutionEndEvent::class, ]); $connection->setLogger(self::$logs[$key]); } } Loading Loading @@ -450,6 +456,10 @@ final protected static function openConnection($key, $target) { // If we have any active logging objects for this connection key, we need // to associate them with the connection we just opened. if (!empty(self::$logs[$key])) { $new_connection->enableEvents([ StatementExecutionStartEvent::class, StatementExecutionEndEvent::class, ]); $new_connection->setLogger(self::$logs[$key]); } Loading
core/lib/Drupal/Core/Database/Event/DatabaseEvent.php 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Database\Event; use Drupal\Component\EventDispatcher\Event; /** * Represents a database event. */ abstract class DatabaseEvent extends Event { /** * The time of the event. */ public readonly float $time; /** * Constructs a DatabaseEvent object. */ public function __construct() { $this->time = microtime(TRUE); } }
core/lib/Drupal/Core/Database/Event/StatementExecutionEndEvent.php 0 → 100644 +54 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Database\Event; /** * Represents the end of a statement execution as an event. */ class StatementExecutionEndEvent extends DatabaseEvent { /** * Constructs a StatementExecutionEndEvent object. * * See 'Customizing database settings' in settings.php for an explanation of * the $key and $target connection values. * * @param int $statementObjectId * The id of the StatementInterface object as returned by spl_object_id(). * @param string $key * The database connection key. * @param string $target * The database connection target. * @param string $queryString * The SQL statement string being executed, with placeholders. * @param array $args * The placeholders' replacement values. * @param array $caller * A normalized debug backtrace entry representing the last non-db method * called. * @param float $startTime * The time of the statement execution start. */ public function __construct( public readonly int $statementObjectId, public readonly string $key, public readonly string $target, public readonly string $queryString, public readonly array $args, public readonly array $caller, public readonly float $startTime, ) { parent::__construct(); } /** * Gets the query execution elapsed time. * * @return float * The elapsed time. */ public function getElapsedTime(): float { return $this->time - $this->startTime; } }