Framework
Core Concepts

Build System

The build turns your annotated classes into generated PHP under src/System. It is the bridge between Discovery and the typed runtime.

How a build runs

The build is the step between writing a class and being able to use it — it is steps two and five of the first feature walkthrough.

./framework build calls Builder::build(), which:

  1. loads the config and the Mustache .mu templates,
  2. discovers every class implementing DiscoveryBuilder (in the app and framework),
  3. orders them by their #[Priority], and
  4. calls generateCode() on each, writing the result into src/System.
./framework build       # generate src/System
./framework destroy     # remove the generated code

Writing a builder

A builder implements DiscoveryBuilder — a generateCode() that writes files and a matching destroyCode() that removes them, both annotated #[\Override]. Collect data from the discovered classes and render it through the Template enum, which knows every .mu file. Each case exposes ->create($path, $data) and ->delete($path), so you write the file yourself and can create the surrounding directory with Storage:

src/Agent/AgentPromptCode.php
use Framework\Discovery\Type\DiscoveryBuilder;
use Framework\Discovery\Attr\Priority;
use Framework\Discovery\Package;
use Framework\System\Template;
use Framework\File\Storage;

#[Priority(Priority::Highest)]
class AgentPromptCode implements DiscoveryBuilder {
    #[\Override]
    public static function generateCode(): int {
        $prompts  = self::collectPrompts();
        $typePath = Package::getSourcePath("Agent", "Type");
        Storage::createDir($typePath);

        // Renders AgentPromptCode.mu into $typePath/AgentPromptCode.php
        Template::AgentPromptCode->create($typePath, [
            "prompts" => array_keys($prompts),
        ]);

        $total = count($prompts);
        print("- Agent code -> $total prompts\n");
        return 1;
    }

    #[\Override]
    public static function destroyCode(): int {
        $typePath = Package::getSourcePath("Agent", "Type");
        $deleted  = 0;

        Template::AgentPromptCode->delete($typePath, $deleted);
        Storage::deleteDir($typePath, $deleted);
        return $deleted;
    }
}

generateCode() usually ends by printing a one-line summary of what it did and returning the number of files it added; destroyCode() mirrors it, returning the number removed. Both can create and remove their own sub-directories through Storage.

Pattern: keep the pure data collection in its own method (e.g. collectPrompts()) and let generateCode() only wire it to the template — the collector is then trivial to unit-test. Note that Builder::generateCode() is a framework-internal shortcut and is not used outside of it; apps render through the Template enum as above.

Templates & the Template enum

Templates are Mustache files ending in .mu. Every .mu file under src is picked up automatically during a build and turned into a case on the generated Template enum, whose name matches the file. A template is plain PHP with {{tags}} for the data you pass:

src/Agent/Template/AgentPromptCode.mu
<?php
enum AgentPrompt {
{{#prompts}}
    case {{.}};
{{/prompts}}
}

Because the file is named AgentPromptCode.mu, the build exposes it as Template::AgentPromptCode. Each case carries the same small set of methods:

MethodDoes
render($data)Renders the .mu file with your data and returns the PHP as a string.
create($path, $data)Renders and writes it as <Name>.php into $path; returns whether the file was written.
delete($path, &$deleted)Removes that file, adding to the $deleted counter — the inverse of create().
getFileName()The output name, <Name>.php.
use Framework\System\Template;

// Render to a string, or write straight to disk
$code = Template::AgentPromptCode->render(["prompts" => $prompts]);
Template::AgentPromptCode->create($typePath, ["prompts" => $prompts]);

// Remove it again (destroyCode)
$deleted = 0;
Template::AgentPromptCode->delete($typePath, $deleted);

Rendering aligns the doc-comment params; passing empty data returns an empty string, so create() skips generation for that template.

The generated files live under src/System (and any sub-directories your builders create) and are recreated on every build, so they should be git-ignored rather than committed.

The builders that ship

Everything generated into src/System comes from a builder like the one above. They are worth knowing by name, because a question about generated code is usually a question about one of them:

BuilderGenerates
RouterCodeThe Router — collects every #[Route] and writes the dispatch table.
SignalCodeThe Signal class — collects the listeners and merges their parameters.
TemplateCodeThe Template enum — one case per .mu file found.
SchemaBuilderThe schema classes of every model.
LanguageBuilderThe Language class, from the language files.
ActionLogBuilderThe Sec and Act enums, from the log attributes.
ConfigsThe Config class, from the .env files.
AccessRole / SettingConfigThe Access and Setting classes.
FilePathThe Path class.

Each one is a DiscoveryBuilder found the same way your own would be, so nothing about them is special — they are just the ones that come with the framework.

Watching for changes

Running build by hand after every edit gets old quickly. Watcher backs the watch command: it scans the app, and rebuilds whenever a file changes:

./framework watch

It respects .gitignore, polls every couple of seconds, and runs until you stop it — so a new route, listener or model is available a moment after you save the file. It is the way to work while developing; CI and deploys should run build explicitly.

What gets generated

A build writes one or more typed classes into src/System for every subsystem. Each is documented where that subsystem is:

GeneratedWhat it is & where it's documented
RouterThe route table — Routing.
SignalThe event dispatcher — Signals.
AccessThe access-role enum — Authentication.
ConfigTyped .env getters — Configuration.
SettingTyped database settings — Settings.
PathFile-path helpers — Files & Storage.
TemplateThe enum of every .mu file — above.
Schema, Query, Entity, Column, StatusPer-model database classes — Database.
RequestPer-model typed request — Model-backed routes.
MediaSchemaGenerated media models — Database.
EmailCodeLocalized email content — Emails.
NotificationCodeNotification content — Notifications.
LanguageThe NLS string accessor for your languages.
Act, Sec, LogIntlAction log types — see Logging.