Gacela in production: Phel
Phel 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.
Version transparency
Phel currently declares gacela-project/gacela:^1.21. The architecture below is real and remains representative of Gacela 2.0, but the project has not yet published a 2.0 migration. Each excerpt was verified against Phel commit f173cf5.
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, RunFacade, RunProvider, CompileCommand, and BuildHealthCheck.
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.