class RulesDebugLog

Logger that stores Rules debug logs with the session service.

This logger stores an array of Rules debug logs in the session under the attribute named 'rules_debug_log'.

Hierarchy

Expanded class hierarchy of RulesDebugLog

1 file declares its use of RulesDebugLog
RulesDebugLoggerChannelTest.php in tests/src/Unit/RulesDebugLoggerChannelTest.php
1 string reference to 'RulesDebugLog'
rules.services.yml in ./rules.services.yml
rules.services.yml
1 service uses RulesDebugLog
logger.rules_debug_log in ./rules.services.yml
Drupal\rules\Logger\RulesDebugLog

File

src/Logger/RulesDebugLog.php, line 20

Namespace

Drupal\rules\Logger
View source
class RulesDebugLog implements LoggerInterface {
  use LoggerTrait;
  use StringTranslationTrait;
  
  /**
   * Local storage of log entries.
   *
   * @var array
   */
  protected $logs = [];
  
  /**
   * The session service.
   *
   * @var \Symfony\Component\HttpFoundation\Session\SessionInterface
   */
  protected $session;
  
  /**
   * Constructs a RulesDebugLog object.
   *
   * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
   *   The session service.
   */
  public function __construct(SessionInterface $session) {
    $this->session = $session;
  }
  
  /**
   * {@inheritdoc}
   */
  public function log($level, $message, array $context = []) : void {
    // Remove any backtraces since they may contain an unserializable variable.
    unset($context['backtrace']);
    $localCopy = $this->session
      ->get('rules_debug_log', []);
    // Append the new log to the $localCopy array.
    // In D7:
    // @code
    //   logs[] = [$msg, $args, $priority, microtime(TRUE), $scope, $path];
    // @endcode
    $localCopy[] = [
      'message' => $message,
      'context' => $context,
      /** @var \Psr\Log\LogLevel $level */
'level' => $level,
      'timestamp' => $context['timestamp'],
      'scope' => $context['scope'],
      'path' => $context['path'],
    ];
    // Write the $localCopy array back into the session;
    // it now includes the new log.
    $this->session
      ->set('rules_debug_log', $localCopy);
  }
  
  /**
   * Returns a structured array of log entries.
   *
   * @return array
   *   Array of stored log entries, keyed by an integer log line number. Each
   *   element of the array contains the following keys:
   *   - message: The log message, optionally with FormattedMarkup placeholders.
   *   - context: An array of message placeholder replacements.
   *   - level: \Psr\Log\LogLevel level.
   *   - timestamp: Microtime timestamp in float format.
   *   - scope: TRUE if there are nested logs for this entry, FALSE if this is
   *     the last of the nested entries.
   *   - path: Path to edit this component.
   */
  public function getLogs() : array {
    return (array) $this->session
      ->get('rules_debug_log');
  }
  
  /**
   * Clears the logs entries from the storage.
   */
  public function clearLogs() : void {
    $this->session
      ->remove('rules_debug_log');
  }
  
  /**
   * Renders the whole log.
   *
   * @return \Drupal\Component\Render\MarkupInterface
   *   An string already rendered to HTML.
   */
  public function render() {
    $build = $this->build();
    return \Drupal::service('renderer')->renderPlain($build);
  }
  
  /**
   * Assembles the entire log into a render array.
   *
   * @return array
   *   A Drupal render array.
   */
  public function build() : array {
    $this->logs = $this->getLogs();
    if (count($this->logs) == 0) {
      // Nothing to render.
      return [];
    }
    // Container for all log entries.
    $build = [
      '#type' => 'details',
      // @codingStandardsIgnoreStart
'#title' => $this->t('Rules evaluation log') . '<span class="rules-debug-open-all">-Open all-</span>',
      // @codingStandardsIgnoreEnd
'#attributes' => [
        'class' => [
          'rules-debug-log',
        ],
      ],
    ];
    $line = 0;
    while (isset($this->logs[$line])) {
      // Each event is in its own 'details' wrapper so the details of
      // evaluation may be opened or closed.
      $build[$line] = [
        '#type' => 'details',
        // @codingStandardsIgnoreStart
        // Need to filter out context keys that aren't recognized as
        // placeholders for t(), because Drupal core no longer supports these.
'#title' => $this->t($this->logs[$line]['message'], $this->filterContext($this->logs[$line]['context'])),
      ];
      // $line is modified inside renderHelper().
      $thisline = $line;
      $build[$thisline][] = $this->renderHelper($line);
      $line++;
    }
    return $build;
  }
  
