Passed
Pull Request — 8.x-1.x (#17)
by Frédéric G.
05:13
created

Sizes::isSchemaCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 15
c 1
b 0
f 1
dl 0
loc 21
rs 9.7666
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Drupal\qa\Plugin\QaCheck\Cache;
6
7
use Drupal\Core\Database\Connection;
8
use Drupal\Core\Database\StatementInterface;
9
use Drupal\qa\Pass;
10
use Drupal\qa\Plugin\QaCheckBase;
11
use Drupal\qa\Plugin\QaCheckInterface;
12
use Drupal\qa\Result;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
15
/**
16
 * Size checks the size of data in cache, flagging extra-wide data.
17
 *
18
 * This is useful especially for memcached which is by default limited to 1MB
19
 * per item, causing the Memcache driver to go through slower remediating
20
 * mechanisms when data is too large.
21
 *
22
 * It also flags extra-large cache bins.
23
 *
24
 * @QaCheck(
25
 *   id = "cache.sizes",
26
 *   label = @Translation("Cache sizes"),
27
 *   details = @Translation("Find cache entries larger than 0.5MB and bins over 1 GB or over 1M items."),
28
 *   usesBatch = false,
29
 *   steps = 1,
30
 * )
31
 */
32
class Sizes extends QaCheckBase implements QaCheckInterface {
33
34
  const NAME = 'cache.sizes';
35
36
  /**
37
   * Memcache default entry limit: 1024*1024 * 0.5 for safety.
38
   */
39
  const MAX_ITEM_SIZE = 1 << 19;
40
41
  /**
42
   * Maximum number of items per bin (128k).
43
   */
44
  const MAX_BIN_ITEMS = 1 << 17;
45
46
  /**
47
   * Maximum data size per bin (1 GB).
48
   */
49
  const MAX_BIN_SIZE = 1 << 30;
50
51
  /**
52
   * Size of data summary in reports.
53
   */
54
  const DATA_SUMMARY_LENGTH = 1024;
55
56
  /**
57
   * The database service.
58
   *
59
   * @var \Drupal\Core\Database\Connection
60
   */
61
  protected $db;
62
63
  /**
64
   * Undeclared constructor.
65
   *
66
   * @param array $configuration
67
   *   The plugin configuration.
68
   * @param string $id
69
   *   The plugin ID.
70
   * @param array $definition
71
   *   The plugin definition.
72
   * @param \Drupal\Core\Database\Connection $db
73
   *   The database service.
74
   */
75
  public function __construct(
76
    array $configuration,
77
    string $id,
78
    array $definition,
79
    Connection $db
80
  ) {
81
    parent::__construct($configuration, $id, $definition);
82
    $this->db = $db;
83
  }
84
85
  /**
86
   * {@inheritdoc}
87
   */
88
  public static function create(
89
    ContainerInterface $container,
90
    array $configuration,
91
    $id,
92
    $definition
93
  ) {
94
    $db = $container->get('database');
95
    assert($db instanceof Connection);
96
    return new static($configuration, $id, $definition, $db);
97
  }
98
99
  /**
100
   * Get the list of cache bins, correlating the DB and container.
101
   *
102
   * @return array
103
   *   The names of all bins.
104
   */
105
  public function getAllBins(): array {
106
    $dbBins = $this->db
107
      ->schema()
108
      ->findTables('cache_%');
109
    $dbBins = array_filter($dbBins, function ($bin) {
110
      return $this->isSchemaCache($bin);
111
    });
112
    sort($dbBins);
113
114
    // TODO add service-based bin detection.
115
    return $dbBins;
116
  }
117
118
  /**
119
   * Does the schema of the table the expected cache schema structure ?
120
   *
121
   * @param string $table
122
   *   The name of the table to check.
123
   *
124
   * @return bool
125
   *   Is it ?
126
   */
127
  public function isSchemaCache(string $table): bool {
128
    // Core findTable messes the "_" conversion to regex. Double-check here.
129
    if (strpos($table, 'cache_') !== 0) {
130
      return FALSE;
131
    }
132
    $referenceSchemaKeys = [
133
      'checksum',
134
      'cid',
135
      'created',
136
      'data',
137
      'expire',
138
      'serialized',
139
      'tags',
140
    ];
141
    // XXX MySQL-compatible only.
142
    $names = array_keys($this->db
143
      ->query("DESCRIBE $table")
144
      ->fetchAllAssoc('Field')
145
    );
146
    sort($names);
147
    return $names == $referenceSchemaKeys;
148
  }
149
150
  /**
151
   * Check a single cache bin.
152
   *
153
   * TODO support table prefixes.
154
   *
155
   * @param string $bin
156
   *   The name of the bin to check in DB, where it matches the table name.
157
   *
158
   * @return \Drupal\qa\Result
159
   *   The check result for the bin.
160
   */
161
  public function checkBin($bin): Result {
162
    $res = new Result($bin, FALSE);
163
    $arg = ['@name' => $bin];
164
165
    if (!$this->db->schema()->tableExists($bin)) {
166
      $res->data = $this->t('Bin @name is missing in the database.', $arg);
0 ignored issues
show
Bug introduced by
The method t() does not exist on Drupal\qa\Plugin\QaCheck\Cache\Sizes. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

166
      /** @scrutinizer ignore-call */ 
167
      $res->data = $this->t('Bin @name is missing in the database.', $arg);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
167
      return $res;
168
    }
169
170
    $sql = <<<SQL
171
SELECT cid, data, expire, created, serialized 
172
FROM {$bin} 
173
ORDER BY cid;
174
SQL;
175
    $q = $this->db->query($sql);
176
    if (!$q) {
177
      $res->data = $this->t('Failed fetching database data for bin @name.', $arg);
178
      return $res;
179
    }
180
181
    [$res->ok, $res->data] = $this->checkBinContents($q);
0 ignored issues
show
Bug introduced by
It seems like $q can also be of type integer and string; however, parameter $q of Drupal\qa\Plugin\QaCheck...zes::checkBinContents() does only seem to accept Drupal\Core\Database\StatementInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

181
    [$res->ok, $res->data] = $this->checkBinContents(/** @scrutinizer ignore-type */ $q);
Loading history...
182
    return $res;
183
  }
184
185
  /**
186
   * Check the contents of an existing and accessible bin.
187
   *
188
   * @param \Drupal\Core\Database\StatementInterface $q
189
   *   The query object for the bin contents, already queried.
190
   *
191
   * @return array
192
   *   - 0 : status bool
193
   *   - 1 : result array
194
   */
195
  protected function checkBinContents(StatementInterface $q) {
196
    $status = TRUE;
197
    $result = [];
198
    foreach ($q->fetchAll() as $row) {
199
      // Cache drivers will need to serialize anyway.
200
      $data = $row->serialized ? $row->data : serialize($row->data);
201
      $len = strlen($data);
202
      if ($len == 0 || $len >= static::MAX_ITEM_SIZE) {
203
        $status = FALSE;
204
        $result[] = [
205
          $row->cid,
206
          number_format($len, 0, ',', ''),
207
          // Auto-escaped in Twig when rendered in the Web UI.
208
          mb_substr($data, 0, static::DATA_SUMMARY_LENGTH) . '&hellip;',
209
        ];
210
      }
211
    }
212
213
    return [$status, $result];
214
  }
215
216
  /**
217
   * Render a result for the Web UI.
218
   *
219
   * @param \Drupal\qa\Pass $pass
220
   *   A check pass to render.
221
   *
222
   * @return array
223
   *   A render array.
224
   *
225
   * @FIXME inconsistent logic, fix in issue #8.
226
   */
227
  protected function build(Pass $pass): array {
228
    $build = [];
229
    $bins = $pass->result['bins'];
230
    if ($pass->ok) {
231
      $info = $this->formatPlural(count($bins),
0 ignored issues
show
Bug introduced by
The method formatPlural() does not exist on Drupal\qa\Plugin\QaCheck\Cache\Sizes. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

231
      /** @scrutinizer ignore-call */ 
232
      $info = $this->formatPlural(count($bins),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
232
        '1 bin checked, not containing suspicious values',
233
        '@count bins checked, none containing suspicious values', []
234
      );
235
    }
236
    else {
237
      $info = $this->formatPlural(count($bins),
238
        '1 view checked and containing suspicious values',
239
        '@count bins checked, @bins containing suspicious values', [
240
          '@bins' => count($pass->result),
241
        ]);
242
    }
243
244
    foreach ($bins as $bin) {
245
      // Prepare for theming.
246
      $result = [];
247
      // @XXX May be inconsistent with non-BMP strings ?
248
      uksort($pass->result, 'strcasecmp');
249
      foreach ($pass->result as $bin_name => $bin_report) {
250
        foreach ($bin_report as $entry) {
251
          array_unshift($entry, $bin_name);
252
          $result[] = $entry;
253
        }
254
      }
255
      $header = [
256
        $this->t('Bin'),
257
        $this->t('CID'),
258
        $this->t('Length'),
259
        $this->t('Beginning of data'),
260
      ];
261
262
      $build[$bin] = [
263
        'info' => [
264
          '#markup' => $info,
265
        ],
266
        'list' => [
267
          '#markup' => '<p>' . $this->t('Checked: @checked', [
268
            '@checked' => implode(', ', $bins),
269
          ]) . "</p>\n",
270
        ],
271
        'table' => [
272
          '#theme' => 'table',
273
          '#header' => $header,
274
          '#rows' => $result,
275
        ],
276
      ];
277
    }
278
279
    return $build;
280
  }
281
282
  /**
283
   * {@inheritdoc}
284
   */
285
  public function run(): Pass {
286
    $pass = parent::run();
287
    $bins = self::getAllBins(TRUE);
0 ignored issues
show
Unused Code introduced by
The call to Drupal\qa\Plugin\QaCheck\Cache\Sizes::getAllBins() has too many arguments starting with TRUE. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

287
    /** @scrutinizer ignore-call */ 
288
    $bins = self::getAllBins(TRUE);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug Best Practice introduced by
The method Drupal\qa\Plugin\QaCheck\Cache\Sizes::getAllBins() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

287
    /** @scrutinizer ignore-call */ 
288
    $bins = self::getAllBins(TRUE);
Loading history...
288
    foreach ($bins as $bin_name) {
289
      $pass->record($this->checkBin($bin_name));
290
    }
291
    $pass->life->end();
292
    return $pass;
293
  }
294
295
}
296