Gacela
Documentation version: 2.4.0
Colour theme

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.

17+application modules
PHP 8.4declared platform
MITopen-source license

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:

  1. Bootstrap starts Gacela once, then calls the Facade. The entry point never learns what a namespace runner is.
  2. Facade is the only public door into the Run module. It delegates, it holds no logic.
  3. Factory builds what the Facade asked for, wiring services that stay inside the module.
  4. 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 interface

Start a new module with the Quickstart, then use Getting dependencies when it needs to communicate with another boundary.

Explore Phel