|null */ private ?array $flags = null; /** * What an administrator last chose, per module id. * * @return array */ public function flags(): array { if ( null === $this->flags ) { $stored = get_option( self::OPTION, array() ); $this->flags = array(); foreach ( (array) $stored as $id => $on ) { if ( is_string( $id ) ) { $this->flags[ $id ] = (bool) $on; } } } return $this->flags; } /** * Resolve descriptors plus stored flags into one yes/no per module. * * - `core` wins over everything and never consults the flag, so a stale `false` in the option can * never black out the dashboard. * - Otherwise the flag decides, defaulting to the descriptor's `default` when nobody has chosen. * - A module with no descriptor at all is ENABLED (see `ModuleRegistry::enabled()`): an add-on's * module knows nothing about this screen, and defaulting it OFF would install it silently inert. * * Dependencies resolve to a FIXED POINT rather than in one pass: if C needs B and B needs A, * turning A off has to reach C, and the descriptors are in filesystem order, not dependency * order. Each pass can only turn things off, so the loop is bounded by the module count. * * @param array> $descriptors Module id => `AppInterface::app()`. * * @return array Module id => actually running. */ public function resolve( array $descriptors ): array { $flags = $this->flags(); $enabled = array(); foreach ( $descriptors as $id => $descriptor ) { if ( ! empty( $descriptor['core'] ) ) { $enabled[ $id ] = true; continue; } $enabled[ $id ] = (bool) ( $flags[ $id ] ?? ( $descriptor['default'] ?? true ) ); } $passes = count( $descriptors ); for ( $pass = 0; $pass < $passes; $pass++ ) { $settled = true; foreach ( $descriptors as $id => $descriptor ) { if ( ! $enabled[ $id ] || ! empty( $descriptor['core'] ) ) { continue; } if ( ! self::dependencies_met( $descriptor, $enabled ) ) { $enabled[ $id ] = false; $settled = false; } } if ( $settled ) { break; } } return $enabled; } /** * Are this app's dependencies running? * * A dependency nobody declared is not a dependency that is off: a module missing from this site * (an extension that is not installed) is not held against the app that names it. * * @param array $descriptor The app's descriptor. * @param array $enabled The answer so far. */ public static function dependencies_met( array $descriptor, array $enabled ): bool { foreach ( (array) ( $descriptor['deps'] ?? array() ) as $dep ) { if ( isset( $enabled[ $dep ] ) && ! $enabled[ $dep ] ) { return false; } } $any = array_values( array_filter( (array) ( $descriptor['deps_any'] ?? array() ), static fn( $dep ) => isset( $enabled[ $dep ] ) ) ); if ( array() !== $any ) { foreach ( $any as $dep ) { if ( $enabled[ $dep ] ) { return true; } } return false; } return true; } /** * Write some flags, leaving every other stored flag exactly as it was. * * IT MERGES, IT NEVER REPLACES. A module that is not on disk right now (say, an extension that * was deactivated) is not in this payload. A whole-map write would erase the record that somebody * had ever switched it off, and it would come back to find their choices reset by a screen that * could not even see it. * * @param array $patch Module id => on. * * @return array The stored map afterwards. */ public function save( array $patch ): array { $flags = $this->flags(); foreach ( $patch as $id => $on ) { if ( is_string( $id ) && '' !== $id ) { $flags[ $id ] = (bool) $on; } } ksort( $flags ); update_option( self::OPTION, $flags ); $this->flags = $flags; return $flags; } /** * Forget everything. The uninstaller's half of `OPTION`. */ public function forget(): void { delete_option( self::OPTION ); $this->flags = null; } }