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
The excerpts are shortened only where unrelated methods would obscure the pattern. Follow Source below each tab group for the complete production files.
use Gacela\Framework\Gacela;
use Phel\Run\RunFacade;
public static function bootstrap(string $projectRootDir): void
{
Gacela::bootstrap(
$projectRootDir,
self::configFn(self::readAppModulePaths($configPath)),
);
}
public static function run(string $projectRootDir, string $namespace): void
{
self::bootstrap($projectRootDir);
(new RunFacade())->runNamespace($namespace);
}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);
}
}final class RunProvider extends AbstractProvider
{
#[Provides(BuildFacadeInterface::class)]
public function buildFacade(Container $container): BuildFacadeInterface
{
return $container->getLocator()->getRequired(BuildFacade::class);
}
#[Provides(FilesystemFacadeInterface::class)]
public function filesystemFacade(Container $container): FilesystemFacadeInterface
{
return $container->getLocator()->getRequired(FilesystemFacade::class);
}
}#[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
{
$this->getFacade()->loadPhelNamespaces();
$ok = $this->getFactory()
->createCompileExecutor()
->execute($source, $writeOutput, $writeError);
return $ok ? self::SUCCESS : self::FAILURE;
}
}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), 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.