This document describes the runtime flow from project root input to yielded file paths.
Typical usage starts with DiscoveryConfigFactory and FileLocator:
<?php
use LiquidRazor\FileLocator\Config\DiscoveryConfigFactory;
use LiquidRazor\FileLocator\FileLocator;
$config = (new DiscoveryConfigFactory())->create('/absolute/project/root');
foreach ((new FileLocator($config))->locate() as $path) {
// $path is an absolute normalized file path
}The flow has two phases:
- configuration assembly
- filesystem traversal
DiscoveryConfigFactory::create(string $projectRoot) performs the full config build.
It does the following in order:
- Normalizes the supplied project root.
- Uses
ConfigLoaderto load the library default config from the logical namerootsunderresources/config. - Uses
ConfigLoaderto attempt an optional project override from the logical namerootsunder<project-root>/config. - Merges the two raw config arrays with ConfigLoader's merge rules.
- Validates and normalizes the merged config.
- Builds immutable runtime value objects.
If both config/roots.yaml and config/roots.yml exist, ConfigLoader fails because both match the same logical config name.
Merge behavior is deterministic and follows ConfigLoader:
- scalar values are overridden by the project config
- indexed arrays are replaced by the project config
- roots are merged by root name
- root scalar fields such as
path,recursive, andenabledare overridden
enabled: false is preserved during merge, then removed when the runtime config is built.
After validation, the runtime config contains:
projectRoot: normalized absolute project pathexclude: normalized absolute global exclusionsdefaults: normalized discovery defaultsroots: enabled roots only, in merged order
Each root contains:
namepathrecursiveextensionsexclude
All root and exclusion paths are normalized to absolute paths with / separators.
FileLocator::locate() processes roots in config order.
For each root:
- Skip the root if the directory does not exist.
- Skip the root if the root path is a symlink and symlink following is disabled.
- Check root readability.
- Build a recursive SPL iterator stack.
- Prune directories before descending into them.
- Yield matching files lazily.
Traversal uses:
RecursiveDirectoryIteratorRecursiveCallbackFilterIteratorRecursiveIteratorIterator
Traversal is depth-first.
Filtering happens as early as possible.
Directory pruning rules:
- do not descend into hidden directories when
include_hiddenisfalse - do not descend into globally excluded paths
- do not descend into root-specific excluded paths
- do not descend below the root when
recursiveisfalse
File yield rules:
- skip hidden files when
include_hiddenisfalse - skip globally excluded files
- skip root-specific excluded files
- skip files without a matching extension
In the current implementation, the only supported extension is php.
locate() returns a Generator<int, string>.
Each yielded value is:
- a file path string
- absolute
- normalized to
/ - produced lazily
The locator does not accumulate a complete file list in memory.
Configuration phase failures:
ConfigLoaderexceptions when a config root, file, format, or YAML document is invalidInvalidDiscoveryConfigExceptionwhen merged config fails schema or path validation
Traversal phase failures:
- unreadable paths are skipped when
on_unreadable: skip - unreadable paths throw
PathAccessExceptionwhenon_unreadable: fail