diff --git a/core/modules/node/src/Tests/NodeLoadMultipleTest.php b/core/modules/node/src/Tests/NodeLoadMultipleTest.php index 4add3bd..c758b3d 100644 --- a/core/modules/node/src/Tests/NodeLoadMultipleTest.php +++ b/core/modules/node/src/Tests/NodeLoadMultipleTest.php @@ -1,7 +1,6 @@ entityTypeManager = $entity_type_manager; + $this->entityRepository = $entity_repository; + $this->statisticsStorage = $statistics_storage; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity.repository'), + $container->get('statistics.storage'), + $container->get('renderer') + ); + } /** * {@inheritdoc} */ public function defaultConfiguration() { - return array( + return [ 'top_day_num' => 0, 'top_all_num' => 0, 'top_last_num' => 0 - ); + ]; } /** @@ -40,29 +112,29 @@ protected function blockAccess(AccountInterface $account) { */ public function blockForm($form, FormStateInterface $form_state) { // Popular content block settings. - $numbers = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40); - $numbers = array('0' => $this->t('Disabled')) + array_combine($numbers, $numbers); - $form['statistics_block_top_day_num'] = array( + $numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40]; + $numbers = ['0' => $this->t('Disabled')] + array_combine($numbers, $numbers); + $form['statistics_block_top_day_num'] = [ '#type' => 'select', '#title' => $this->t("Number of day's top views to display"), '#default_value' => $this->configuration['top_day_num'], '#options' => $numbers, '#description' => $this->t('How many content items to display in "day" list.'), - ); - $form['statistics_block_top_all_num'] = array( + ]; + $form['statistics_block_top_all_num'] = [ '#type' => 'select', '#title' => $this->t('Number of all time views to display'), '#default_value' => $this->configuration['top_all_num'], '#options' => $numbers, '#description' => $this->t('How many content items to display in "all time" list.'), - ); - $form['statistics_block_top_last_num'] = array( + ]; + $form['statistics_block_top_last_num'] = [ '#type' => 'select', '#title' => $this->t('Number of most recent views to display'), '#default_value' => $this->configuration['top_last_num'], '#options' => $numbers, '#description' => $this->t('How many content items to display in "recently viewed" list.'), - ); + ]; return $form; } @@ -79,31 +151,67 @@ public function blockSubmit($form, FormStateInterface $form_state) { * {@inheritdoc} */ public function build() { - $content = array(); + $content = []; if ($this->configuration['top_day_num'] > 0) { - $result = statistics_title_list('daycount', $this->configuration['top_day_num']); + $result = $this->statisticsStorage->fetchAll('daycount', $this->configuration['top_day_num']); if ($result) { - $content['top_day'] = node_title_list($result, $this->t("Today's:")); + $content['top_day'] = $this->nodeTitleList($result, $this->t("Today's:")); $content['top_day']['#suffix'] = '
'; } } if ($this->configuration['top_all_num'] > 0) { - $result = statistics_title_list('totalcount', $this->configuration['top_all_num']); + $result = $this->statisticsStorage->fetchAll('totalcount', $this->configuration['top_all_num']); if ($result) { - $content['top_all'] = node_title_list($result, $this->t('All time:')); + $content['top_all'] = $this->nodeTitleList($result, $this->t('All time:')); $content['top_all']['#suffix'] = '
'; } } if ($this->configuration['top_last_num'] > 0) { - $result = statistics_title_list('timestamp', $this->configuration['top_last_num']); - $content['top_last'] = node_title_list($result, $this->t('Last viewed:')); + $result = $this->statisticsStorage->fetchAll('timestamp', $this->configuration['top_last_num']); + $content['top_last'] = $this->nodeTitleList($result, $this->t('Last viewed:')); $content['top_last']['#suffix'] = '
'; } return $content; } + /** + * Generates the ordered array of node links for build(). + * + * @param int[] $nids + * An ordered array of node ids. + * @param string $title + * The title for the list. + * + * @return array + * A render array for the list. + */ + protected function nodeTitleList(array $nids, $title) { + $nodes = Node::loadMultiple($nids); + + $items = []; + foreach ($nids as $nid) { + $node = $this->entityRepository->getTranslationFromContext($nodes[$nid]); + $item = [ + '#type' => 'link', + '#title' => $node->getTitle(), + '#url' => $node->urlInfo('canonical'), + ]; + $this->renderer->addCacheableDependency($item, $node); + $items[] = $item; + } + + return [ + '#theme' => 'item_list__node', + '#items' => $items, + '#title' => $title, + '#cache' => [ + 'tags' => $this->entityTypeManager->getDefinition('node')->getListCacheTags(), + ], + ]; + } + } diff --git a/core/modules/statistics/src/StatisticsDatabaseStorage.php b/core/modules/statistics/src/StatisticsDatabaseStorage.php new file mode 100644 index 0000000..a8ea5ef --- /dev/null +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php @@ -0,0 +1,150 @@ +connection = $connection; + $this->state = $state; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public function recordHit($id) { + return (bool) $this->connection + ->merge('node_counter') + ->key('nid', $id) + ->fields([ + 'daycount' => 1, + 'totalcount' => 1, + 'timestamp' => $this->getRequestTime(), + ]) + ->expression('daycount', 'daycount + 1') + ->expression('totalcount', 'totalcount + 1') + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function fetchViews($id) { + $views = $this->connection + ->select('node_counter', 'nc') + ->fields('nc', ['totalcount', 'daycount', 'timestamp']) + ->condition('nid', $id, '=') + ->execute() + ->fetchAssoc(); + return new StatisticsViews($views['totalcount'], $views['daycount'], $views['timestamp']); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($order = 'totalcount', $limit = 5) { + // @todo replace exception with assert() - #2408013. + if (!in_array($order, ['totalcount', 'daycount', 'timestamp'])) { + throw new \InvalidArgumentException(); + } + + return $this->connection + ->select('node_counter', 'nc') + ->fields('nc', ['nid']) + ->orderBy($order, 'DESC') + ->range(0, $limit) + ->execute() + ->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function clean($id) { + return (bool) $this->connection + ->delete('node_counter') + ->condition('nid', $id) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function needsReset() { + $statistics_timestamp = $this->state->get('statistics.day_timestamp') ?: 0; + return ($this->getRequestTime() - $statistics_timestamp) >= 86400; + } + + /** + * {@inheritdoc} + */ + public function resetDayCount() { + $this->state->set('statistics.day_timestamp', $this->getRequestTime()); + return (bool) $this->connection->update('node_counter') + ->fields(['daycount' => 0]) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function maxTotalCount() { + $query = $this->connection->select('node_counter', 'nc'); + $query->addExpression('MAX(totalcount)'); + $max_total_count = (int)$query->execute()->fetchField(); + $this->state->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count)); + return $max_total_count; + } + + /** + * Get current request time. + * + * @return int + * Unix timestamp for current server request time. + */ + protected function getRequestTime() { + return $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME'); + } + +} diff --git a/core/modules/statistics/src/StatisticsStorageInterface.php b/core/modules/statistics/src/StatisticsStorageInterface.php new file mode 100644 index 0000000..d287d2b --- /dev/null +++ b/core/modules/statistics/src/StatisticsStorageInterface.php @@ -0,0 +1,95 @@ +totalCount = $total_count; + $this->dayCount = $day_count; + $this->timestamp = $timestamp; + } + + /** + * {@inheritdoc} + */ + public function getTotalCount() { + return $this->totalCount; + } + + /** + * {@inheritdoc} + */ + public function getDayCount() { + return $this->dayCount; + } + + /** + * {@inheritdoc} + */ + public function getTimestamp() { + return $this->timestamp; + } +} diff --git a/core/modules/statistics/src/StatisticsViewsInterface.php b/core/modules/statistics/src/StatisticsViewsInterface.php new file mode 100644 index 0000000..2c72184 --- /dev/null +++ b/core/modules/statistics/src/StatisticsViewsInterface.php @@ -0,0 +1,28 @@ +drupalGet('node/' . $this->testNode->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $this->testNode->id(); - $post = array('nid' => $nid); + $post = array('id' => $nid); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; $this->client->post($stats_path, array('form_params' => $post)); @@ -108,7 +108,7 @@ function testDeleteNode() { $this->drupalGet('node/' . $this->testNode->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $this->testNode->id(); - $post = array('nid' => $nid); + $post = array('id' => $nid); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; $this->client->post($stats_path, array('form_params' => $post)); @@ -142,7 +142,7 @@ function testExpiredLogs() { $this->drupalGet('node/' . $this->testNode->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $this->testNode->id(); - $post = array('nid' => $nid); + $post = array('id' => $nid); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; $this->client->post($stats_path, array('form_params' => $post)); diff --git a/core/modules/statistics/src/Tests/StatisticsLoggingTest.php b/core/modules/statistics/src/Tests/StatisticsLoggingTest.php index 7cfec7c..81c917f 100644 --- a/core/modules/statistics/src/Tests/StatisticsLoggingTest.php +++ b/core/modules/statistics/src/Tests/StatisticsLoggingTest.php @@ -113,17 +113,17 @@ function testLogging() { $this->drupalGet($path); $settings = $this->getDrupalSettings(); $this->assertPattern($expected_library, 'Found statistics library JS on node page.'); - $this->assertIdentical($this->node->id(), $settings['statistics']['data']['nid'], 'Found statistics settings on node page.'); + $this->assertIdentical($this->node->id(), $settings['statistics']['data']['id'], 'Found statistics settings on node page.'); // Verify the same when loading the site in a non-default language. $this->drupalGet($this->language['langcode'] . '/' . $path); $settings = $this->getDrupalSettings(); $this->assertPattern($expected_library, 'Found statistics library JS on a valid node page in a non-default language.'); - $this->assertIdentical($this->node->id(), $settings['statistics']['data']['nid'], 'Found statistics settings on valid node page in a non-default language.'); + $this->assertIdentical($this->node->id(), $settings['statistics']['data']['id'], 'Found statistics settings on valid node page in a non-default language.'); // Manually call statistics.php to simulate ajax data collection behavior. global $base_root; - $post = array('nid' => $this->node->id()); + $post = array('id' => $this->node->id()); $this->client->post($base_root . $stats_path, array('form_params' => $post)); $node_counter = statistics_get($this->node->id()); $this->assertIdentical($node_counter['totalcount'], '1'); diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php index 9c0d26c..ada5e10 100644 --- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php @@ -2,6 +2,9 @@ namespace Drupal\statistics\Tests; +use Drupal\Core\Cache\Cache; +use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; + /** * Tests display of statistics report blocks. * @@ -9,6 +12,8 @@ */ class StatisticsReportsTest extends StatisticsTestBase { + use AssertPageCacheContextsAndTagsTrait; + /** * Tests the "popular content" block. */ @@ -21,7 +26,7 @@ function testPopularContentBlock() { $this->drupalGet('node/' . $node->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $node->id(); - $post = http_build_query(array('nid' => $nid)); + $post = http_build_query(array('id' => $nid)); $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; @@ -30,7 +35,7 @@ function testPopularContentBlock() { $client->post($stats_path, array('headers' => $headers, 'body' => $post)); // Configure and save the block. - $this->drupalPlaceBlock('statistics_popular_block', array( + $block = $this->drupalPlaceBlock('statistics_popular_block', array( 'label' => 'Popular content', 'top_day_num' => 3, 'top_all_num' => 3, @@ -44,9 +49,16 @@ function testPopularContentBlock() { $this->assertText('All time', 'Found the all time popular content.'); $this->assertText('Last viewed', 'Found the last viewed popular content.'); - // statistics.module doesn't use node entities, prevent the node language - // from being added to the options. - $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical', ['language' => NULL])), 'Found link to visited node.'); + $tags = Cache::mergeTags($node->getCacheTags(), $block->getCacheTags()); + $tags = Cache::mergeTags($tags, $this->blockingUser->getCacheTags()); + $tags = Cache::mergeTags($tags, ['block_view', 'config:block_list', 'node_list', 'rendered', 'user_view']); + $this->assertCacheTags($tags); + $contexts = Cache::mergeContexts($node->getCacheContexts(), $block->getCacheContexts()); + $contexts = Cache::mergeContexts($contexts, ['url.query_args:_wrapper_format']); + $this->assertCacheContexts($contexts); + + // Check if the node link is displayed. + $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical')), 'Found link to visited node.'); } } diff --git a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php b/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php index b7d22b8..2a0ce86 100644 --- a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php +++ b/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php @@ -24,7 +24,7 @@ function testStatisticsTokenReplacement() { $this->drupalGet('node/' . $node->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $node->id(); - $post = http_build_query(array('nid' => $nid)); + $post = http_build_query(array('id' => $nid)); $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; diff --git a/core/modules/statistics/src/Tests/Views/IntegrationTest.php b/core/modules/statistics/src/Tests/Views/IntegrationTest.php index 4882fd2..26b2b68 100644 --- a/core/modules/statistics/src/Tests/Views/IntegrationTest.php +++ b/core/modules/statistics/src/Tests/Views/IntegrationTest.php @@ -72,7 +72,7 @@ public function testNodeCounterIntegration() { global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php'; $client = \Drupal::service('http_client_factory')->fromOptions(['config/curl', array(CURLOPT_TIMEOUT => 10)]); - $client->post($stats_path, array('form_params' => array('nid' => $this->node->id()))); + $client->post($stats_path, array('form_params' => array('id' => $this->node->id()))); $this->drupalGet('test_statistics_integration'); $expected = statistics_get($this->node->id()); diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module index 5079e43..6fdf3b5 100644 --- a/core/modules/statistics/statistics.module +++ b/core/modules/statistics/statistics.module @@ -40,7 +40,7 @@ function statistics_help($route_name, RouteMatchInterface $route_match) { function statistics_node_view(array &$build, EntityInterface $node, EntityViewDisplayInterface $display, $view_mode) { if (!$node->isNew() && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) { $build['#attached']['library'][] = 'statistics/drupal.statistics'; - $settings = array('data' => array('nid' => $node->id()), 'url' => Url::fromUri('base:' . drupal_get_path('module', 'statistics') . '/statistics.php')->toString()); + $settings = ['data' => ['id' => $node->id()], 'url' => Url::fromUri('base:' . drupal_get_path('module', 'statistics') . '/statistics.php')->toString()]; $build['#attached']['drupalSettings']['statistics'] = $settings; } } @@ -52,14 +52,14 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array if ($context['view_mode'] != 'rss') { $links['#cache']['contexts'][] = 'user.permissions'; if (\Drupal::currentUser()->hasPermission('view post access counter')) { - $statistics = statistics_get($entity->id()); - if ($statistics) { - $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics['totalcount'], '1 view', '@count views'); - $links['statistics'] = array( + $statistics = \Drupal::service('statistics.storage')->fetchViews($entity->id()); + if ($statistics instanceof \Drupal\statistics\StatisticsViewsInterface) { + $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics->getTotalCount(), '1 view', '@count views'); + $links['statistics'] = [ '#theme' => 'links__node__statistics', '#links' => $statistics_links, '#attributes' => array('class' => array('links', 'inline')), - ); + ]; } $links['#cache']['max-age'] = \Drupal::config('statistics.settings')->get('display_max_age'); } @@ -70,79 +70,28 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array * Implements hook_cron(). */ function statistics_cron() { - $statistics_timestamp = \Drupal::state()->get('statistics.day_timestamp') ?: 0; - - if ((REQUEST_TIME - $statistics_timestamp) >= 86400) { - // Reset day counts. - db_update('node_counter') - ->fields(array('daycount' => 0)) - ->execute(); - \Drupal::state()->set('statistics.day_timestamp', REQUEST_TIME); + $storage = \Drupal::service('statistics.storage'); + if ($storage->needsReset()) { + $storage->resetDayCount(); } - - // Calculate the maximum of node views, for node search ranking. - \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField())); + $storage->maxTotalCount(); } /** - * Returns the most viewed content of all time, today, or the last-viewed node. - * - * @param string $dbfield - * The database field to use, one of: - * - 'totalcount': Integer that shows the top viewed content of all time. - * - 'daycount': Integer that shows the top viewed content for today. - * - 'timestamp': Integer that shows only the last viewed node. - * @param int $dbrows - * The number of rows to be returned. - * - * @return SelectQuery|FALSE - * A query result containing the node ID, title, user ID that owns the node, - * and the username for the selected node(s), or FALSE if the query could not - * be executed correctly. - */ -function statistics_title_list($dbfield, $dbrows) { - if (in_array($dbfield, array('totalcount', 'daycount', 'timestamp'))) { - $query = db_select('node_field_data', 'n'); - $query->addTag('node_access'); - $query->join('node_counter', 's', 'n.nid = s.nid'); - $query->join('users_field_data', 'u', 'n.uid = u.uid'); - - return $query - ->fields('n', array('nid', 'title')) - ->fields('u', array('uid', 'name')) - ->condition($dbfield, 0, '<>') - ->condition('n.status', 1) - // @todo This should be actually filtering on the desired node status - // field language and just fall back to the default language. - ->condition('n.default_langcode', 1) - ->condition('u.default_langcode', 1) - ->orderBy($dbfield, 'DESC') - ->range(0, $dbrows) - ->execute(); - } - return FALSE; -} - - -/** * Retrieves a node's "view statistics". * - * @param int $nid - * The node ID. - * - * @return array - * An associative array containing: - * - totalcount: Integer for the total number of times the node has been - * viewed. - * - daycount: Integer for the total number of times the node has been viewed - * "today". For the daycount to be reset, cron must be enabled. - * - timestamp: Integer for the timestamp of when the node was last viewed. + * @deprecated in Drupal 8.2.x, will be removed before Drupal 9.0.0. + * Use \Drupal::service('statistics.storage')->fetchViews($id). */ -function statistics_get($nid) { - - if ($nid > 0) { - // Retrieve an array with both totalcount and daycount. - return db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = :nid', array(':nid' => $nid), array('target' => 'replica'))->fetchAssoc(); +function statistics_get($id) { + if ($id > 0) { + /** @var \Drupal\statistics\StatisticsViewsInterface $statistics */ + $statistics = \Drupal::service('statistics.storage')->fetchViews($id); + return [ + 'totalcount' => $statistics->getTotalCount(), + 'daycount' => $statistics->getDayCount(), + 'timestamp' => $statistics->getTimestamp(), + ]; } } @@ -151,9 +100,8 @@ function statistics_get($nid) { */ function statistics_node_predelete(EntityInterface $node) { // Clean up statistics table when node is deleted. - db_delete('node_counter') - ->condition('nid', $node->id()) - ->execute(); + $id = $node->id(); + return \Drupal::service('statistics.storage')->clean($id); } /** @@ -161,15 +109,15 @@ function statistics_node_predelete(EntityInterface $node) { */ function statistics_ranking() { if (\Drupal::config('statistics.settings')->get('count_content_views')) { - return array( - 'views' => array( + return [ + 'views' => [ 'title' => t('Number of views'), - 'join' => array( + 'join' => [ 'type' => 'LEFT', 'table' => 'node_counter', 'alias' => 'node_counter', 'on' => 'node_counter.nid = i.sid', - ), + ], // Inverse law that maps the highest view count on the site to 1 and 0 // to 0. Note that the ROUND here is necessary for PostgreSQL and SQLite // in order to ensure that the :statistics_scale argument is treated as @@ -177,9 +125,9 @@ function statistics_ranking() { // values in as strings instead of numbers in complex expressions like // this. 'score' => '2.0 - 2.0 / (1.0 + node_counter.totalcount * (ROUND(:statistics_scale, 4)))', - 'arguments' => array(':statistics_scale' => \Drupal::state()->get('statistics.node_counter_scale') ?: 0), - ), - ); + 'arguments' => [':statistics_scale' => \Drupal::state()->get('statistics.node_counter_scale') ?: 0], + ], + ]; } } diff --git a/core/modules/statistics/statistics.php b/core/modules/statistics/statistics.php index a79af5f..9b861e8 100644 --- a/core/modules/statistics/statistics.php +++ b/core/modules/statistics/statistics.php @@ -14,24 +14,17 @@ $kernel = DrupalKernel::createFromRequest(Request::createFromGlobals(), $autoloader, 'prod'); $kernel->boot(); +$container = $kernel->getContainer(); -$views = $kernel->getContainer() +$views = $container ->get('config.factory') ->get('statistics.settings') ->get('count_content_views'); if ($views) { - $nid = filter_input(INPUT_POST, 'nid', FILTER_VALIDATE_INT); - if ($nid) { - \Drupal::database()->merge('node_counter') - ->key('nid', $nid) - ->fields(array( - 'daycount' => 1, - 'totalcount' => 1, - 'timestamp' => REQUEST_TIME, - )) - ->expression('daycount', 'daycount + 1') - ->expression('totalcount', 'totalcount + 1') - ->execute(); + $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT); + if ($id) { + $container->get('request_stack')->push(Request::createFromGlobals()); + $container->get('statistics.storage')->recordHit($id); } } diff --git a/core/modules/statistics/statistics.services.yml b/core/modules/statistics/statistics.services.yml new file mode 100644 index 0000000..b034780 --- /dev/null +++ b/core/modules/statistics/statistics.services.yml @@ -0,0 +1,6 @@ +services: + statistics.storage: + class: Drupal\statistics\StatisticsDatabaseStorage + arguments: ['@database', '@state', '@request_stack'] + tags: + - { name: backend_overridable }