|
| 1 | +<?php |
| 2 | + |
| 3 | +/** |
| 4 | + * Scholiq OpenRegister autoload prelude |
| 5 | + * |
| 6 | + * Puts OpenRegister's PSR-4 prefix on the autoloader so this app can reference |
| 7 | + * `OCA\OpenRegister\AppHost\…` from its own `Application::register()`. |
| 8 | + * |
| 9 | + * SPDX-License-Identifier: EUPL-1.2 |
| 10 | + * SPDX-FileCopyrightText: 2026 Conduction B.V. |
| 11 | + * |
| 12 | + * @category AppInfo |
| 13 | + * @package OCA\Scholiq\AppInfo |
| 14 | + * |
| 15 | + * @author Conduction Development Team <dev@conduction.nl> |
| 16 | + * @copyright 2026 Conduction B.V. |
| 17 | + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 |
| 18 | + * |
| 19 | + * @version GIT: <git-id> |
| 20 | + * |
| 21 | + * @link https://conduction.nl |
| 22 | + */ |
| 23 | + |
| 24 | +declare(strict_types=1); |
| 25 | + |
| 26 | +namespace OCA\Scholiq\AppInfo; |
| 27 | + |
| 28 | +/** |
| 29 | + * Registers OpenRegister's autoload prefix before AppHost is referenced. |
| 30 | + * |
| 31 | + * ## Why this is needed (ADR-040) |
| 32 | + * |
| 33 | + * `OC_App::getEnabledApps()` does `sort($apps)`, and |
| 34 | + * `Coordinator::registerApps()` walks THAT sorted list calling |
| 35 | + * `OC_App::registerAutoloading($appId, $path)` and then `$app->register()` for |
| 36 | + * one app at a time. So every app's `register()` runs BEFORE the PSR-4 prefix |
| 37 | + * of every alphabetically-LATER app exists. |
| 38 | + * |
| 39 | + * `scholiq` sorts AFTER `openregister`, so today the prefix happens to be on |
| 40 | + * the autoloader by the time this app registers. That is an accident of the |
| 41 | + * alphabet, not a design property, and Scholiq depends on it far more sharply |
| 42 | + * than the apps that sort earlier: its `Bootstrap::register()` call is |
| 43 | + * UNGUARDED, so the moment the ordering stops holding — an app id change, a |
| 44 | + * multi-`apps_paths` install, this composition root moving into a package that |
| 45 | + * sorts earlier — the resulting `\Error` aborts the WHOLE of |
| 46 | + * `Application::register()`. `Coordinator::registerApps()` catches it, logs an |
| 47 | + * `emergency` and continues, so Scholiq would stay enabled and keep serving |
| 48 | + * with every registration below that line silently missing. |
| 49 | + * |
| 50 | + * Registering the prefix ourselves removes the dependency on ordering |
| 51 | + * entirely. `OC_App::registerAutoloading()` is idempotent, so on the current |
| 52 | + * ordering this call is free. |
| 53 | + * |
| 54 | + * Lives in its own class rather than inline in `Application::register()` for |
| 55 | + * one reason: `Application` cannot be constructed without a Nextcloud DI |
| 56 | + * container, so an inline prelude is unreachable from a unit test. Here the |
| 57 | + * degraded-path contract — "this NEVER throws, whatever the instance looks |
| 58 | + * like" — is directly assertable, and it is asserted. |
| 59 | + * |
| 60 | + * @spec openspec/specs/apphost-adoption/spec.md |
| 61 | + */ |
| 62 | +final class OpenRegisterAutoloader |
| 63 | +{ |
| 64 | + /** |
| 65 | + * Register OpenRegister's PSR-4 prefix on the composer autoloader. |
| 66 | + * |
| 67 | + * MUST be called before any `OCA\OpenRegister\…` reference in |
| 68 | + * `Application::register()`, including a `class_exists()` probe — the probe |
| 69 | + * answers FALSE, not "not yet loaded", and a FALSE is indistinguishable |
| 70 | + * from OpenRegister being absent. |
| 71 | + * |
| 72 | + * `OC_App::registerAutoloading()` touches only the autoloader and is |
| 73 | + * idempotent: it early-returns on an `$alreadyRegistered` key, so calling |
| 74 | + * this more than once is free. |
| 75 | + * |
| 76 | + * Deliberately NOT `IAppManager::loadApp('openregister')`: that marks |
| 77 | + * OpenRegister loaded and calls `Coordinator::bootApp()`, booting it before |
| 78 | + * its own `register()` has run. |
| 79 | + * |
| 80 | + * @param string|null $appId App id to register the autoloader for. |
| 81 | + * Production callers pass nothing and get |
| 82 | + * 'openregister'. It exists so the degraded |
| 83 | + * path below — the branch that must NEVER |
| 84 | + * rethrow — is reachable from a test with an id |
| 85 | + * that cannot resolve; without it that branch |
| 86 | + * is dead on any instance where OpenRegister IS |
| 87 | + * installed, which is every instance this app |
| 88 | + * is tested on. |
| 89 | + * |
| 90 | + * @return void This never reports success or failure. The caller's own |
| 91 | + * `class_exists()` guard is the authoritative signal; a |
| 92 | + * return value here would only duplicate it, and would add a |
| 93 | + * `return true`/`return false` pair of which exactly one is |
| 94 | + * dead in any given run. |
| 95 | + * |
| 96 | + * @SuppressWarnings(PHPMD.StaticAccess) OC_App is Nextcloud's legacy |
| 97 | + * bootstrap class. There is no OCP interface for registering another app's |
| 98 | + * autoloader, and this runs at the composition root where no container is |
| 99 | + * available to resolve an adapter from. |
| 100 | + * |
| 101 | + * @spec openspec/specs/apphost-adoption/spec.md |
| 102 | + */ |
| 103 | + public static function register(?string $appId=null): void |
| 104 | + { |
| 105 | + try { |
| 106 | + // The app id is written as a literal at the call site rather than |
| 107 | + // defaulted in the signature, so it is visible where it is used — |
| 108 | + // to a reader, and to hydra gate-64, which reads |
| 109 | + // registerAutoloading()'s arguments. No return value: the caller's |
| 110 | + // class_exists() guard is the authoritative signal. |
| 111 | + $path = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppPath($appId ?? 'openregister'); |
| 112 | + \OC_App::registerAutoloading($appId ?? 'openregister', $path); |
| 113 | + } catch (\Throwable) { |
| 114 | + // OpenRegister absent, disabled, or the server container is not up |
| 115 | + // (unit tests). Never rethrow: an exception escaping here would |
| 116 | + // abort the caller's entire register(), which is the exact defect |
| 117 | + // this prelude exists to prevent. |
| 118 | + } |
| 119 | + |
| 120 | + }//end register() |
| 121 | +}//end class |
0 commit comments