deprecation_status-8.x-3.x-dev/src/Form/ErrorsForm.php
src/Form/ErrorsForm.php
<?php
namespace Drupal\deprecation_status\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\TableSort;
use Drupal\deprecation_status\DataSource;
use Symfony\Component\HttpFoundation\RedirectResponse;
class ErrorsForm extends FormBase {
/**
* Stored sorting criteria.
*
* @type int
*/
protected $column;
/**
* Stored sorting criteria ('asc' or 'desc').
*
* @type string
*/
protected $sort;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'deprecation_status_errors_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#cache']['tags'][] = 'deprecation_status';
$form['#attached']['library'][] = 'deprecation_status/deprecation_status.lists';
$form['filters'] = [
'#type' => 'details',
'#title' => $this->t('Filter errors'),
'#attributes' => ['class' => ['container-inline']],
'#open' => TRUE,
];
$form['filters']['error'] = [
'#title' => 'Error',
'#type' => 'textfield',
'#default_value' => @$_GET['error'],
'#attributes' => ['placeholder' => "Use *, eg. '*Entity*'"],
'#size' => 20,
];
$form['filters']['names'] = [
'#title' => 'Names',
'#type' => 'textfield',
'#default_value' => @$_GET['names'],
'#attributes' => ['placeholder' => "Use *, eg. 'commerce*'"],
'#size' => 20,
];
$form['filters']['category'] = [
'#title' => 'Category',
'#type' => 'select',
'#options' => [
'Info.yml or composer.json' => 'Info.yml or composer.json',
'Drupal API, rector covered' => 'Drupal API, rector covered',
'Drupal API, not rector covered' => 'Drupal API, not rector covered',
'Symfony API' => 'Symfony API',
'Twig API' => 'Twig API',
'PHPUnit API' => 'PHPUnit API',
'Guzzle API' => 'Guzzle API',
'Frontend API' => 'Frontend API',
'Other problem' => 'Other problem',
'Parse error' => 'Parse error'
],
'#default_value' => @$_GET['category'],
'#required' => FALSE,
'#empty_option' => '- No filter -',
];
$form['filters']['topx'] = [
'#title' => 'Top X by usage',
'#type' => 'select',
'#options' => [],
'#default_value' => @$_GET['topx'],
'#required' => FALSE,
'#empty_option' => '- No filter -',
];
foreach([50, 100, 200, 500, 1000] as $number) {
$form['filters']['topx']['#options'][$number] = '<= ' . $number;
}
$form['filters']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
$common_script = <<<SCRIPTEND
SCRIPTEND;
$form['chart'] = [
'#children' => <<<SCRIPTEND
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.6.2/dist/chart.min.js"></script>
<div id="charts">
<canvas id="total8vs9"></canvas>
<canvas id="totalDeprecations"></canvas>
</div>
<script type="text/javascript">
var ctx = document.getElementById('totalDeprecations').getContext('2d');
var chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [@totalDeprecationsLabels],
datasets: [{
data: [@totalDeprecationsData],
backgroundColor: ['#e9d8a6', '#fdae61', '#f46d43', '#d73027', '#a50026', '#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695']
}]
},
options: {
aspectRatio: window.innerWidth < 800 ? 1.5 : 2,
responsive: false,
plugins: {
title: {
display: true,
text: '@deprecatedcount occurences of errors found',
position: 'bottom'
},
legend: {
display: true,
position: window.innerWidth < 800 ? 'top' : 'right',
onClick: null
}
}
}
});
var ctx = document.getElementById('total8vs9').getContext('2d');
var chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [@8vs9Labels],
datasets: [{
data: [@8vs9DeprecationsData],
backgroundColor: ["#4393c3", "#053061"]
}]
},
options: {
aspectRatio: window.innerWidth < 800 ? 1.5 : 2,
responsive: false,
plugins: {
title: {
display: true,
text: '@8vs9dcount total Drupal deprecation occurences',
position: 'bottom'
},
legend: {
display: true,
position: window.innerWidth < 800 ? 'top' : 'right',
onClick: null
}
}
}
});
</script>
SCRIPTEND
];
// Read all errors into one big array.
$errors = [];
$file = fopen(DataSource::getFullPath('errors_detail.csv'), 'r');
$i = 0;
while ($line = fgetcsv($file, 0, ";")) {
// Skip the header row.
if ($i > 0) {
$errors[] = $line;
}
$i++;
}
fclose($file);
// Set up columns for the table.
$columns = [
'total' => 'All occurences',
'projects' => 'Affected projects',
'top' => 'Occurs in a top X project by usage',
'category' => 'Category',
'message' => 'Message',
];
if (!empty($_GET['names']) || !empty($_GET['topx'])) {
$columns['filtered_total'] = 'Filtered occurrences';
$columns['filtered_projects'] = 'Filtered affected projects';
}
// Add sorting information to columns.
$i = 0;
foreach ($columns as $name => &$column) {
$column = ['data' => $column, 'field' => $i];
$i++;
}
// Sort by total descending initially.
$columns['total']['sort'] = 'desc';
$errors = $this->tableFilter($errors);
if (count($errors) == 0) {
unset($form['chart']);
}
else {
$summary = $this->dataSummary($errors);
$deprecation_labels = "'" . join("','", array_keys($summary['errors'])) . "'";
$deprecation_data = join(',', array_values($summary['errors']));
$deprecation_count = $summary['total'];
$drupal89_labels = "'" . join("','", array_keys($summary['drupal'])) . "'";
$drupal89_data = join(',', array_values($summary['drupal']));
$drupal89_count = $summary['total_drupal'];
$form['chart']['#children'] = str_replace(
[
'@deprecatedcount', '@totalDeprecationsLabels', '@totalDeprecationsData',
'@8vs9dcount', '@8vs9Labels', '@8vs9DeprecationsData'
],
[
$deprecation_count, $deprecation_labels, $deprecation_data,
$drupal89_count, $drupal89_labels, $drupal89_data
],
$form['chart']['#children']
);
}
// Sort the data array based on query tablesort information.
$order = TableSort::getOrder($columns, \Drupal::request());
$this->column = $order['sql'] + ($order['sql'] > 4 ? 2 : 0);
$this->sort = TableSort::getSort($columns, \Drupal::request());
uasort($errors, [$this, 'tableSort']);
// Display the resulting data.
$form['table'] = [
'#type' => 'table',
'#header' => $columns,
'#empty' => t('No matching errors found.'),
];
// Page the results based on query information.
$page = \Drupal::service('pager.parameters')->findPage();
$num_per_page = 30;
\Drupal::service('pager.manager')->createPager(count($errors), $num_per_page);
$offset = $num_per_page * $page;
$errors_page = array_slice($errors, $offset, $num_per_page);
// Key results based on header key names.
$keys = array_keys($columns);
foreach ($errors_page as $error) {
$error_link = [
'data' => [
'#type' => 'link',
'#title' => $error[1],
'#url' => Url::fromRoute('deprecation_status.error', ['error_index' => $error[6]]),
]
];
$error_class = 'warning';
if ($error[3] == 'Drupal API, rector covered') {
$error_class = 'passed';
}
elseif ($error[3] == 'Parse error') {
$error_class = 'error';
}
elseif ($error[3] == 'Other problem') {
$error_class = 'unknown';
}
$row = [
'#attributes' => ['class' => ['item-' . $error_class]],
'total' => ['#type' => 'markup', '#markup' => $error[0]],
'projects' => $error_link,
'top' => ['#type' => 'markup', '#markup' => $error[2]],
'category' => ['#type' => 'markup', '#markup' => '<span class="category">' . $error[3] . '</span>'],
'message' => ['#type' => 'markup', '#markup' => DataSource::formatError($error[4])],
];
if (!empty($_GET['names']) || !empty($_GET['topx'])) {
$row['filtered_total'] = ['#type' => 'markup', '#markup' => $error[7]];
$query = [];
if (!empty($_GET['names'])) {
$query['names'] = $_GET['names'];
}
if (!empty($_GET['topx'])) {
$query['topx'] = $_GET['topx'];
}
$row['filtered_projects'] = [
'data' => [
'#type' => 'link',
'#title' => $error[8],
'#url' => Url::fromRoute('deprecation_status.error', ['error_index' => $error[6]], ['query' => $query]),
]
];
}
$form['table'][] = $row;
}
$form['pager'] = array(
'#type' => 'pager'
);
$form['data_info'] = DataSource::getDataInfo();
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$keys = ['error', 'names', 'category', 'topx'];
$submitted = array_filter($form_state->getValues());
$query = [];
foreach ($keys as $key) {
if (!empty($submitted[$key])) {
$query[$key] = $submitted[$key];
}
}
$form_state->setRedirect('deprecation_status.errors_form', [], ['query' => $query]);
}
/**
* Custom table sorting based on stored sorting criteria.
*/
protected function tableSort($a, $b) {
if ($a[$this->column] == $b[$this->column]) {
return 0;
}
$cmp = ($a[$this->column] < $b[$this->column]) ? -1 : 1;
return ($this->sort == 'desc') ? $cmp * -1 : $cmp;
}
/**
* Custom table filter based on form values.
*/
protected function tableFilter($errors) {
$keys = ['error', 'names', 'category', 'topx'];
foreach ($keys as $key) {
$$key = @$_GET[$key];
}
// Process project name matching for preg_match() later.
$name_list = [];
if (!empty($names)) {
$all_names = explode(',', $names);
foreach($all_names as $one_name) {
$parts = explode('*', trim($one_name));
$processed_parts = [];
foreach ($parts as $part) {
$processed_parts[] = preg_quote($part, '!');
}
$name_list[] = join('.*', $processed_parts);
}
}
// Process error message matching for preg_match() later.
$message = '';
if (!empty($error)) {
$parts = explode('*', trim($error));
$processed_parts = [];
foreach ($parts as $part) {
$processed_parts[] = preg_quote($part, '!');
}
$message = join('.*', $processed_parts);
}
$filtered = [];
foreach ($errors as $index => $error) {
// Assume this row is a match.
$match = TRUE;
$counts = [];
// Go through each filter and if the row is still a match, see if this
// filter will invalidate it.
if (!empty($message) && $match) {
$match = preg_match("!^$message$!", $error[4]);
}
if (!empty($category) && $match) {
$match = ($error[3] == $category);
}
if (!empty($topx) && $match) {
if ($error[2] <= $topx) {
// If the group was a match for the topmost group, extract affected projects and counts.
$projects = explode(', ', $error[5]);
foreach ($projects as $item) {
if ($topx == 50 && preg_match('!\[50\] \((\d+)\)$!', $item, $found)) {
$counts[$item] = $found[1];
}
if ($topx == 100 && preg_match('!\[(50|100)\] \((\d+)\)$!', $item, $found)) {
$counts[$item] = $found[2];
}
if ($topx == 200 && preg_match('!\[(50|100|200)\] \((\d+)\)$!', $item, $found)) {
$counts[$item] = $found[2];
}
if ($topx == 500 && preg_match('!\[(50|100|200|500)\] \((\d+)\)$!', $item, $found)) {
$counts[$item] = $found[2];
}
if ($topx == 1000 && preg_match('!\[(50|100|200|500|1000)\] \((\d+)\)$!', $item, $found)) {
$counts[$item] = $found[2];
}
}
}
else {
// The group was not a match.
$match = FALSE;
}
}
if (!empty($name_list) && $match) {
$error_projects = explode(', ', $error[5]);
// Assume match will become false and only set to TRUE if explicit.
$match = FALSE;
foreach ($error_projects as $item) {
list($error_project, $error_data) = explode(' [', $item);
foreach($name_list as $name) {
if (preg_match("!^$name$!", $error_project)) {
list($nil, $error_number1) = explode('(', $error_data);
list($error_number) = explode(')', $error_number1);
$counts[$item] = $error_number;
$match = TRUE;
break;
}
}
}
}
// If the row was still a match, continue.
if ($match) {
$error[] = $index;
if (!empty($names) || !empty($topx)) {
$error[] = (int) array_sum($counts);
$error[] = (int) count($counts);
}
$filtered[] = $error;
}
}
return $filtered;
}
/**
* Summarize results from $projects for charts.
*
* @param array $projects
* Project data array
*
* @return array
* Summary array with project, total and errors / next step breakdown.
*/
protected function dataSummary($errors) {
$summary = [
'total' => 0,
'errors' => [
'Info.yml or composer.json' => 0,
'Drupal API, rector covered' => 0,
'Drupal API, not rector covered' => 0,
'Symfony API' => 0,
'Twig API' => 0,
'PHPUnit API' => 0,
'Guzzle API' => 0,
'Frontend API' => 0,
'Other problem' => 0,
'Parse error' => 0,
],
'total_drupal' => 0,
'drupal' => [
'Drupal 8 deprecated API' => 0,
'Drupal 9 deprecated API' => 0,
]
];
foreach($errors as $error) {
//$this_count = !empty($error[6]) ? $error[6] : $error[0];
$summary['total'] += $error[0];
$summary['errors'][$error[3]] += $error[0];
if (preg_match('!(in|iin|as of) [Dd]rupal[ :]([89]).[0-9x]!', $error[4], $major_deprecated_version)) {
$summary['total_drupal'] += $error[0];
$summary['drupal']['Drupal ' . $major_deprecated_version[2] . ' deprecated API'] += $error[0];
}
}
// Update error results with percentages in labels.
$errors = [];
foreach ($summary['errors'] as $key => $result) {
$percentage = !empty($summary['total']) ? round($result / ($summary['total'] / 100), 2) : 0;
$errors[$key . ' (' . $percentage . '%)'] = $result;
}
$summary['errors'] = $errors;
// Update Drupal results with percentages in labels.
if ($summary['total_drupal'] > 0) {
$errors = [];
foreach ($summary['drupal'] as $key => $result) {
$percentage = round($result / ($summary['total_drupal'] / 100), 2);
$errors[$key . ' (' . $percentage . '%)'] = $result;
}
$summary['drupal'] = $errors;
}
return $summary;
}
}
