deprecation_status-8.x-3.x-dev/src/Form/SingleProjectForm.php
src/Form/SingleProjectForm.php
<?php
namespace Drupal\deprecation_status\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\Core\Utility\TableSort;
use Drupal\deprecation_status\DataSource;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SingleProjectForm 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_project_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $machine_name = NULL) {
// Find this single project if present.
$project = [];
$file = fopen(DataSource::getFullPath('projects_detail.csv'), 'r');
$i = 0;
while ($line = fgetcsv($file, 0, ";")) {
// Skip the header row.
if ($i > 0 && strpos($line[0], $machine_name . ' ') === 0) {
$project = $line;
break;
}
$i++;
}
fclose($file);
// Throw a 404 if this project was not found.
if (empty($project)) {
throw new NotFoundHttpException();
}
// "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
$form['#cache']['tags'][] = 'deprecation_status';
$form['machine_name'] = [
'#type' => 'value',
'#value' => $machine_name,
];
$form['#attached']['library'][] = 'deprecation_status/deprecation_status.lists';
$form['#title'] = $this->t('@name Drupal 10 deprecation details', ['@name' => $project[0]]);
$form['#attached'] = ['library' => ['deprecation_status/deprecation_status.lists']];
// Add high level summary table.
list($machine_name) = explode(' ', $project[0]);
$columns = [
'name' => 'Name and release',
'type' => 'Project type',
'projects' => 'Usage count',
'top' => 'Top X project by usage',
'maintainers' => 'Maintainers',
];
$form['summary'] = [
'#type' => 'table',
'#header' => $columns,
'#rows' => [
[
[
'data' => [
'#type' => 'markup',
'#markup' => '<a href="https://drupal.org/project/' . $machine_name . '">' . $project[0] . '</a>',
]
],
$project[3],
$project[4],
$project[5],
$project[24]
]
],
];
// Read the errors related to this project.
// "Occurence count";"Affected project count";"Occurs in a top X project by usage";
// Category;Error;"List of projects [group] (occurences)"
$errors = [];
$file = fopen(DataSource::getFullPath('errors_detail.csv'), 'r');
$i = 0;
while ($line = fgetcsv($file, 0, ";")) {
// Skip the header row.
if ($i > 0) {
$this_project_count = 0;
$error_projects = explode(', ', $line[5]);
foreach ($error_projects as $error_project) {
if (strpos($error_project, $machine_name . ' ') === 0) {
if (preg_match('!\((\d+)\)$!', $error_project, $count)) {
$this_project_count = (int) $count[1];
}
}
}
if ($this_project_count > 0) {
$errors[] = [$this_project_count, $line[0], $line[1], $line[3], $line[4], $i - 1];
}
}
$i++;
}
fclose($file);
$file = fopen(DataSource::getFullPath('next_steps_help.csv'), 'r');
$step_help = '';
$i = 0;
while ($line = fgetcsv($file, 0, ";")) {
if ($i > 0) {
if ($line[0] == '-- ' . $project[2]) {
$step_help = $line[1];
break;
}
}
$i++;
}
fclose($file);
$project_class = 'warning';
if ($project[1] == 'Resolve pre-scanning errors') {
$project_class = 'unknown';
}
elseif ($project[1] == 'Release as Drupal 10-ready') {
$project_class = 'passed';
}
elseif (in_array($project[2], ['Run Rector to fix all errors', 'Run Rector to fix some errors', 'Manually review and fix errors'])) {
$project_class = 'error';
}
$next_step_columns = [
'status' => 'Status',
'next_step' => 'Next step',
'help' => 'Instructions',
'issues' => 'Drupal.org issues',
];
$form['next_step'] = [
'#type' => 'table',
'#header' => $next_step_columns,
'#attributes' => ['class' => ['single-project-table']],
];
$issue_link = '<a href="https://drupal.org/project/issues/search/' . $machine_name . '?issue_tags=Drupal+10+compatibility">Drupal 10 compatibility issues</a>';
$row = [
'#attributes' => ['class' => ['item-' . $project_class]],
'status' => ['data' => ['#markup' => '<span class="category">' . $project[1] . '</span>']],
'next_step' => ['data' => ['#markup' => $project[2]]],
'help' => ['data' => ['#markup' => $step_help]],
'issues' => ['data' => ['#markup' => $issue_link]],
];
if ($project[1] == 'Resolve pre-scanning errors') {
$form['next_step']['#header']['link'] = 'Error log';
$row['link'] = [
'data' => [
'#markup' => '<a href="' . $project[23] . '">' . $this->t('Read the error log') . '</a>',
],
];
}
$form['next_step'][] = $row;
$form['data_info'] = DataSource::getDataInfo();
// If there are pre-scanning errors or no errors, no need to display
// an error list/filter since it would always be empty.
if ($project_class == 'unknown' || $project_class == 'passed') {
return $form;
}
$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']['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']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
// Add error listing table.
$columns = [
'occurrence_project' => 'Occurrence in project',
'occurrence_all' => 'All occurrences',
'projects' => 'Affected projects',
'category' => 'Category',
'error' => 'Error',
];
// Add sorting information to columns.
$i = 0;
foreach ($columns as $name => &$column) {
$column = ['data' => $column, 'field' => $i];
$i++;
}
// Sort by occurrences descending initially.
$columns['occurrence_project']['sort'] = 'desc';
$form['errors'] = [
'#type' => 'table',
'#header' => $columns,
'#empty' => t('No matching errors found. Adjust filters.'),
];
$errors = $this->tableFilter($errors);
// 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($errors, [$this, 'tableSort']);
foreach ($errors as $error) {
$i = $error[5];
$error = array_slice($error, 0, 5);
$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]],
'occurrence_project' => ['#type' => 'markup', '#markup' => $error[0]],
'occurrence_all' => ['#type' => 'markup', '#markup' => $error[1]],
'projects' => [
'data' => [
'#type' => 'link',
'#title' => $error[2],
'#url' => Url::fromRoute('deprecation_status.error', ['error_index' => $i]),
],
],
'category' => ['#type' => 'markup', '#markup' => '<span class="category">' . $error[3] . '</span>'],
'message' => ['#type' => 'markup', '#markup' => DataSource::formatError($error[4])],
];
$form['errors'][] = $row;
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$keys = ['error', 'category'];
$submitted = $form_state->getValues();
$query = [];
foreach ($keys as $key) {
if (!empty($submitted[$key])) {
$query[$key] = $submitted[$key];
}
}
$form_state->setRedirect('deprecation_status.project', ['machine_name' => $submitted['machine_name']], ['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', 'category'];
foreach ($keys as $key) {
$$key = @$_GET[$key];
}
// 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;
// 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 the row was still a match, continue.
if ($match) {
$filtered[] = $error;
}
}
return $filtered;
}
}
