Completed
Push — 8.x-1.x ( 4c4e07...db76fc )
by Frédéric G.
25s queued 10s
created

Sizes::build()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 53
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 35
c 1
b 0
f 1
dl 0
loc 53
rs 9.0488
cc 5
nc 8
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Core\StringTranslation\StringTranslationTrait;
10
use Drupal\qa\Pass;
11
use Drupal\qa\Plugin\QaCheckBase;
12
use Drupal\qa\Plugin\QaCheckInterface;
13
use Drupal\qa\Result;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
16
/**
17
 * Size checks the size of data in cache, flagging extra-wide data.
18
 *
19
 * This is useful especially for memcached which is by default limited to 1MB
20
 * per item, causing the Memcache driver to go through slower remediating
21
 * mechanisms when data is too large.
22
 *
23
 * It also flags extra-large cache bins.
24
 *
25
 * @QaCheck(
26
 *   id = "cache.sizes",
27
 *   label = @Translation("Cache sizes"),
28
 *   details = @Translation("Find cache entries ≥0.5MB and bins which are empty, ≥1GB, or ≥128k items."),
29
 *   usesBatch = false,
30
 *   steps = 1,
31
 * )
32
 */
33
class Sizes extends QaCheckBase implements QaCheckInterface {
34
  use StringTranslationTrait;
35
36
  const ELLIPSIS = '…';
37
38
  const NAME = 'cache.sizes';
39
40
  /**
41
   * Memcache default entry limit: 1024*1024 * 0.5 for safety.
42
   */
43
  const MAX_ITEM_SIZE = 1 << 19;
44
45
  /**
46
   * Maximum number of items per bin (128k).
47
   */
48
  const MAX_BIN_ITEMS = 1 << 17;
49
50
  /**
51
   * Maximum data size per bin (1 GB).
52
   */
53
  const MAX_BIN_SIZE = 1 << 30;
54
55
  /**
56
   * Size of data summary in reports.
57
   */
58
  const SUMMARY_LENGTH = 1 << 9;
59
60
  /**
61
   * The database service.
62
   *
63
   * @var \Drupal\Core\Database\Connection
64
   */
65
  protected $db;
66
67
  /**
68
   * Undeclared constructor.
69
   *
70
   * @param array $configuration
71
   *   The plugin configuration.
72
   * @param string $id
73
   *   The plugin ID.
74
   * @param array $definition
75
   *   The plugin definition.
76
   * @param \Drupal\Core\Database\Connection $db
77
   *   The database service.
78
   */
79
  public function __construct(
80
    array $configuration,
81
    string $id,
82
    array $definition,
83
    Connection $db
84
  ) {
85
    parent::__construct($configuration, $id, $definition);
86
    $this->db = $db;
87
  }
88
89
  /**
90
   * {@inheritdoc}
91
   */
92
  public static function create(
93
    ContainerInterface $container,
94
    array $configuration,
95
    $id,
96
    $definition
97
  ) {
98
    $db = $container->get('database');
99
    assert($db instanceof Connection);
100
    return new static($configuration, $id, $definition, $db);
101
  }
102
103
  /**
104
   * Get the list of cache bins, correlating the DB and container.
105
   *
106
   * @return array
107
   *   The names of all bins.
108
   */
109
  public function getAllBins(): array {
110
    $dbBins = $this->db
111
      ->schema()
112
      ->findTables('cache_%');
113
    $dbBins = array_filter($dbBins, function ($bin) {
114
      return $this->isSchemaCache($bin);
115
    });
116
    sort($dbBins);
117
118
    // TODO add service-based bin detection.
119
    return $dbBins;
120
  }
121
122
  /**
123
   * Does the schema of the table the expected cache schema structure ?
124
   *
125
   * @param string $table
126
   *   The name of the table to check.
127
   *
128
   * @return bool
129
   *   Is it ?
130
   */
131
  public function isSchemaCache(string $table): bool {
132
    // Core findTable messes the "_" conversion to regex. Double-check here.
133
    if (strpos($table, 'cache_') !== 0) {
134
      return FALSE;
135
    }
136
    $referenceSchemaKeys = [
137
      'checksum',
138
      'cid',
139
      'created',
140
      'data',
141
      'expire',
142
      'serialized',
143
      'tags',
144
    ];
145
    // XXX MySQL-compatible only.
146
    $names = array_keys($this->db
147
      ->query("DESCRIBE $table")
148
      ->fetchAllAssoc('Field')
149
    );
150
    sort($names);
151
    return $names == $referenceSchemaKeys;
152
  }
153
154
  /**
155
   * Check a single cache bin.
156
   *
157
   * TODO support table prefixes.
158
   *
159
   * @param string $bin
160
   *   The name of the bin to check in DB, where it matches the table name.
161
   *
162
   * @return \Drupal\qa\Result
163
   *   The check result for the bin.
164
   */
165
  public function checkBin($bin): Result {
166
    $res = new Result($bin, FALSE);
167
    $arg = ['@name' => $bin];
168
169
    if (!$this->db->schema()->tableExists($bin)) {
170
      $res->data = $this->t('Bin @name is missing in the database.', $arg);
171
      return $res;
172
    }
173
174
    $sql = <<<SQL
175
SELECT cid, data, expire, created, serialized
176
FROM {$bin}
177
ORDER BY cid;
178
SQL;
179
    $q = $this->db->query($sql);
180
    if (!$q instanceof StatementInterface) {
181
      $res->data = $this->t('Failed fetching database data for bin @name.', $arg);
182
      return $res;
183
    }
184
185
    [$res->ok, $res->data['count'], $res->data['size'], $res->data['items']] =
186
      $this->checkBinContents($q);
187
    if ($res->ok) {
188
      unset($res->data['items']);
189
    }
190
    return $res;
191
  }
192
193
  /**
194
   * Check the contents of an existing and accessible bin.
195
   *
196
   * @param \Drupal\Core\Database\StatementInterface $q
197
   *   The query object for the bin contents, already queried.
198
   *
199
   * @return array
200
   *   - 0 : status bool
201
   *   - 1 : item count
202
   *   - 2 : total size
203
   *   - 3 : result array
204
   */
205
  protected function checkBinContents(StatementInterface $q) {
206
    $count = 0;
207
    $size = 0;
208
    $status = TRUE;
209
    $items = [];
210
    foreach ($q->fetchAll() as $row) {
211
      // Cache drivers will need to serialize anyway.
212
      $data = $row->serialized ? $row->data : serialize($row->data);
213
      $len = strlen($data);
214
      if ($len == 0 || $len >= static::MAX_ITEM_SIZE) {
215
        $status = FALSE;
216
        $items[$row->cid] = [
217
          'len' => number_format($len, 0, ',', ''),
218
          // Auto-escaped in Twig when rendered in the Web UI.
219
          'data' => mb_substr($data, 0, static::SUMMARY_LENGTH) . self::ELLIPSIS,
220
        ];
221
      }
222
      $size += $len;
223
      $count++;
224
    }
225
226
    // Empty bins are suspicious. So are bins with empty entries.
227
    if ($count === 0 || $size === 0) {
1 ignored issue
show
introduced by
The condition $size === 0 is always true.
Loading history...
228
      $status = FALSE;
229
    }
230
    return [$status, $count, $size, $items];
231
  }
232
233
  /**
234
   * Render a result for the Web UI.
235
   *
236
   * @param \Drupal\qa\Pass $pass
237
   *   A check pass to render.
238
   *
239
   * @return array
240
   *   A render array.
241
   *
242
   * @FIXME inconsistent logic, fix in issue #8.
243
   */
244
  protected function build(Pass $pass): array {
245
    $build = [];
246
    $bins = $pass->result['bins'];
247
    if ($pass->ok) {
248
      $info = $this->formatPlural(count($bins),
249
        '1 bin checked, not containing suspicious values',
250
        '@count bins checked, none containing suspicious values', []
251
      );
252
    }
253
    else {
254
      $info = $this->formatPlural(count($bins),
255
        '1 view checked and containing suspicious values',
256
        '@count bins checked, @bins containing suspicious values', [
257
          '@bins' => count($pass->result),
258
        ]);
259
    }
260
261
    foreach ($bins as $bin) {
262
      // Prepare for theming.
263
      $result = [];
264
      // @XXX May be inconsistent with non-BMP strings ?
265
      uksort($pass->result, 'strcasecmp');
266
      foreach ($pass->result as $bin_name => $bin_report) {
267
        foreach ($bin_report as $entry) {
268
          array_unshift($entry, $bin_name);
269
          $result[] = $entry;
270
        }
271
      }
272
      $header = [
273
        $this->t('Bin'),
274
        $this->t('CID'),
275
        $this->t('Length'),
276
        $this->t('Beginning of data'),
277
      ];
278
279
      $build[$bin] = [
280
        'info' => [
281
          '#markup' => $info,
282
        ],
283
        'list' => [
284
          '#markup' => '<p>' . $this->t('Checked: @checked', [
285
            '@checked' => implode(', ', $bins),
286
          ]) . "</p>\n",
287
        ],
288
        'table' => [
289
          '#theme' => 'table',
290
          '#header' => $header,
291
          '#rows' => $result,
292
        ],
293
      ];
294
    }
295
296
    return $build;
297
  }
298
299
  /**
300
   * {@inheritdoc}
301
   */
302
  public function run(): Pass {
303
    $pass = parent::run();
304
    $bins = $this->getAllBins();
305
    foreach ($bins as $bin_name) {
306
      $pass->record($this->checkBin($bin_name));
307
    }
308
    $pass->life->end();
309
    return $pass;
310
  }
311
312
}
313