deprecation_status-8.x-3.x-dev/src/Form/ProjectsForm.php
src/Form/ProjectsForm.php
<?php
namespace Drupal\deprecation_status\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\Utility\TableSort;
use Drupal\deprecation_status\DataSource;
use Symfony\Component\HttpFoundation\RedirectResponse;
class ProjectsForm 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_projects_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Get dataset metadata for outside links to error logs.
// list ($data_id, $data_date) = DataSource::getMetaData();
// $target_version_result_url = 'https://dispatcher.drupalci.org/job/project_analysis_d10/' . $data_id;
$form['#cache']['tags'][] = 'deprecation_status';
$form['#attached']['library'][] = 'deprecation_status/deprecation_status.lists';
$form['filters'] = [
'#type' => 'details',
'#title' => $this->t('Filter projects'),
'#attributes' => ['class' => ['container-inline']],
'#open' => TRUE,
];
$form['filters']['names'] = [
'#title' => 'Names',
'#type' => 'textfield',
'#default_value' => @$_GET['names'],
'#attributes' => ['placeholder' => "Use *, eg. 'commerce*'"],
'#size' => 20,
];
$form['filters']['next_step'] = [
'#title' => 'Next step',
'#type' => 'select',
'#options' => [
'Release as Drupal 10-ready' => 'Release as Drupal 10-ready',
'-- Stable, congrats' => '-- Stable, congrats',
'-- Improve stability of the release' => '-- Improve stability of the release',
'-- Make tagged release available' => '-- Make tagged release available',
'Fix deprecation errors found' => 'Fix deprecation errors found',
'-- Fix info.yml/composer.json errors found' => '-- Fix info.yml/composer.json errors found',
'-- Run Rector to fix all errors' => '-- Run Rector to fix all errors',
'-- Run Rector to fix some errors' => '-- Run Rector to fix some errors',
'-- Manually review and fix errors' => '-- Manually review and fix errors',
'Resolve pre-scanning errors' => 'Resolve pre-scanning errors',
'-- Resolve fatal error from PHPStan' => '-- Resolve fatal error from PHPStan',
'-- Fix invalid PHPStan results' => '-- Fix invalid PHPStan results',
'-- Fix malformed info.yml file' => '-- Fix malformed info.yml file',
'-- Fix machine name issue' => '-- Fix machine name issue',
'-- Fix composer installation issue' => '-- Fix composer installation issue',
'-- Fix PHP fatal error' => '-- Fix PHP fatal error',
'-- Fix PHP environment constraints if possible' => '-- Fix PHP environment constraints if possible',
'-- Check uncategorized error leading to missing results' => '-- Check uncategorized error leading to missing results',
'-- Check uncategorized error leading to empty results' => '-- Check uncategorized error leading to empty results',
],
'#default_value' => @$_GET['next_step'],
'#required' => FALSE,
'#empty_option' => '- No filter -',
];
$form['filters']['maintainer'] = [
'#title' => 'Maintainer',
'#type' => 'textfield',
'#default_value' => @$_GET['maintainer'],
'#attributes' => ['placeholder' => "eg. Dries"],
'#size' => 20,
];
$form['filters']['type'] = [
'#title' => 'Type',
'#type' => 'select',
'#options' => [
'Module' => 'Module',
'Theme' => 'Theme',
'Theme_engine' => 'Theme engine',
'Distribution' => 'Distribution',
],
'#default_value' => @$_GET['type'],
'#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']['plan'] = [
'#title' => 'Upgrade plan',
'#type' => 'select',
'#options' => ['none' => 'None', 'specified' => 'Specified'],
'#default_value' => @$_GET['plan'],
'#required' => FALSE,
'#empty_option' => '- No filter -',
];
$form['filters']['patch'] = [
'#title' => 'Rector patch',
'#type' => 'select',
'#options' => ['none' => 'None', 'available' => 'Available'],
'#default_value' => @$_GET['patch'],
'#required' => FALSE,
'#empty_option' => '- No filter -',
];*/
$form['filters']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
// Read all projects into one big array.
$projects = [];
$file = fopen(DataSource::getFullPath('projects_detail.csv'), 'r');
$i = 0;
while ($line = fgetcsv($file, 0, ";")) {
// Skip the header row.
if ($i > 0) {
$projects[] = $line;
}
$i++;
}
fclose($file);
/*
"Name and release";"General next step";"Specific next step";"Project type";
"Usage count";"Top X by usage";"Highest compatible stability level";
"Highest compatible stability version";"Total errors";"Total parse errors";
"Total deprecated API uses";"Drupal API, rector covered";
"Drupal API, not rector covered";"Symfony API";"Twig API";"PHPUnit API";
"Guzzle API";"Frontend API";"Info.yml or composer.json";"Other problem";
"Drupal 8 API deprecations";"Upgrade plan";"Rector patchfile";
"Pre-scan error log";Maintainers
*/
// Set up columns for the table.
$columns = [
'name' => ['data' => 'Name and release', 'field' => 0],
'status' => ['data' => 'Status', 'field' => 1],
'next_step' => ['data' => 'Next step', 'field' => 2],
'usage' => ['data' => 'Usage', 'field' => 4, 'sort' => 'desc'],
'topx' => ['data' => 'Top X by usage', 'field' => 5],
'total' => ['data' => 'Total errors', 'field' => 8],
'issues' => ['data' => 'Drupal.org issues'],
];
// Sort the data array based on query tablesort information.
$order = TableSort::getOrder($columns, \Drupal::request());
$this->column = $order['sql'];
$this->sort = TableSort::getSort($columns, \Drupal::request());
uasort($projects, [$this, 'tableSort']);
$common_script = <<<SCRIPTEND
legend: {
display: true,
position: window.innerWidth < 800 ? 'top' : 'right',
onClick: null
}
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="totalProjects"></canvas>
<canvas id="totalDeprecations"></canvas>
</div>
<script type="text/javascript">
var ctx = document.getElementById('totalProjects').getContext('2d');
var chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [@totalProjectsLabels],
datasets: [{
data: [@totalProjectsData],
backgroundColor: ["#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#9B2226"]
}]
},
options: {
aspectRatio: window.innerWidth < 800 ? 1.5 : 2,
responsive: false,
plugins: {
title: {
display: true,
text: '@projectcount projects by next steps',
position: 'bottom'
},
$common_script
}
}
});
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'
},
$common_script
}
}
});
</script>
SCRIPTEND
];
// Filter projects down based on form input. Add data summaries of overall
// results or filtered results.
$projects = $this->tableFilter($projects);
if (count($projects) == 0) {
unset($form['chart']);
}
else {
$summary = $this->dataSummary($projects);
$project_labels = "'" . join("','", array_keys($summary['results'])) . "'";
$project_data = join(',', array_values($summary['results']));
$deprecation_labels = "'" . join("','", array_keys($summary['errors'])) . "'";
$deprecation_data = join(',', array_values($summary['errors']));
$deprecation_count = $summary['total'];
$form['chart']['#children'] = str_replace(
[
'@projectcount', '@totalProjectsLabels', '@totalProjectsData',
'@deprecatedcount', '@totalDeprecationsLabels', '@totalDeprecationsData'
],
[
count($projects), $project_labels, $project_data,
$deprecation_count, $deprecation_labels, $deprecation_data
],
$form['chart']['#children']
);
}
if (!empty(@$_GET['next_step'])) {
// Get next steps descriptions to display.
$file = fopen(DataSource::getFullPath('next_steps_help.csv'), 'r');
$step_help = '';
$i = 0;
while ($line = fgetcsv($file, 0, ";")) {
if ($i > 0 && $line[0] == $_GET['next_step']) {
$step_help = $line[1];
}
$i++;
}
fclose($file);
// Add a step help element if we found the help. This also validates the
// GET value, so we can use it without fear of XSS.
if (!empty($step_help)) {
$form['step_help'] = [
'#type' => 'markup',
'#markup' => '<h2>' . str_replace('-- ', '', $_GET['next_step']) . ': ' . count($projects) . ' projects</h2><p>' . $step_help . '</p>'
];
}
}
// Display the resulting data.
$form['table'] = [
'#type' => 'table',
'#header' => $columns,
'#empty' => t('No matching projects found.'),
];
// Page the results based on query information.
$page = \Drupal::service('pager.parameters')->findPage();
$num_per_page = 30;
\Drupal::service('pager.manager')->createPager(count($projects), $num_per_page);
$offset = $num_per_page * $page;
$project_page = array_slice($projects, $offset, $num_per_page);
// Key results based on header key names.
$keys = array_keys($columns);
foreach ($project_page as $project) {
list ($machine_name) = explode(' ', $project[0]);
$info = [
'name' => [
'data' => [
'#type' => 'link',
'#title' => $project[0],
'#url' => Url::fromRoute('deprecation_status.project', ['machine_name' => $machine_name]),
],
],
];
$class = 'warning';
if ($project[1] == 'Resolve pre-scanning errors') {
$class = 'unknown';
}
elseif ($project[1] == 'Release as Drupal 10-ready') {
$class = 'passed';
}
elseif (in_array($project[2], ['Run Rector to fix all errors', 'Run Rector to fix some errors', 'Manually review and fix errors'])) {
$class = 'error';
}
$info['#attributes']['class'] = ['item-' . $class];
$info['status'] = [
'#type' => 'link',
'#prefix' => '<span class="category">',
'#title' => $project[1],
'#url' => Url::fromRoute('deprecation_status.projects_form', [], ['query' => ['next_step' => $project[1]]]),
'#suffix' => '</span>',
];
$info['next_step'] = [
'#type' => 'link',
'#title' => $project[2],
'#url' => Url::fromRoute('deprecation_status.projects_form', [], ['query' => ['next_step' => '-- ' . $project[2]]]),
];
foreach($keys as $key) {
if (!isset($info[$key])) {
$info[$key] = [
'#type' => 'markup',
'#markup' => $project[$columns[$key]['field']],
];
}
}
$info['issues'] = [
'#type' => 'markup',
'#markup' => '<a href="https://drupal.org/project/issues/search/' . $machine_name . '?issue_tags=Drupal+10+compatibility">Compatibility issues</a>',
];
$info['topx'] = [
'#type' => 'link',
'#title' => $project[5],
'#url' => Url::fromRoute('deprecation_status.projects_form', [], ['query' => ['topx' => $project[5]]]),
];
/*// Format drupal.org issue links in plans and fix relative links.
$plan = preg_replace('!(\[#([0-9]+)\])!', '<a target="_blank" href="https://drupal.org/node/\2">\1</a>', $project[21]);
$plan = preg_replace('!href="/!', 'href="https://drupal.org/', $plan);
$info['plan'] = [
'#type' => 'markup',
'#markup' => $plan,
];
$info['total']['#markup'] = '<strong>' . $info['total']['#markup'] . '</strong>';
if (!empty($project[22]) && $project[1] != 'Improve stability of the release') {
$patch_uri = $target_version_result_url . '/artifact/phpstan-results/' . $project[22] . '/*view*' . '/';
$info['patch'] = [
'data' => [
'#type' => 'link',
'#title' => $this->t('Available'),
'#url' => Url::fromUri($patch_uri),
],
];
}
else {
$info['patch'] = [
'#type' => 'markup',
'#markup' => ''
];
}*/
$form['table'][] = $info;
}
$form['pager'] = array(
'#type' => 'pager'
);
$form['data_info'] = DataSource::getDataInfo();
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$keys = ['names', 'next_step', 'type', 'topx', 'maintainer', /*'plan', 'patch'*/];
$submitted = array_filter($form_state->getValues());
$query = [];
foreach ($keys as $key) {
if (!empty($submitted[$key])) {
$query[$key] = $submitted[$key];
}
}
$form_state->setRedirect('deprecation_status.projects_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($projects) {
$keys = ['names', 'next_step', 'type', 'topx', 'maintainer', /*'plan', 'patch'*/];
foreach ($keys as $key) {
$$key = @$_GET[$key];
}
// Process peoject name matching for preg_match() later.
$name_list = [];
if (!empty($names)) {
$all_names = explode(',', $names);
foreach($all_names as $one_name) {
$one_name = trim($one_name);
// If this name does not contain a wildcard and also does not contain a
// target_version number (which should have a dot), then fix it for the user
// and add a space and wildcard at the end. This would turn searches for
// 'google_analytics' into 'google_analytics *' which does match.
if (strpos($one_name, '.') === FALSE && strpos($one_name, '*') === FALSE) {
$one_name .= ' *';
}
$parts = explode('*', trim($one_name));
$processed_parts = [];
foreach ($parts as $part) {
$processed_parts[] = preg_quote($part, '!');
}
$name_list[] = join('.*', $processed_parts);
}
}
$filtered = [];
foreach ($projects as $project) {
// Assume this row is a match.
$match = TRUE;
// Go through each filter and if the row is still a match, see if this
// filter will invalidate it.
if (!empty($name_list) && $match) {
$one_match = FALSE;
foreach($name_list as $name) {
if (preg_match("!^$name$!", $project[0])) {
$one_match = TRUE;
break;
}
}
$match = $one_match;
}
if (!empty($next_step) && $match) {
if (strpos($next_step, '--') === 0) {
$match = ($project[2] == str_replace('-- ', '', $next_step));
}
else {
$match = ($project[1] == $next_step);
}
}
if (!empty($error) && $match) {
$match = ($project[2] == $error);
}
if (!empty($type) && $match) {
$match = ($project[3] == $type);
}
if (!empty($topx) && $match) {
$match = ($project[5] <= $topx);
}
if (!empty($maintainer) && $match) {
// "Berdir, Dave Reid, eaton, fago, greggles, mikeryan, tsvenson"
$regex = preg_quote($maintainer, '!');
$match = preg_match('!(^|, )' . $maintainer . '($|, )!', $project[24]);
}
/*if (!empty($plan) && $match) {
$match = ($plan == 'none' ? empty($project[21]) : !empty($project[21]));
}
if (!empty($patch) && $match) {
$match = ($patch == 'none' ? empty($project[22]) : !empty($project[22]) && $project[1] != 'Improve stability of the release');
}*/
// If the row was still a match, continue.
if ($match) {
$filtered[] = $project;
}
}
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($projects) {
$summary = [
'projects' => count($projects),
'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,
],
'results' => [
'Stable, congrats' => 0,
'Improve stability of the release' => 0,
'Make tagged release available' => 0,
'Fix info.yml/composer.json errors found' => 0,
'Run Rector to fix all errors' => 0,
'Run Rector to fix some errors' => 0,
'Manually review and fix errors' => 0,
'Resolve pre-scanning errors' => 0,
],
];
foreach($projects as $project) {
if ($project[1] == 'Resolve pre-scanning errors') {
$summary['results'][$project[1]]++;
}
else {
$summary['results'][$project[2]]++;
}
if ($project[1] != 'Release as Drupal 10-ready') {
$summary['total'] += $project[8];
$summary['errors']['Info.yml or composer.json'] += $project[18];
$summary['errors']['Drupal API, rector covered'] += $project[11];
$summary['errors']['Drupal API, not rector covered'] += $project[12];
$summary['errors']['Symfony API'] += $project[13];
$summary['errors']['Twig API'] += $project[14];
$summary['errors']['PHPUnit API'] += $project[15];
$summary['errors']['Guzzle API'] += $project[16];
$summary['errors']['Frontend API'] += $project[17];
$summary['errors']['Other problem'] += $project[19];
$summary['errors']['Parse error'] += $project[9];
}
}
// Update project results with percentages in labels.
$results = [];
foreach ($summary['results'] as $key => $result) {
$percentage = count($projects) ? round($result / (count($projects) / 100), 2) : 0;
$results[$key . ' (' . $percentage . '%)'] = $result;
}
$summary['results'] = $results;
// Update error results with percentages in labels.
if ($summary['total'] > 0) {
$errors = [];
foreach ($summary['errors'] as $key => $result) {
$percentage = round($result / ($summary['total'] / 100), 2);
$errors[$key . ' (' . $percentage . '%)'] = $result;
}
$summary['errors'] = $errors;
}
return $summary;
}
}