  /**
   * Renders the log of one event invocation.
   *
   * Called recursively, consuming all the log lines for this event.
   *
   * @param int $line
   *   The line number of the log, starting at 0.
   *
   * @return array
   *   A render array.
   */
  protected function renderHelper(int &$line = 0) : array {
    $build = [];
    $startTime = $this->logs[$line]['timestamp'];
    while ($line < count($this->logs)) {
      if ($build && !empty($this->logs[$line]['scope'])) {
        // This next entry stems from another evaluated set so we create a
        // new container for its log messages then fill that container with
        // a recursive call to renderHelper().
        $link = NULL;
        if (isset($this->logs[$line]['path'])) {
          $link = Link::fromTextAndUrl($this->t('edit'), Url::fromUserInput('/' . $this->logs[$line]['path']))
            ->toString();
        }
        $build[$line] = [
          '#type' => 'details',
          // @codingStandardsIgnoreStart
          // Need to filter out context keys that aren't recognized as
          // placeholders for t(), because Drupal core no longer supports these.
'#title' => $this->t($this->logs[$line]['message'], $this->filterContext($this->logs[$line]['context'])) . ' [' . $link . ']',
        ];
        $thisline = $line;
        $build[$thisline][] = $this->renderHelper($line);
      }
      else {
        // This next entry is a leaf of the evaluated set so we just have to
        // add the details of the log entry.
        $link = NULL;
        if (isset($this->logs[$line]['path']) && !isset($this->logs[$line]['scope'])) {
          $link = [
            'title' => $this->t('edit'),
            'url' => Url::fromUserInput('/' . $this->logs[$line]['path']),
          ];
        }
        $build[$line] = [
          '#theme' => 'rules_debug_log_element',
          '#starttime' => $startTime,
          '#timestamp' => $this->logs[$line]['timestamp'],
          '#level' => $this->logs[$line]['level'],
          // @codingStandardsIgnoreStart
          // Need to filter out context keys that aren't recognized as
          // placeholders for t(), because Drupal core no longer supports these.
'#text' => $this->t($this->logs[$line]['message'], $this->filterContext($this->logs[$line]['context'])),
          // @codingStandardsIgnoreEnd
'#link' => $link,
        ];
        if (isset($this->logs[$line]['scope']) && !$this->logs[$line]['scope']) {
          // This was the last log entry of this set.
          return [
            '#theme' => 'item_list',
            '#items' => $build,
          ];
        }
      }
      $line++;
    }
    return [
      '#theme' => 'item_list',
      '#items' => $build,
    ];
  }
  
  /**
   * Removes invalid placeholders from the given array.
   *
   * As of Drupal 10, arrays that contain placeholder replacement strings for
   * use by the core Drupal t() function may not contain any keys that aren't
   * valid placeholders for the string being translated. That means we have to
   * remove keys from these arrays before passing them to the t() function.
   *
   * @param array $context
   *   An array containing placeholder replacements for use by t(), keyed by
   *   the placeholder.
   *
   * @return array
   *   The context array, with invalid placeholders removed.
   *
   * @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat()
   */
  protected function filterContext(array $context = []) : array {
    // This implementation assumes that all valid placeholders start with a
    // punctuation character. In reality Drupal currently supports only '@',
    // '%', and ':', but testing for just those three is considerably slower
    // than using the built-in PHP ctype_punct() function. This will work to
    // remove all invalid placeholders added by the Rules module, but another
    // invalid placeholder added by a user might fall through and still cause
    // an error (as it should, to indicate the user has made an error).
    return array_filter($context, function ($key) {
      return ctype_punct($key[0]);
    }, ARRAY_FILTER_USE_KEY);
  }

}

Members

Title Sort descending Modifiers Object type Summary Overrides
RulesDebugLog::$logs protected property Local storage of log entries.
RulesDebugLog::$session protected property The session service.
RulesDebugLog::build public function Assembles the entire log into a render array.
RulesDebugLog::clearLogs public function Clears the logs entries from the storage.
RulesDebugLog::filterContext protected function Removes invalid placeholders from the given array.
RulesDebugLog::getLogs public function Returns a structured array of log entries.
RulesDebugLog::log public function
RulesDebugLog::render public function Renders the whole log.
RulesDebugLog::renderHelper protected function Renders the log of one event invocation.
RulesDebugLog::__construct public function Constructs a RulesDebugLog object.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.