<?php class RepositoryList { private const GIT_EXT = '.git'; private const ORDER_FILE = '/order.txt'; private const GLOB_PATTERN = '/*'; private const HIDDEN_PREFIX = '.'; private const EXCLUDE_CHAR = '-'; private const SORT_MAX = PHP_INT_MAX; private const KEY_SAFE_NAME = 'safe_name'; private const KEY_EXCLUDE = 'exclude'; private const KEY_ORDER = 'order'; private const KEY_PATH = 'path'; private const KEY_NAME = 'name'; private string $reposPath; public function __construct( string $path ) { $this->reposPath = $path; } public function eachRepository( callable $callback ): void { $repos = $this->sortRepositories( $this->loadRepositories() ); foreach( $repos as $repo ) { $callback( $repo ); } } private function loadRepositories(): array { $repos = []; $path = $this->reposPath . self::GLOB_PATTERN; $dirs = glob( $path, GLOB_ONLYDIR ); if( $dirs !== false ) { $repos = $this->processDirectories( $dirs ); } return $repos; } private function processDirectories( array $dirs ): array { $repos = []; foreach( $dirs as $dir ) { $data = $this->createRepositoryData( $dir ); if( $data !== [] ) { $repos[$data[self::KEY_NAME]] = $data; } } return $repos; } private function createRepositoryData( string $dir ): array { $data = []; $base = basename( $dir ); if( $base[0] !== self::HIDDEN_PREFIX ) { $name = $this->extractName( $base ); $data = [ self::KEY_NAME => $name, self::KEY_SAFE_NAME => $name, self::KEY_PATH => $dir, ]; } return $data; } private function extractName( string $base ): string { $name = $base; if( str_ends_with( $base, self::GIT_EXT ) ) { $len = strlen( self::GIT_EXT ); $name = substr( $base, 0, -$len ); } return $name; } private function sortRepositories( array $repos ): array { $file = __DIR__ . self::ORDER_FILE; if( file_exists( $file ) ) { $repos = $this->applyCustomOrder( $repos, $file ); } else { ksort( $repos, SORT_NATURAL | SORT_FLAG_CASE ); } return $repos; } private function applyCustomOrder( array $repos, string $file ): array { $lines = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); if( $lines !== false ) { $config = $this->parseOrderFile( $lines ); $repos = $this->filterExcluded( $repos, $config[self::KEY_EXCLUDE] ); $repos = $this->sortWithConfig( $repos, $config[self::KEY_ORDER] ); } return $repos; } private function parseOrderFile( array $lines ): array { $order = []; $exclude = []; foreach( $lines as $line ) { $trim = trim( $line ); if( $trim !== '' ) { if( str_starts_with( $trim, self::EXCLUDE_CHAR ) ) { $exclude = $this->addExclusion( $exclude, $trim ); } else { $order[$trim] = count( $order ); } } } return [ self::KEY_ORDER => $order, self::KEY_EXCLUDE => $exclude ]; } private function addExclusion( array $exclude, string $line ): array { $name = substr( $line, 1 ); $exclude[$name] = true; return $exclude; } private function filterExcluded( array $repos, array $exclude ): array { foreach( $repos as $key => $repo ) { if( isset( $exclude[$repo[self::KEY_SAFE_NAME]] ) ) { unset( $repos[$key] ); } } return $repos; } private function sortWithConfig( array $repos, array $order ): array { uasort( $repos, function( array $repoA, array $repoB ) use( $order ): int { return $this->compareRepositories( $repoA, $repoB, $order ); } ); return $repos; } private function compareRepositories( array $repoA, array $repoB, array $order ): int { $safeA = $repoA[self::KEY_SAFE_NAME]; $safeB = $repoB[self::KEY_SAFE_NAME]; $posA = $order[$safeA] ?? self::SORT_MAX; $posB = $order[$safeB] ?? self::SORT_MAX; $result = $posA === $posB ? strcasecmp( $safeA, $safeB ) : $posA <=> $posB; return $result; } }