[]>> */ protected array $blocksByLocationAndPosition = []; /** * @var array[]>> */ protected array $locationBlockCache = []; /** * Register a block type to be displayed at the given location and position. * @param class-string $blockClass */ public function register(string $location, string $defaultPosition, string $blockClass): void { if (!isset($this->blocksByLocationAndPosition[$location])) { $this->blocksByLocationAndPosition[$location] = []; } if (!isset($this->blocksByLocationAndPosition[$location][$defaultPosition])) { $this->blocksByLocationAndPosition[$location][$defaultPosition] = []; } // @phpstan-ignore-next-line if (!is_a($blockClass, ViewBlockInterface::class, true)) { throw new \InvalidArgumentException('When registering a view block, the block class must implement ViewBlockInterface'); } $this->blocksByLocationAndPosition[$location][$defaultPosition][] = $blockClass; } /** * Get all blocks registered for a given location and position, considering the * preferences for the current user. * @return ViewBlockInterface[] * @throws BindingResolutionException */ public function getInstancesForLocationAndPositionForCurrentUser(string $location, string $position): array { $key = $location; if (isset($this->locationBlockCache[$key])) { $blocks = $this->locationBlockCache[$key][$position] ?? []; return $this->blocksToInstances($blocks); } $forLocation = $this->getForLocationForCurrentUser($location); $this->locationBlockCache[$key] = $forLocation; $blocks = $forLocation[$position] ?? []; return $this->blocksToInstances($blocks); } /** * Create instances of the given block classes. * @param class-string[] $blocks * @return ViewBlockInterface[] * @throws BindingResolutionException */ protected function blocksToInstances(array $blocks): array { return array_map(fn (string $blockClass) => app()->make($blockClass), $blocks); } /** * Get all blocks registered for a given location, as sets of arrays * keyed by position. * @return array[]> */ protected function getForLocation(string $location): array { $defaults = ViewBlockDefaults::getForLocation($location); $registered = $this->blocksByLocationAndPosition[$location] ?? []; return array_merge_recursive($defaults, $registered); } /** * Get all blocks registered for a given location, as sets of arrays * keyed by position, for the current user. * Same as above but with user-specific preferences applied. * @return array>> * @throws BindingResolutionException */ public function getForLocationForCurrentUser(string $location): array { $forLocation = $this->getForLocation($location); $userBlocksByPosition = $this->preferences->getIdByPositionMap($location); if (empty($userBlocksByPosition)) { return $forLocation; } $results = []; $blocksById = $this->blocksByPositionToIdMap($forLocation); $idPositionMap = $this->blocksByPositionToIdPositionMap($forLocation); $positions = array_unique([...array_keys($forLocation), 'unused']); // Add based on user preferences foreach ($positions as $position) { $userBlockIds = $userBlocksByPosition[$position] ?? []; $results[$position] = []; foreach ($userBlockIds as $blockId) { $block = $blocksById[$blockId] ?? null; if ($block) { $results[$position][] = $block; unset($blocksById[$blockId]); } } } // Add remaining blocks based on their default locations foreach ($blocksById as $block) { $position = $idPositionMap[$block::getId()] ?? 'unused'; $results[$position][] = $block; } return $results; } /** * Get the names of all locations where blocks are registered. * Returns an array where the keys are location strings, and the * values are translated labels for that location. * @return array */ public function getNamedLocations(): array { $labels = ViewBlockDefaults::getLocationLabels(); $defaults = ViewBlockDefaults::getLocations(); $registered = array_keys($this->blocksByLocationAndPosition); $merged = array_unique(array_merge($defaults, $registered)); $results = []; foreach ($merged as $location) { $results[$location] = $labels[$location] ?? $location; } $usingDefaultHome = setting('app-homepage-type') === 'default'; $toIgnore = $usingDefaultHome ? 'home-non-default' : 'home-default'; unset($results[$toIgnore]); return $results; } /** * Update user preferences for a given location to match the given layout data map. * @param array $layoutData * @throws BindingResolutionException */ public function updatePreferencesFromIdPositionMap(string $location, array $layoutData): void { $this->preferences->storeByIdPositionMap( $location, $layoutData, $this->getForLocation($location), ); } /** * Clear the local user-specific cache of blocks. * The cache only needs to exist for the current request time since its purpose is to * avoid duplicate loading across views. */ public function clearLocalCache(): void { $this->locationBlockCache = []; } /** * Convert a blocksByPosition array into a map of block IDs to blocks. * @param array[]> $blocksByPosition * @return array> */ protected function blocksByPositionToIdMap(array $blocksByPosition): array { $map = []; foreach ($blocksByPosition as $position => $blocks) { foreach ($blocks as $block) { $map[$block::getId()] = $block; } } return $map; } /** * Convert a blocksByPosition array into a map of block IDs to their positions. * @param array[]> $blocksByPosition * @return array */ protected function blocksByPositionToIdPositionMap(array $blocksByPosition): array { $map = []; foreach ($blocksByPosition as $position => $blocks) { foreach ($blocks as $block) { $map[$block::getId()] = $position; } } return $map; } }