diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index e17aebd360f30e0b23b75fb7577de41bc76701c0..6650d7d1c89202116cbb73d7a35b2862695c0e6b 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -361,8 +361,8 @@ final class InstallerController extends ControllerBase { * Status message. */ public function apply(string $stage_id): JsonResponse { - foreach (array_keys($this->installState->toArray()) as $project_id) { - $this->installState->setState($project_id, 'applying'); + foreach ($this->installState->toArray() as $project) { + $this->installState->setState($project['project_id'], 'applying'); } try { $this->installer->claim($stage_id)->apply(); diff --git a/src/EnabledSourceHandler.php b/src/EnabledSourceHandler.php index 68a121ff321c2e94c714ffb31a754c9939c67bec..500f880bdc1526ec1a43ae76329e559fccfff218 100644 --- a/src/EnabledSourceHandler.php +++ b/src/EnabledSourceHandler.php @@ -143,14 +143,14 @@ final class EnabledSourceHandler implements LoggerAwareInterface, EventSubscribe // Cache all the projects individually so they can be loaded by // ::getStoredProject(). foreach ($results->list as $project) { - $storage->setIfNotExists($project->id, $project); + $this->storeProject($source_id, $project); } // If there were no query errors, store the results as a set of arguments // to ProjectsResultsPage. if (empty($results->error)) { $storage->set($cache_key, [ $results->totalResults, - array_column($results->list, 'id'), + array_map(Project::normalizeId(...), array_column($results->list, 'id')), $results->pluginLabel, $source_id, $results->error, @@ -201,6 +201,19 @@ final class EnabledSourceHandler implements LoggerAwareInterface, EventSubscribe return $enabled_sources[$source_id]->getProjects($query); } + /** + * Store a project in the non-volatile data store. + * + * @param string $source_id + * The ID of the source plugin to store the project in. + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * Project to store. + */ + private function storeProject(string $source_id, Project $project): void { + $id = Project::normalizeId($project->id); + $this->keyValue($source_id)->setIfNotExists($id, $project); + } + /** * Looks up a previously stored project by its ID. * @@ -215,6 +228,7 @@ final class EnabledSourceHandler implements LoggerAwareInterface, EventSubscribe */ public function getStoredProject(string $id): Project { [$source_id, $local_id] = explode('/', $id, 2); + $local_id = Project::normalizeId($local_id); return $this->keyValue($source_id)->get($local_id) ?? throw new \RuntimeException("Project '$id' was not found in non-volatile storage."); } diff --git a/src/InstallState.php b/src/InstallState.php index 9b0059b458f73dfccc9e1342d9dcbd810a9a6984..4f98b422e1b197725d91165a8a09c895b5e8bb40 100644 --- a/src/InstallState.php +++ b/src/InstallState.php @@ -75,25 +75,29 @@ final class InstallState { */ public function setState(string $project_id, ?string $status): void { $this->keyValue->setIfNotExists('__timestamp', $this->time->getRequestTime()); + $normalized_id = Project::normalizeId($project_id); if (is_string($status)) { - $this->keyValue->set($project_id, ['status' => $status]); + $this->keyValue->set($normalized_id, [ + 'status' => $status, + 'project_id' => $project_id, + ]); } else { - $this->keyValue->delete($project_id); + $this->keyValue->delete($normalized_id); } } /** * Retrieves the install state of a project. * - * @param \Drupal\project_browser\ProjectBrowser\Project $project - * The project object for which to retrieve the install state. + * @param string $project_id + * The project ID to retrieve. * * @return string|null * The current install status of the project, or NULL if not found. */ - public function getStatus(Project $project): ?string { - $project_data = $this->keyValue->get($project->id); + public function getStatus(string $project_id): ?string { + $project_data = $this->keyValue->get(Project::normalizeId($project_id)); return $project_data['status'] ?? NULL; } diff --git a/src/ProjectBrowser/Project.php b/src/ProjectBrowser/Project.php index 5931f3c32eae2c28767bb2d60aaed1b20891f593..8da2231b9a4a7fbcf21ebb5570019c36601ad490 100644 --- a/src/ProjectBrowser/Project.php +++ b/src/ProjectBrowser/Project.php @@ -121,6 +121,23 @@ final class Project { $this->id = $id; } + /** + * Normalizes a project ID for database storage. + * + * Ensures the ID is no more than 104 characters long, so that it can fit into + * a `varchar` database column. We append a partial hash of the original, + * full-length ID in order to guarantee uniqueness. + * + * @param string $id + * A project ID to normalize. + * + * @return string + * The normalized project ID. + */ + public static function normalizeId(string $id): string { + return substr($id, 0, 96) . '-' . substr(sha1($id), 0, 8); + } + /** * Set the project short description. * diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index 462c2d58563fe0a5e03558ced323355e112f1025..a5551f28d766cf41afc3396a2a3d0acce48c1f2b 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -444,13 +444,7 @@ final class InstallerControllerTest extends BrowserTestBase { $response = $this->drupalGet('admin/modules/project_browser/install-begin', $request_options); $this->assertSession()->statusCodeEquals(418); $assert_unlock_response($response, "The process for adding the project that was locked less than a minute ago might still be in progress. Consider waiting a few more minutes before using [+unlock link]."); - $expected = [ - 'project_browser_test_mock/awesome_module' => [ - 'status' => 'requiring', - ], - ]; - $install_state = $this->container->get(InstallState::class)->toArray(); - $this->assertSame($expected, $install_state); + $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'requiring'); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); @@ -591,16 +585,14 @@ final class InstallerControllerTest extends BrowserTestBase { * * @param string $project_id * The ID of the project being enabled. - * @param string|null $status + * @param string|null $expected_status * The install state. */ - protected function assertInstallInProgress(string $project_id, ?string $status = NULL): void { - $expect_install[$project_id] = [ - 'status' => $status, - ]; - $install_state = $this->container->get(InstallState::class) - ->toArray(); - $this->assertSame($expect_install, $install_state); + protected function assertInstallInProgress(string $project_id, ?string $expected_status = NULL): void { + $this->assertSame( + $expected_status, + $this->container->get(InstallState::class)->getStatus($project_id), + ); } /**