drupalorg-1.0.x-dev/src/Utilities/CoreCompatibility.php
src/Utilities/CoreCompatibility.php
<?php
namespace Drupal\drupalorg\Utilities;
use Drupal\drupalorg\Traits\UpdateXML;
use Drupal\node\Entity\Node;
use Requtize\SemVerConverter\SemVerConverter;
/**
* Utility functions to calculate and populate the core_compatibility values.
*/
class CoreCompatibility {
use UpdateXML;
/**
* Max core version supported for cases where constraints are ">=".
*
* @var string
*/
const MAX_CORE_SUPPORTED = 12;
/**
* Calculate the core compatibility limits for modules.
*
* @param int $updated_since_timestamp
* Calculate only for projects with releases newer than this timestamp.
*/
public static function calculateNewCoreCompatibilities($updated_since_timestamp = NULL) {
$memory_cache = \Drupal::service('entity.memory_cache');
$memory_cache_clear_counter = 0;
$ids = is_null($updated_since_timestamp) ?
self::getAllProjectIds() :
self::getProjectsWithReleasesNewerThan($updated_since_timestamp);
foreach ($ids as $id) {
$project = Node::load($id);
if ($project->hasField('field_project_machine_name')) {
$machine_name = $project->get('field_project_machine_name')->getValue()[0]['value'] ?? NULL;
if (
$machine_name &&
$project->hasField('field_core_semver_minimum') &&
$project->hasField('field_core_semver_maximum')
) {
$min = $project->get('field_core_semver_minimum')->getValue()[0]['value'] ?? PHP_INT_MAX;
$max = $project->get('field_core_semver_maximum')->getValue()[0]['value'] ?? PHP_INT_MIN;
$releases = self::getProjectInformation($machine_name);
if (!empty($releases)) {
$releases = $releases['releases']['release'] ?? [];
foreach ($releases as $release) {
$core_compatibility = $release['core_compatibility'] ?? FALSE;
if (!empty($core_compatibility)) {
$range = self::getSemverRange($core_compatibility);
if (!empty($range)) {
$min = ($range['min'] < $min) ? $range['min'] : $min;
$max = ($range['max'] > $max) ? $range['max'] : $max;
}
else {
\Drupal::logger('drupalorg')
->warning('@core_compatibility is an invalid version constraint found in a release in @project project.', [
'@project' => $machine_name,
'@core_compatibility' => $core_compatibility,
]);
}
}
}
}
if ($min != PHP_INT_MAX && $max != PHP_INT_MIN) {
$project
->set('field_core_semver_minimum', $min)
->set('field_core_semver_maximum', $max)
->save();
}
}
}
// Reset the cache in order to free memory as we progress.
$memory_cache_clear_counter++;
if ($memory_cache_clear_counter > 100) {
$memory_cache->deleteAll();
$memory_cache_clear_counter = 0;
}
}
}
/**
* Return all (published and full) project ids.
*
* @return array
* List of IDs.
*/
protected static function getAllProjectIds(): array {
return \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'project_module')
->condition('field_project_type', 'full')
->condition('field_project_has_releases', 1)
->execute();
}
/**
* Get projects with recent releases.
*
* @param int $timestamp
* Time to check from.
*
* @return array
* List of projects that match the condition.
*/
protected static function getProjectsWithReleasesNewerThan(int $timestamp): array {
if (empty($timestamp)) {
return self::getAllProjectIds();
}
$release_ids = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'project_release')
->exists('field_packaged_git_sha1')
->condition('created', $timestamp, '>')
->execute();
$project_ids = [];
$chunks = array_chunk($release_ids, 100);
foreach ($chunks as $chunk) {
$releases = Node::loadMultiple($chunk);
foreach ($releases as $release) {
$project_id = $release->get('field_release_project')->getValue();
if (!empty($project_id[0]['target_id'])) {
$id = $project_id[0]['target_id'];
$project_ids[$id] = $id;
}
}
}
return $project_ids;
}
/**
* Calculates the upper and lower end supported by a semver string.
*
* @param string $semver_value
* Value to check.
*
* @return array
* Min and max supported values, if found.
*/
protected static function getSemverRange($semver_value): array {
$cache = \Drupal::cache();
$cache_key = 'drupalorg:core_compatibility_range:' . md5($semver_value);
if ($result = $cache->get($cache_key)) {
return $result->data;
}
$result = [];
try {
$limits = (new SemVerConverter())->convert($semver_value);
if (!empty($limits)) {
$lower_end = $limits[0];
$min = $lower_end['from'][0];
if ($min == 0) {
// SemVerConverter lower bound when the constraint is "<".
// Make it 8000000 as we're using the default 3 zeroes, 3 sections.
$min = 8000000;
}
$upper_end = array_pop($limits);
$max = $upper_end['to'][0];
if ($max == 999999999999) {
// SemVerConverter upper bound when the constraint is ">=".
// Make it X999999 as we're using the default 3 zeroes, 3 sections.
$max = (int) (self::MAX_CORE_SUPPORTED . '999999');
}
$result = [
'min' => $min,
'max' => $max,
];
}
}
catch (\Throwable $e) {
\Drupal::logger('drupalorg')
->warning('Invalid version constraint @version.', [
'@version' => $semver_value,
]);
}
$cache->set($cache_key, $result);
return $result;
}
}
