Files
mokoplatform/lib/Enterprise/SourceResolver.php
T
Jonathan Miller ca55e5d2d2 feat(core): add SourceResolver for backwards-compatible src/ → source/ migration
Introduces SourceResolver utility class with source/ → src/ → htdocs/
fallback chain, replacing hardcoded src/ references across 28 files.
This enables renaming root-level src/ to source/ in all repos while
maintaining backwards compatibility during the transition.

Phase 1: New lib/Enterprise/SourceResolver.php with resolve(),
resolveAbsolute(), globSource(), findUnderSource(), warnIfLegacy()
Phase 2: Updated 19 CLI/deploy tools to use SourceResolver
Phase 3: Updated 7 validator/lib files (McpServerPlugin,
PackageBuilder, RepositorySynchronizer, auto_detect_platform,
check_dolibarr_module, check_client_theme, check_structure)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:23:41 -05:00

188 lines
6.0 KiB
PHP

<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Enterprise
* INGROUP: MokoPlatform.Lib
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/SourceResolver.php
* BRIEF: Resolve the root-level source directory across repos (source/, src/, htdocs/)
*/
declare(strict_types=1);
namespace MokoEnterprise;
/**
* Source Directory Resolver
*
* Provides a single, consistent fallback chain for locating the root-level
* source directory in any MokoStandards repository. The preferred directory
* is `source/`, with legacy `src/` and `htdocs/` as fallbacks.
*
* This class exists because Joomla extensions use `src/` for namespace
* autoloading (e.g. administrator/components/com_foo/src/). Renaming our
* root-level source directory to `source/` avoids that collision. During
* the transition period, repos may still use `src/`, so all tooling must
* check both.
*
* Usage:
* $dir = SourceResolver::resolve($repoRoot); // 'source', 'src', or 'htdocs'
* $abs = SourceResolver::resolveAbsolute($repoRoot); // full path or null
* $xmls = SourceResolver::globSource($repoRoot, '*.xml'); // glob under first match
* $path = SourceResolver::findUnderSource($repoRoot, 'core/modules'); // subpath lookup
*
* @since 09.02.00
*/
class SourceResolver
{
/**
* Ordered candidate directories. source/ is preferred, src/ is legacy fallback.
*
* When the migration is complete and all repos use source/, the 'src'
* entry can be removed from this list.
*
* @var string[]
*/
private const CANDIDATES = ['source', 'src', 'htdocs'];
/**
* Resolve the source directory name for a repository root.
*
* Returns the first candidate directory that exists, or 'source' as the
* default when no candidate is found (e.g. for new repos being scaffolded).
*
* @param string $root Absolute path to the repository root.
* @return string Directory name (e.g. 'source', 'src', 'htdocs').
*/
public static function resolve(string $root): string
{
foreach (self::CANDIDATES as $candidate) {
if (is_dir("{$root}/{$candidate}")) {
return $candidate;
}
}
return 'source';
}
/**
* Resolve the source directory as an absolute path.
*
* @param string $root Absolute path to the repository root.
* @return string|null Absolute path to the source directory, or null if none exists.
*/
public static function resolveAbsolute(string $root): ?string
{
foreach (self::CANDIDATES as $candidate) {
$path = "{$root}/{$candidate}";
if (is_dir($path)) {
return $path;
}
}
return null;
}
/**
* Glob for files under the source directory.
*
* Checks each candidate directory in order and returns matches from the
* first candidate that produces results. This replaces patterns like:
*
* glob("{$root}/src/*.xml")
*
* With the backwards-compatible:
*
* SourceResolver::globSource($root, '*.xml')
*
* @param string $root Absolute path to the repository root.
* @param string $pattern Glob pattern relative to the source directory.
* @return string[] Matched file paths (may be empty).
*/
public static function globSource(string $root, string $pattern): array
{
foreach (self::CANDIDATES as $candidate) {
$dir = "{$root}/{$candidate}";
if (!is_dir($dir)) {
continue;
}
$matches = glob("{$dir}/{$pattern}") ?: [];
if ($matches !== []) {
return $matches;
}
}
return [];
}
/**
* Find a subpath under any source directory candidate.
*
* Useful for locating platform-specific subdirectories like
* `core/modules/` (Dolibarr) or `media/templates/` (Joomla client themes)
* regardless of whether the repo uses `source/` or `src/`.
*
* @param string $root Absolute path to the repository root.
* @param string $subpath Relative path to look for (e.g. 'core/modules', 'index.ts').
* @return string|null Absolute path if found, null otherwise.
*/
public static function findUnderSource(string $root, string $subpath): ?string
{
foreach (self::CANDIDATES as $candidate) {
$full = "{$root}/{$candidate}/{$subpath}";
if (file_exists($full) || is_dir($full)) {
return $full;
}
}
return null;
}
/**
* Get the ordered list of candidate directory names.
*
* Useful for workflows or scripts that need to iterate candidates
* themselves (e.g. building find/grep patterns).
*
* @return string[]
*/
public static function getCandidates(): array
{
return self::CANDIDATES;
}
/**
* Check whether the resolved source directory is a legacy name (src/).
*
* @param string $root Absolute path to the repository root.
* @return bool True if the repo uses src/ instead of source/.
*/
public static function isLegacy(string $root): bool
{
$resolved = self::resolve($root);
return $resolved === 'src';
}
/**
* Emit a deprecation warning to stderr if the repo still uses src/.
*
* CLI tools should call this after resolving the source directory so
* that maintainers know to rename src/ → source/.
*
* @param string $root Absolute path to the repository root.
*/
public static function warnIfLegacy(string $root): void
{
if (self::isLegacy($root)) {
fwrite(STDERR, "⚠ WARNING: This repo uses src/ which is deprecated. Rename to source/ per MokoStandards.\n");
}
}
}