Discovery
Discovery is the reflection layer that finds classes and attributes across your app and the framework. It is the foundation the whole code generator is built on.
Where your classes sit does not matter — Discovery scans the whole source tree, which is why the project structure is a convention rather than a requirement.
Finding classes
Discovery::findClasses() scans the source tree and returns the classes matching the given
filters — by parent class, by implemented interface, or within a path.
use Framework\Discovery\Discovery;
use Framework\Discovery\Type\DiscoveryBuilder;
// Every class that implements a given interface, in the app and the framework
$classes = Discovery::findClasses(
interface: DiscoveryBuilder::class,
forAll: true,
forFramework: true,
);
foreach ($classes as $class) {
$class->getFullyQualifiedName();
$class->getMethods();
$class->getAttribute(SomeAttribute::class);
}
| Parameter | Purpose |
|---|---|
parentClass | Only classes extending this one. |
interface | Only classes implementing this interface. |
path | Restrict the scan to a sub-path. |
forAll / forFramework | Include the app and/or the framework sources. |
The DiscoveryClass
Every match is a DiscoveryClass — a thin wrapper over reflection with the helpers the generators
need, so a builder rarely touches ReflectionClass directly:
foreach ($classes as $class) {
$class->getName(); // short class name
$class->getFullyQualifiedName(); // with namespace
$class->getFileName(); // its source file
$class->getParentClass(); // another DiscoveryClass
$class->getAttribute(Route::class); // a ReflectionAttribute, or null
$class->getMethods(); // the reflected methods
$class->getProperties(); // the reflected properties
$class->getPriority(); // its #[Priority], if any
$instance = $class->newInstance(); // construct it
}
Discovery interfaces
Two marker interfaces let a class hook into the pipeline: Discovery finds every implementer and the matching runner calls it, so there is nothing to register — implement the interface and build:
| Interface | Runs when |
|---|---|
DiscoveryBuilder | On the build — its generateCode() / destroyCode() write and remove the generated files. |
DiscoveryMigration | On migrate — its migrateData() keeps a table in sync with your code or config. |
Attributes drive everything
The generator reads PHP attributes off the discovered classes and methods. You annotate your code; the
build turns those annotations into typed src/System classes. The key attributes are:
| Attribute | Used for |
|---|---|
#[Route("/path", Access::X)] | Declares an HTTP route. |
#[Listener("event")] | Subscribes a method to a signal. |
#[ConsoleCommand("name")] | Exposes a CLI command. |
#[Priority(Priority::Highest)] | Orders a builder during the build. |
Loading the config files
DiscoveryConfig is what makes a
config file take effect. It walks the app once, includes every file
whose name ends in .config.php, and remembers that it has done so:
use Framework\Discovery\DiscoveryConfig;
DiscoveryConfig::load(); // include every *.config.php in the app
DiscoveryConfig::loadDefault("access"); // fall back to the Framework's own
Three rules follow from how it scans, and they explain most surprises:
| Rule | Means |
|---|---|
The whole app is walked, vendor skipped | A config file works anywhere — the config/ folder is only a convention. |
| It runs once per process | Registering something after load() has run has no effect. |
| Nothing is loaded inside the Framework itself | Its own config files are defaults for apps, not settings for it. |
When an app registers nothing, loadDefault() pulls the framework's own file for that concern —
which is how the General / Admin / API roles exist in an
app that never declared any. Every command that needs the app's configuration calls load() first,
so by the time a builder or a migration runs,
the registrations are in place.
Ordering with Priority
Discovery returns classes in no particular order, which matters when one generator depends on another having
run. #[Priority] fixes that — Discovery::getPriority()
reads it, and the callers sort by it:
use Framework\Discovery\Attr\Priority;
#[Priority(Priority::Highest)]
class ConfigCode implements DiscoveryBuilder {
// Runs before the builders that read the config
}
It orders builders during a build and commands in the CLI listing. Without it, order is undefined — so if a generator needs another's output, say so rather than relying on the scan.
Loading data
Discovery also finds the data files that live beside the code, so a module can ship JSON without anyone configuring a path. It looks in the app first and falls back to the framework, which is how a default template exists until an app overrides it:
use Framework\Discovery\Discovery;
// A JSON file in a directory — the per-language strings, for instance
Discovery::loadJSON("nls/strings", "en");
// The HTML template an email is wrapped in
Discovery::loadEmailTemplate("welcome");
// Any other data file the app ships
Discovery::loadCustomData("countries");
This is what the internationalization layer reads its strings through, and what the email template is loaded with.
Reading properties
The generators need to inspect a class's properties as well as its methods — models are almost entirely properties. Those helpers sit on the same class:
Discovery::getProperties($instance); // name => value
Discovery::getPropertyNames($instance); // just the names
Discovery::getReflectionProps($className); // the reflected properties
getPropertiesBaseFirst() on a DiscoveryClass returns them parent-first, which
is what keeps a generated entity's inherited fields in a stable order.