class ScanResultFormatter in Upgrade Status 8.2
Same name and namespace in other branches
- 8.3 src/ScanResultFormatter.php \Drupal\upgrade_status\ScanResultFormatter
- 8 src/ScanResultFormatter.php \Drupal\upgrade_status\ScanResultFormatter
Format scan results for display or export.
Hierarchy
- class \Drupal\upgrade_status\ScanResultFormatter uses StringTranslationTrait
Expanded class hierarchy of ScanResultFormatter
3 files declare their use of ScanResultFormatter
- ScanResultController.php in src/
Controller/ ScanResultController.php - UpgradeStatusCommands.php in src/
Commands/ UpgradeStatusCommands.php - UpgradeStatusForm.php in src/
Form/ UpgradeStatusForm.php
1 string reference to 'ScanResultFormatter'
1 service uses ScanResultFormatter
File
- src/
ScanResultFormatter.php, line 17
Namespace
Drupal\upgrade_statusView source
class ScanResultFormatter {
use StringTranslationTrait;
/**
* Upgrade status scan result storage.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $scanResultStorage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a \Drupal\upgrade_status\Controller\ScanResultFormatter.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key/value factory.
* @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
* The date formatter service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(KeyValueFactoryInterface $key_value_factory, DateFormatterInterface $dateFormatter, TimeInterface $time, ModuleHandlerInterface $module_handler) {
$this->scanResultStorage = $key_value_factory
->get('upgrade_status_scan_results');
$this->dateFormatter = $dateFormatter;
$this->time = $time;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container
->get('keyvalue'), $container
->get('date.formatter'), $container
->get('datetime.time'), $container
->get('module_handler'));
}
/**
* Get scanning result for an extension.
*
* @param \Drupal\Core\Extension\Extension $extension
* Drupal extension object.
* @return null|array
* Scan results array or null if no scan results are saved.
*/
public function getRawResult(Extension $extension) {
$scan_results = $this->scanResultStorage
->get($extension
->getName());
if (!empty($scan_results)) {
$scan_results = json_decode($scan_results, TRUE);
}
return $scan_results;
}
/**
* Format results output for an extension.
*
* @param \Drupal\Core\Extension\Extension $extension
* Drupal extension object.
*
* @return array
* Build array.
*/
public function formatResult(Extension $extension) {
$result = $this
->getRawResult($extension);
$info = $extension->info;
$label = $info['name'] . (!empty($info['version']) ? ' ' . $info['version'] : '');
// This project was not yet scanned or the scan results were removed.
if (empty($result)) {
return [
'#title' => $label,
'result' => [
'#type' => 'markup',
'#markup' => $this
->t('No deprecation scanning data available. <a href="@url">Go to the Upgrade Status form</a>.', [
'@url' => Url::fromRoute('upgrade_status.report')
->toString(),
]),
],
];
}
if (isset($result['data']['totals'])) {
$project_error_count = $result['data']['totals']['file_errors'];
}
else {
$project_error_count = 0;
}
$build = [
'#attached' => [
'library' => [
'upgrade_status/upgrade_status.admin',
],
],
'#title' => $label,
'date' => [
'#type' => 'markup',
'#markup' => '<div class="list-description">' . $this
->t('Scanned on @date.', [
'@date' => $this->dateFormatter
->format($result['date']),
]) . '</div>',
'#weight' => -10,
],
];
if (!empty($result['plans'])) {
$build['plans'] = [
'#type' => 'markup',
'#markup' => '<div class="list-description">' . $result['plans'] . '</div>',
'#weight' => 50,
];
}
// If this project had no known issues found, report that.
if ($project_error_count === 0) {
$build['data'] = [
'#type' => 'markup',
'#markup' => $this
->t('No known issues found.'),
'#weight' => 5,
];
return $build;
}
// Otherwise prepare list of errors in groups.
$groups = [];
foreach ($result['data']['files'] as $filepath => $errors) {
foreach ($errors['messages'] as $error) {
// Remove the Drupal root directory. If this is a composer setup, then
// the webroot is in a web/ directory, add that back in for easy path
// copy-pasting.
$short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath);
if (preg_match('!/web$!', DRUPAL_ROOT)) {
$short_path = 'web/' . $short_path;
}
// Allow paths and namespaces to wrap. Emphasize filename as it may
// show up in the middle of the info
$short_path = str_replace('/', '/<wbr>', $short_path);
if (strpos($short_path, 'in context of')) {
$short_path = preg_replace('!/([^/]+)( \\(in context of)!', '/<strong>\\1</strong>\\2', $short_path);
$short_path = str_replace('\\', '\\<wbr>', $short_path);
}
else {
$short_path = preg_replace('!/([^/]+)$!', '/<strong>\\1</strong>', $short_path);
}
// @todo could be more accurate with reflection but not sure it is even possible as the reflected
// code may not be in the runtime at this point (eg. functions in include files)
// see https://www.php.net/manual/en/reflectionfunctionabstract.getfilename.php
// see https://www.php.net/manual/en/reflectionclass.getfilename.php
// Link to documentation for a function in this specific Drupal version.
$api_version = preg_replace('!^(8\\.\\d+)\\..+$!', '\\1', \Drupal::VERSION) . '.x';
$api_link = 'https://api.drupal.org/api/drupal/' . $api_version . '/search/';
$formatted_error = preg_replace('!deprecated function ([^(]+)\\(\\)!', 'deprecated function <a target="_blank" href="' . $api_link . '\\1">\\1()</a>', $error['message']);
// Replace deprecated class links.
if (preg_match('!class (Drupal\\\\\\S+)\\.( |$)!', $formatted_error, $found)) {
if (preg_match('!Drupal\\\\([a-z_0-9A-Z]+)\\\\(.+)$!', $found[1], $namespace)) {
$path_parts = explode('\\', $namespace[2]);
$class = array_pop($path_parts);
if (in_array($namespace[1], [
'Component',
'Core',
])) {
$class_file = 'core!lib!Drupal!' . $namespace[1];
}
elseif (in_array($namespace[1], [
'KernelTests',
'FunctionalTests',
'FunctionalJavascriptTests',
'Tests',
])) {
$class_file = 'core!tests!Drupal!' . $namespace[1];
}
else {
$class_file = 'core!modules!' . $namespace[1] . '!src';
}
if (count($path_parts)) {
$class_file .= '!' . join('!', $path_parts);
}
$class_file .= '!' . $class . '.php';
$api_link = 'https://api.drupal.org/api/drupal/' . $class_file . '/class/' . $class . '/' . $api_version;
$formatted_error = str_replace($found[1], '<a target="_blank" href="' . $api_link . '">' . $found[1] . '</a>', $formatted_error);
}
}
// Allow error messages to wrap.
$formatted_error = str_replace('\\', '\\<wbr>', $formatted_error);
// Make drupal.org documentation links clickable.
$formatted_error = preg_replace('!See (https://drupal.org(.\\S+)).$!', 'See <a href="\\1">\\1<a>.', $formatted_error);
// Format core_version_requirement message.
$formatted_error = preg_replace('!(core_version_requirement: .+) (to designate|is not)!', '<code>\\1</code> \\2', $formatted_error);
$category = 'uncategorized';
if (!empty($error['upgrade_status_category'])) {
if (in_array($error['upgrade_status_category'], [
'safe',
'old',
])) {
$category = 'now';
}
else {
$category = $error['upgrade_status_category'];
}
}
@($groups[$category][] = [
'filename' => [
'#type' => 'markup',
'#markup' => $short_path,
'#wrapper_attributes' => [
'class' => [
'status-info',
],
],
],
'line' => [
'#type' => 'markup',
'#markup' => $error['line'],
],
'issue' => [
'#type' => 'markup',
'#markup' => $formatted_error,
],
]);
}
}
$build['groups'] = [
'#weight' => 100,
];
$group_help = [
'rector' => [
$this
->t('Fix now with automation'),
'rector-covered',
$this
->t('Avoid some manual work by using <a href="@drupal-rector">drupal-rector to fix issues automatically</a> or <a href="@upgrade-rector">Upgrade Rector to generate patches</a>.', [
'@drupal-rector' => 'https://www.drupal.org/project/rector',
'@upgrade-rector' => 'https://www.drupal.org/project/upgrade_rector',
]),
],
'now' => [
$this
->t('Fix now manually'),
'known-errors',
$this
->t('It does not seem like these are covered by automation yet. <a href="@drupal-rector">Contribute to drupal-rector to provide coverage</a>. Fix manually in the meantime.', [
'@drupal-rector' => 'https://www.drupal.org/project/rector',
]),
],
'uncategorized' => [
$this
->t('Check manually'),
'known-warnings',
$this
->t('Errors without Drupal source version numbers including parse errors and use of APIs from dependencies.'),
],
'later' => [
$this
->t('Fix later'),
'known-later',
// Issues to fix later need different guidance based on whether they
// were found in a contributed project or a custom project.
!empty($extension->info['project']) ? $this
->t('Based on the Drupal deprecation version number of these, fixing them may make the contributed project incompatible with supported Drupal core versions.') : $this
->t('Based on the Drupal deprecation version number of these, fixing them will likely make them incompatible with your current Drupal version.'),
],
'ignore' => [
$this
->t('Ignore'),
'known-ignore',
$this
->t('Deprecated API use for APIs removed in future Drupal major versions is not required to fix yet.'),
],
];
foreach ($group_help as $group_key => $group_info) {
if (empty($groups[$group_key])) {
// Skip this group if there was no error to display.
continue;
}
$build['groups'][$group_key] = [
'title' => [
'#type' => 'markup',
'#markup' => '<h3 class="upgrade-status-group">' . $group_info[0] . '</h3>',
],
'description' => [
'#type' => 'markup',
'#markup' => '<div class="upgrade-status-description">' . $group_info[2] . '</div>',
],
'errors' => [
'#type' => 'table',
'#attributes' => [
'class' => [
'upgrade-status-error-list',
],
],
'#header' => [
'filename' => $this
->t('File name'),
'line' => $this
->t('Line'),
'issue' => $this
->t('Error'),
],
],
];
foreach ($groups[$group_key] as $item) {
$item['#attributes']['class'] = [
$group_info[1],
];
$build['groups'][$group_key]['errors'][] = $item;
}
// All modules (thinking of Upgrade Rector here primarily) to alter
// results display.
$this->moduleHandler
->alter('upgrade_status_result', $build['groups'][$group_key], $extension, $group_key);
}
$summary = [];
if (!empty($result['data']['totals']['upgrade_status_split']['error'])) {
$summary[] = $this
->formatPlural($result['data']['totals']['upgrade_status_split']['error'], '@count error found.', '@count errors found.');
}
if (!empty($result['data']['totals']['upgrade_status_split']['warning'])) {
$summary[] = $this
->formatPlural($result['data']['totals']['upgrade_status_split']['warning'], '@count warning found.', '@count warnings found.');
}
$build['summary'] = [
'#type' => '#markup',
'#markup' => '<div class="list-description">' . join(' ', $summary) . '</div>',
'#weight' => 5,
];
$build['export'] = [
'#type' => 'link',
'#title' => $this
->t('Export as HTML'),
'#name' => 'export',
'#url' => Url::fromRoute('upgrade_status.export', [
'type' => $extension
->getType(),
'project_machine_name' => $extension
->getName(),
'format' => 'html',
]),
'#attributes' => [
'class' => [
'button',
'button--primary',
],
],
'#weight' => 200,
];
$build['export_ascii'] = [
'#type' => 'link',
'#title' => $this
->t('Export as text'),
'#name' => 'export_ascii',
'#url' => Url::fromRoute('upgrade_status.export', [
'type' => $extension
->getType(),
'project_machine_name' => $extension
->getName(),
'format' => 'ascii',
]),
'#attributes' => [
'class' => [
'button',
'button--primary',
],
],
'#weight' => 200,
];
return $build;
}
/**
* Format results output for an extension as ASCII.
*
* @return array
* Build array.
*/
public function formatAsciiResult(Extension $extension) {
$result = $this
->getRawResult($extension);
$info = $extension->info;
$label = $info['name'] . (!empty($info['version']) ? ' ' . $info['version'] : '');
// This project was not yet scanned or the scan results were removed.
if (empty($result)) {
return [
'#title' => $label,
'data' => [
'#type' => 'markup',
'#markup' => $this
->t('No deprecation scanning data available.'),
],
];
}
if (isset($result['data']['totals'])) {
$project_error_count = $result['data']['totals']['file_errors'];
}
else {
$project_error_count = 0;
}
$build = [
'#title' => $label,
'date' => [
'#type' => 'markup',
'#markup' => wordwrap($this
->t('Scanned on @date.', [
'@date' => $this->dateFormatter
->format($result['date']),
]), 80, "\n", true),
'#weight' => -10,
],
];
if (!empty($result['plans'])) {
$build['plans'] = [
'#type' => 'markup',
'#markup' => wordwrap(strip_tags($result['plans']), 80, "\n", true),
'#weight' => 50,
];
}
// If this project had no known issues found, report that.
if ($project_error_count === 0) {
$build['data'] = [
'#type' => 'markup',
'#markup' => $this
->t('No known issues found.'),
'#weight' => 5,
];
return $build;
}
// Otherwise prepare list of errors in tables.
$tables = '';
$hasFixRector = FALSE;
foreach ($result['data']['files'] as $filepath => $errors) {
// Remove the Drupal root directory name. If this is a composer setup,
// then the webroot is in a web/ directory, add that back in for easy
// path copy-pasting.
$short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath);
if (preg_match('!/web$!', DRUPAL_ROOT)) {
$short_path = 'web/' . $short_path;
}
$short_path = wordwrap($short_path, 80, "\n", TRUE);
$tables .= $short_path . ":\n";
$table = [];
foreach ($errors['messages'] as $error) {
$level_label = $this
->t('Check manually');
if (!empty($error['upgrade_status_category'])) {
if ($error['upgrade_status_category'] == 'ignore') {
$level_label = $this
->t('Ignore');
}
elseif ($error['upgrade_status_category'] == 'later') {
$level_label = $this
->t('Fix later');
}
elseif (in_array($error['upgrade_status_category'], [
'safe',
'old',
])) {
$level_label = $this
->t('Fix now');
}
elseif ($error['upgrade_status_category'] == 'rector') {
$level_label = $this
->t('Fix with rector');
$hasFixRector = TRUE;
}
}
$message = str_replace("\n", ' ', $error['message']);
$table[] = [
'status' => wordwrap($level_label, 8, "\n", true),
'line' => wordwrap($error['line'], 7, "\n", true),
'message' => wordwrap($message . "\n", 60, "\n", true),
];
}
$asciiRenderer = new ArrayToTextTable($table);
$tables .= $asciiRenderer
->getTable() . "\n";
}
$build['data'] = $tables;
$summary = [];
if (!empty($result['data']['totals']['upgrade_status_split']['error'])) {
$summary[] = $this
->formatPlural($result['data']['totals']['upgrade_status_split']['error'], '@count error found.', '@count errors found.');
}
if (!empty($result['data']['totals']['upgrade_status_split']['warning'])) {
$summary[] = $this
->formatPlural($result['data']['totals']['upgrade_status_split']['warning'], '@count warning found.', '@count warnings found.');
}
if ($hasFixRector) {
$summary[] = $this
->t('Avoid some manual work by using drupal-rector for fixing issues automatically or Upgrade Rector to generate patches.');
}
$build['summary'] = [
'#type' => '#markup',
'#markup' => wordwrap(join(' ', $summary), 80, "\n", true),
'#weight' => 5,
];
return $build;
}
/**
* Format date/time.
*
* @param int $time
* (optional) Timestamp. Current time used if not specified.
* @param string $format
* (optional) Format identifier. Default format is used it not specified.
*
* @return string
* Formatted date/time.
*/
public function formatDateTime($time = 0, $format = '') {
if (empty($time)) {
$time = $this->time
->getCurrentTime();
}
return $this->dateFormatter
->format($time, $format);
}
}
Members
Name![]() |
Modifiers | Type | Description | Overrides |
---|---|---|---|---|
ScanResultFormatter:: |
protected | property | The date formatter service. | |
ScanResultFormatter:: |
protected | property | The module handler. | |
ScanResultFormatter:: |
protected | property | Upgrade status scan result storage. | |
ScanResultFormatter:: |
protected | property | The time service. | |
ScanResultFormatter:: |
public static | function | ||
ScanResultFormatter:: |
public | function | Format results output for an extension as ASCII. | |
ScanResultFormatter:: |
public | function | Format date/time. | |
ScanResultFormatter:: |
public | function | Format results output for an extension. | |
ScanResultFormatter:: |
public | function | Get scanning result for an extension. | |
ScanResultFormatter:: |
public | function | Constructs a \Drupal\upgrade_status\Controller\ScanResultFormatter. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |