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