Gacela in production: Phel
Phel (opens in a new tab) is a functional language that compiles to PHP. Its compiler, CLI, formatter, language server, REPL, filesystem, and tooling are organized as Gacela modules in one actively maintained codebase.
Why Gacela fits Phel
Phel has many subsystems but needs one coherent application. Gacela gives each subsystem a recognizable public boundary and makes cross-module dependencies explicit:
- Callers enter through a Facade instead of depending on compiler internals.
- Factories construct application and domain services inside their module.
- Providers translate concrete Facades into the interfaces another module expects.
- Framework-created Symfony commands can still resolve typed Gacela services.
- Module health checks can be collected into operational diagnostics.
Real code walkthrough
Every excerpt is Phel's own code, shortened where unrelated lines would obscure the Gacela pattern. The Sources below the tabs link the complete production files.
The first four tabs follow one call:
- Bootstrap starts Gacela once, then calls the Facade. The entry point never learns what a namespace runner is.
- Facade is the only public door into the Run module. It delegates, it holds no logic.
- Factory builds what the Facade asked for, wiring services that stay inside the module.
- Provider is where another module arrives, as the interface the Factory asks for rather than a concrete class.
Symfony command and Health check are not later steps. They are two more callers entering that same boundary, one created by the framework, one by operational tooling.
// phel-lang: src/php/Phel.php
use Gacela\Framework\Gacela;
use Phel\Run\RunFacade;
public static function run(string $projectRootDir, string $namespace): void
{
self::bootstrap($projectRootDir);
(new RunFacade())->runNamespace($namespace);
}
public static function bootstrap(string $projectRootDir): void
{
$configPath = $projectRootDir . '/' . self::PHEL_CONFIG_FILE_NAME;
Gacela::bootstrap(
$projectRootDir,
self::configFn(self::readAppModulePaths($configPath)),
);
}// phel-lang: src/php/Run/RunFacade.php
final class RunFacade extends AbstractFacade implements RunFacadeInterface
{
public function runNamespace(string $namespace): void
{
$this->getFactory()
->createNamespaceRunner()
->run($namespace);
}
public function getNamespaceFromFile(string $path): NamespaceInformation
{
return $this->getFactory()
->getBuildFacade()
->getNamespaceFromFile($path);
}
}// phel-lang: src/php/Run/RunFactory.php
class RunFactory extends AbstractFactory
{
public function createNamespaceRunner(): NamespaceRunnerInterface
{
return new NamespaceRunner(
$this->getCommandFacade(),
$this->getBuildFacade(),
);
}
public function getCommandFacade(): CommandFacadeInterface
{
return $this->getProvidedDependency(CommandFacadeInterface::class);
}
public function getBuildFacade(): BuildFacadeInterface
{
return $this->getProvidedDependency(BuildFacadeInterface::class);
}
}// phel-lang: src/php/Run/RunProvider.php
final class RunProvider extends AbstractProvider
{
#[Provides(CommandFacadeInterface::class)]
public function commandFacade(Container $container): CommandFacadeInterface
{
return $container->getLocator()->getRequired(CommandFacade::class);
}
#[Provides(BuildFacadeInterface::class)]
public function buildFacade(Container $container): BuildFacadeInterface
{
return $container->getLocator()->getRequired(BuildFacade::class);
}
}// phel-lang: src/php/Run/Infrastructure/Command/CompileCommand.php
#[ServiceMap(method: 'getFacade', className: RunFacade::class)]
#[ServiceMap(method: 'getFactory', className: RunFactory::class)]
final class CompileCommand extends Command
{
use ServiceResolverAwareTrait;
protected function execute(InputInterface $input, OutputInterface $output): int
{
$stderr = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
$this->getFacade()->loadPhelNamespaces();
$source = $this->resolveSource(ScalarCoercion::toString($input->getArgument('source') ?? null));
$ok = $this->getFactory()
->createCompileExecutor()
->execute(
$source,
static fn(string $chunk) => $output->write($chunk),
static fn(string $chunk) => $stderr->write($chunk),
);
return $ok ? self::SUCCESS : self::FAILURE;
}
}// phel-lang: src/php/Build/Application/BuildHealthCheck.php
final readonly class BuildHealthCheck implements ModuleHealthCheckInterface
{
public function checkHealth(): HealthStatus
{
if (is_dir($this->cacheDir) && !is_writable($this->cacheDir)) {
return HealthStatus::unhealthy(
sprintf('Cache dir not writable: %s', $this->cacheDir),
['path' => $this->cacheDir],
);
}
return HealthStatus::healthy('Build directories are ready');
}
}Sources: bootstrap (opens in a new tab), RunFacade (opens in a new tab), RunFactory (opens in a new tab), RunProvider (opens in a new tab), CompileCommand (opens in a new tab), and BuildHealthCheck (opens in a new tab).
What to copy into your project
The useful pattern is the direction of dependencies, not Phel's exact filenames:
entry point → Facade → Factory → application/domain service
↓
Provider → another module's Facade interfaceStart a new module with the Quickstart, then use Getting dependencies when it needs to communicate with another boundary.