# Gacela 2.0 — complete documentation

Canonical index: https://gacela-project.com/llms.txt

---

Source: https://gacela-project.com/docs/index.md

# Gacela documentation

Build modular PHP applications with a small, predictable vocabulary: a **Facade** exposes a module, a **Factory** creates its internal services, a **Provider** supplies external dependencies, and a **Config** reads application settings.

::: tip New to Gacela?
Start with the [Quickstart](https://gacela-project.com/docs/quickstart.md). It takes you from installation to a working module, then points to the next concept only when you need it.
:::

## Choose your path

<div class="gz-doc-grid">
  <a class="gz-doc-card" href="/docs/quickstart">
    <strong>Build your first module</strong>
    <span>Install Gacela and create a working Facade and Factory.</span>
  </a>
  <a class="gz-doc-card" href="/docs/getting-dependencies">
    <strong>Wire a dependency</strong>
    <span>Choose between Factory, Provider, bindings, Inject, and Service Map.</span>
  </a>
  <a class="gz-doc-card" href="/docs/upgrading">
    <strong>Upgrade to 2.0</strong>
    <span>Check requirements, replace removed APIs, and verify the migration.</span>
  </a>
  <a class="gz-doc-card" href="/used-in">
    <strong>Study a real application</strong>
    <span>See how the Phel language project structures production modules.</span>
  </a>
</div>

## Recommended journey

Follow this sequence once; use search and the task index after that:

1. **Get a working result:** complete the [Quickstart](https://gacela-project.com/docs/quickstart.md) and run `example.php`.
2. **Understand the boundary:** read [Facade](https://gacela-project.com/docs/facade.md) and [Factory](https://gacela-project.com/docs/factory.md) while following the call inward.
3. **Add real dependencies:** use the [dependency decision guide](https://gacela-project.com/docs/getting-dependencies.md), then add Provider or Config only when required.
4. **Make it production-ready:** add [tests](https://gacela-project.com/docs/testing.md), [static analysis](https://gacela-project.com/docs/static-analysis.md), and [health checks](https://gacela-project.com/docs/health-checks.md).
5. **Inspect a real system:** compare the result with the [Phel production case study](https://gacela-project.com/used-in.md).

::: tip Find an answer quickly
Press <kbd>⌘ K</kbd> on macOS or <kbd>Ctrl K</kbd> on Windows/Linux to search every page. For wiring questions, start with [Getting dependencies](https://gacela-project.com/docs/getting-dependencies.md) instead of browsing individual APIs.
:::

## The module boundary

| Class | Responsibility | Called by |
|---|---|---|
| [Facade](https://gacela-project.com/docs/facade.md) | The module's public API | Other modules and entry points |
| [Factory](https://gacela-project.com/docs/factory.md) | Internal object construction | The module's Facade and services |
| [Provider](https://gacela-project.com/docs/provider.md) | Cross-module and infrastructure dependencies | The module's Factory |
| [Config](https://gacela-project.com/docs/config.md) | Typed application settings | The module's Factory |

You do not need all four classes in every module. Start with a Facade and Factory; add a Provider when the module crosses a boundary, and a Config when it needs application settings.

## Design outside-in

Gacela works best when you follow the request from the caller into the module:

1. Write the controller, command, or script call you want to make.
2. Turn that call into a small Facade method.
3. Let the Factory construct the service that fulfills it.
4. Add a Provider or Config only when that service needs something outside the module.

This keeps the public API driven by real use cases instead of exposing internal classes speculatively. The [Quickstart](https://gacela-project.com/docs/quickstart.md) demonstrates the complete flow.

## Common tasks

- [Bootstrap an application](https://gacela-project.com/docs/bootstrap.md)
- [Configure container bindings and lifetimes](https://gacela-project.com/docs/bindings.md)
- [Resolve a service in framework-managed code](https://gacela-project.com/docs/inject.md)
- [Inspect modules and dependency cycles from the CLI](https://gacela-project.com/docs/gacela-script.md)
- [Add health checks](https://gacela-project.com/docs/health-checks.md)
- [Test with isolated container state](https://gacela-project.com/docs/testing.md)
- [Enforce boundaries with PHPStan or Psalm](https://gacela-project.com/docs/static-analysis.md)

## Documentation for coding agents

Every page has **Copy Markdown** and **View Markdown** actions above its title. Machine-readable entry points are also available:

- [`/llms.txt`](/llms.txt) — compact index with page descriptions
- [`/llms-full.txt`](/llms-full.txt) — the complete documentation in one context file
- Append `.md` to a page URL — for example, [`/docs/bootstrap.md`](/docs/bootstrap.md)
- Use **Copy agent prompt** on any page to copy a source-of-truth instruction with its Markdown URL

When prompting an agent, give it `https://gacela-project.com/llms.txt` for discovery or `https://gacela-project.com/llms-full.txt` when the entire documentation fits the task's context budget.

---

Source: https://gacela-project.com/docs/quickstart.md

# Quickstart

Gacela gives PHP modules a predictable public boundary without imposing rules on your domain model. This guide creates a complete module you can run from the command line.

**You will build:** one runnable entry point, one public module boundary, and one framework-independent service.

**Before you start:** use PHP 8.3 or newer and have [Composer](https://getcomposer.org/) available.

## Installation

Gacela 2.0 requires **PHP 8.3 or newer**. Install it from [Packagist](https://packagist.org/packages/gacela-project/gacela):

```bash
composer require gacela-project/gacela:^2.0
```

## Start with the code you want to run

Write the caller first. It defines the only API this module needs to expose: `greet()`.

```php [example.php]
<?php

declare(strict_types=1);

use Gacela\Framework\Gacela;
use Module\Facade;

require __DIR__ . '/vendor/autoload.php';

Gacela::bootstrap(__DIR__);

$facade = new Facade();

echo $facade->greet('Alice');
```

The flow behind that call will be:

```text
example.php → Facade → Factory → Greeter
```

Create the directories for those classes:

```bash
mkdir -p src/Module/Service
```

## 1. Expose the module through a Facade

The [Facade](https://gacela-project.com/docs/facade.md) is the module's public API. It delegates the request instead of containing business logic.

```php [src/Module/Facade.php]
<?php

declare(strict_types=1);

namespace Module;

use Gacela\Framework\AbstractFacade;

/**
 * @extends AbstractFacade<Factory>
 */
final class Facade extends AbstractFacade
{
    public function greet(string $name): string
    {
        return $this->getFactory()
            ->createGreeter()
            ->greet($name);
    }
}
```

Gacela resolves the sibling `Factory` automatically when `getFactory()` is called.

## 2. Construct the service in a Factory

The [Factory](https://gacela-project.com/docs/factory.md) owns object construction inside the module. This keeps construction details out of the Facade and the service itself.

```php [src/Module/Factory.php]
<?php

declare(strict_types=1);

namespace Module;

use Gacela\Framework\AbstractFactory;
use Module\Service\Greeter;

final class Factory extends AbstractFactory
{
    public function createGreeter(): Greeter
    {
        return new Greeter();
    }
}
```

## 3. Add the application service

`Greeter` is ordinary PHP. It does not extend or import anything from Gacela.

```php [src/Module/Service/Greeter.php]
<?php

declare(strict_types=1);

namespace Module\Service;

final class Greeter
{
    public function greet(string $name): string
    {
        return "Hi, {$name}!";
    }
}
```

## 4. Run it

Make sure Composer maps the `Module\\` namespace to `src/Module/`:

```json [composer.json]
{
    "autoload": {
        "psr-4": {
            "Module\\": "src/Module/"
        }
    }
}
```

Then rebuild the autoloader and run the entry point:

```bash
composer dump-autoload
php example.php
```

```text
Hi, Alice!
```

If you see that output, the complete resolution path works: Composer loaded the classes, Gacela found the module's Factory, and the Facade reached the service.

### If it does not run

| Error | Check |
|---|---|
| `Class "Module\\Facade" not found` | Confirm the PSR-4 mapping, then run `composer dump-autoload` again |
| Gacela cannot resolve `Factory` | Confirm `Factory.php` is beside `Facade.php`, both use `namespace Module`, and the class name is exactly `Factory` |
| `vendor/autoload.php` is missing | Run `composer install` from the project root |
| Your PHP version is rejected | Run `php -v`; Gacela 2.0 requires PHP 8.3+ |

That is a complete Gacela module. Add a [Provider](https://gacela-project.com/docs/provider.md) only when it needs another module or infrastructure service, and add a [Config](https://gacela-project.com/docs/config.md) only when it needs application settings.

::: tip Optional CLI setup
Applications using the optional CLI can install `symfony/console` 7 or 8 and run `vendor/bin/gacela init` to scaffold `gacela.php`. This example does not need that file.
:::

## Next steps

Continue according to what the module needs next:

- [Getting dependencies](https://gacela-project.com/docs/getting-dependencies.md) — choose the right wiring mechanism
- [Provider](https://gacela-project.com/docs/provider.md) — communicate with another module through its Facade
- [Config](https://gacela-project.com/docs/config.md) — expose application settings through typed getters
- [Bindings and container services](https://gacela-project.com/docs/bindings.md) — configure application-wide dependency policies
- [Testing](https://gacela-project.com/docs/testing.md) — bootstrap Gacela with isolated state in PHPUnit

---

Source: https://gacela-project.com/docs/getting-dependencies.md

# Getting dependencies

Choose wiring only after the caller and service reveal a dependency. Start with the relationship: **who needs what, and who owns it?** Gacela has several resolution tools because those relationships need different boundaries.

Ask these questions in order:

1. Is the dependency created inside this module? Use the Factory.
2. Is it owned by another module? Request that module's Facade through the Provider.
3. Is it an application-wide implementation policy? Add a binding.
4. Is the caller created by another framework? Use constructor injection or Service Map at that entry point.

Then use this table as the concise decision guide.

| Intent | Recommended path |
|---|---|
| Reach another module | From an entry point, `ServiceResolverAwareTrait` + `#[ServiceMap]`; from a Factory, expose the other module's Facade through the Provider |
| Build a collaborator inside the same module | A `create*()` Factory method, or `AbstractFactory::make()` for pure autowiring |
| Obtain infrastructure | `#[Provides]` in the Provider, or app-wide `addBinding()` for an interface |
| Collect several implementations | `tag()` for an unkeyed iterable; `addHandlerRegistry()` for keyed lookup |
| Read application configuration | Typed getters on the module Config |

## Reach another module

An entry-point class such as a controller or command declares the target Facade with `#[ServiceMap]` and supplies the magic accessor with `ServiceResolverAwareTrait`:

```php
use Gacela\Framework\ServiceResolver\ServiceMap;
use Gacela\Framework\ServiceResolverAwareTrait;

#[ServiceMap(method: 'getFacade', className: BillingFacade::class)]
final class SendInvoiceController
{
    use ServiceResolverAwareTrait;

    public function __invoke(): void
    {
        $this->getFacade()->sendInvoice();
    }
}
```

Inside a Factory, go through the module's Provider instead. Factories must not call another module's Facade directly:

```php
final class InvoiceProvider extends AbstractProvider
{
    #[Provides(BillingFacade::class)]
    public function billingFacade(Container $container): BillingFacade
    {
        return $container->getLocator()->get(BillingFacade::class);
    }
}

final class InvoiceFactory extends AbstractFactory
{
    public function createSender(): InvoiceSender
    {
        return new InvoiceSender(
            $this->getProvidedDependency(BillingFacade::class),
        );
    }
}
```

Cross-module access always targets the other module's Facade, never its Factory or internal services.

## Build inside the same module

Use an explicit Factory method when construction includes decisions:

```php
public function createInvoiceSender(): InvoiceSender
{
    return new InvoiceSender($this->createPdfRenderer());
}
```

When wiring is entirely type-driven, let the module container autowire it:

```php
public function createInvoiceSender(): InvoiceSender
{
    return $this->make(InvoiceSender::class);
}
```

`make()` honors bindings, contextual bindings, `#[Inject]`, `#[Singleton]`, `#[Factory]`, and `#[Lazy]`. Runtime overrides can be passed by constructor parameter name: `$this->make(Service::class, ['currency' => 'EUR'])`.

## Obtain infrastructure

Declare a module-local dependency in its Provider:

```php
final class PaymentProvider extends AbstractProvider
{
    #[Provides(PaymentGateway::class)]
    public function paymentGateway(): PaymentGateway
    {
        return new StripeGateway();
    }
}
```

Read it in the Factory with the class-string form, which static analysis can type:

```php
$gateway = $this->getProvidedDependency(PaymentGateway::class);
```

For an interface-to-implementation rule that applies across the application, configure a binding in `gacela.php`:

```php
$config->addBinding(PaymentGateway::class, StripeGateway::class);
```

## Collect implementations

Use tags when a consumer iterates every member:

```php
$config->tag(
    [NotEmptyValidator::class, EmailValidator::class],
    'validators',
);
```

Resolve the group in a Provider with `$container->tagged('validators')`. An app-wide tag reaches every module scope; a tag added from one module's Provider stays local to that module.

Use `addHandlerRegistry()` when the consumer selects one handler by key:

```php
$config->addHandlerRegistry(HandlerRegistry::class, [
    'email' => EmailHandler::class,
    'sms' => SmsHandler::class,
]);
```

A registry answers “which handler matches this key?” and throws on a miss. A tag answers “give me all implementations” and has no key.

## Read configuration

Expose intention-revealing methods from the module Config:

```php
final class BillingConfig extends AbstractConfig
{
    public function retryAttempts(): int
    {
        return $this->getInt('billing.retry-attempts', 3);
    }
}
```

Available protected getters are `getString()`, `getInt()`, `getFloat()`, `getBool()`, `getArray()`, and untyped `get()`.

## Specialized tools

These APIs remain supported; use them when their more specific behavior is what you need:

| Tool | Use it when |
|---|---|
| `addBindingIf()` | A plugin supplies a default the application may override |
| `addFactory()` | Every resolution needs a new instance |
| `addProtected()` | The value itself is a closure and must not be invoked |
| `addAlias()` | One service needs another identifier |
| `addLazy()` / `#[Lazy]` | Construction is expensive and the service may remain unused |
| `extendService()` | Resolution must return a decorated or replaced service |
| `afterResolving()` | A resolved object needs an idempotent setter or similar hook |
| `when()->needs()->give()` | One consumer needs a contextual implementation or scalar |
| `loadDefinitions()` | Wiring is generated, shared, or environment-specific |
| `addExternalService()` | Bootstrap must hand a framework-owned object to Gacela |

See [Bindings](https://gacela-project.com/docs/bindings.md), [Provider](https://gacela-project.com/docs/provider.md), and [Service Map](https://gacela-project.com/docs/service-map.md) for the complete APIs.

---

Source: https://gacela-project.com/docs/bootstrap.md

# Bootstrap

Call `Gacela::bootstrap()` once in each application entry point, before resolving a Facade. Pass the application root as the first argument and an optional `Closure(GacelaConfig)` as the second.

```php
<?php # index.php

use Gacela\Framework\Bootstrap\GacelaConfig;
use Gacela\Framework\Gacela;

require __DIR__ . '/vendor/autoload.php';

Gacela::bootstrap(__DIR__, static function (GacelaConfig $config): void {
    // Optional application-wide configuration.
});
```

## Choose where configuration lives

Use the bootstrap closure for entry-point-specific runtime values. Use `gacela.php` for shared, version-controlled application configuration. When both exist, Gacela combines them.

```php
<?php # gacela.php

use Gacela\Framework\Bootstrap\GacelaConfig;

return static function (GacelaConfig $config): void {
    // Shared application configuration.
};
```

## Environment-specific bootstrap

Set `APP_ENV` to load a matching file after `gacela.php`:

- `APP_ENV=dev` loads `gacela-dev.php`
- `APP_ENV=prod` loads `gacela-prod.php`
- `APP_ENV=staging` loads `gacela-staging.php`

The environment file may add or override settings from the default file.

Application config supports the same pattern; see [environment-specific config files](https://gacela-project.com/docs/config.md#config-files-for-different-environments).

::: info Extending a Gacela-based package
An application's `gacela.php` is combined with configuration discovered in vendor packages, allowing the application to override or extend package defaults.
:::

## GacelaConfig

`GacelaConfig` controls application-wide behavior. Keep this page focused on bootstrap concerns; use the dedicated references for deeper wiring:

- [Bindings](https://gacela-project.com/docs/bindings.md): bindings, factories, tags, resolution hooks, aliases, contextual bindings, and definitions
- [Getting dependencies](https://gacela-project.com/docs/getting-dependencies.md): which configuration mechanism to use for each intent
- [Extensions & Plugins](https://gacela-project.com/docs/extensions.md): plugins, extendService, extendGacelaConfig, handler registry
- [Module Customization](https://gacela-project.com/docs/customization.md): suffix types, project namespaces, events

### File cache

```php
enableFileCache(?string $dir = null);             // default: system temp directory
setFileCache(bool $enabled, ?string $dir = null); // default: system temp directory
```
The file cache is disabled by default. Enable it in production to persist resolved class names and merged configuration between requests.

A configured directory is relative to the application root. A leading `/` is still rooted under the app; use `GACELA_CACHE_DIR` for an external absolute path. Cache filenames include an application-root hash, so applications may safely share the default system temporary directory.

```php
<?php # gacela.php

return static function (GacelaConfig $config): void {
    $config->enableFileCache('.gacela/cache');
};
```

The project config may also control the cache:

```php
<?php # config/default.php

use Gacela\Framework\ClassResolver\Cache\GacelaFileCache;

return [GacelaFileCache::KEY_ENABLED => true];
```

### Application config

```php
addAppConfig(string $path, string $pathLocal = '', $reader = null);
```

`addAppConfig()` registers config sources. PHP is the default format; custom formats require a `ConfigReaderInterface` implementation.

#### PHP config files
```php
<?php # gacela.php

return static function (GacelaConfig $config): void {
    $config->addAppConfig(
        path: 'config/*.php',
        pathLocal: 'config/local.php',
        reader: PhpConfigReader::class,
    );
};
```

- `path` supports [`glob()`](https://www.php.net/manual/en/function.glob.php) patterns and loads matching files in order.
- `pathLocal` loads last, making it suitable for ignored developer-specific overrides.
- `reader` parses the source and must implement `ConfigReaderInterface`.

Register multiple formats when the application needs them:

```php
<?php # gacela.php

return static function (GacelaConfig $config): void {
    $config->addAppConfig('config/.env', '', EnvConfigReader::class);
    $config->addAppConfig('config/*.custom', '', CustomConfigReader::class);
    $config->addAppConfig('config/*.php', 'config/local.php');
};
```

For the conventional PHP setup:
```php
<?php # index.php
Gacela::bootstrap(__DIR__, GacelaConfig::defaultPhpConfig());
```

### Application module paths

```php
setAppModulePaths(array $paths): self
```

Restrict which directories are scanned when Gacela discovers application modules. This scan powers the console commands `list:modules`, `debug:modules`, `cache:warm`, and `doctor`.

```php
<?php # gacela.php

return static function (GacelaConfig $config): void {
    $config->setAppModulePaths(['src']);
};
```

- Paths can be absolute or relative to the application root
- Missing paths are skipped with a warning at scan time
- When unset, the entire application root is scanned

On large code bases this narrows the scan to your module directories, so `cache:warm` and the discovery commands skip unrelated folders.

### Container scopes

Gacela creates one application container and a child scope for each module's Provider registrations. App-wide wiring runs once per bootstrap. Provider keys remain private to their module, and app-wide bindings resolve within the requesting module's scope.

## Production baseline

Start with the smallest shared configuration that matches the application. Add bindings, plugins, listeners, or custom discovery only when a concrete requirement appears.

```php
<?php # gacela.php

use Gacela\Framework\Bootstrap\GacelaConfig;

return static function (GacelaConfig $config): void {
    $config
        ->addAppConfig('config/*.php', 'config/local.php')
        ->setAppModulePaths(['src'])
        ->enableFileCache('.gacela/cache');
};
```

## Runtime access

### Gacela::rootDir()

Returns the application root passed to `bootstrap()`.

### Gacela::get(string::class)

Returns a registered service or `null` when it is missing.

### Gacela::getRequired(string::class)

Returns a registered service or throws `ServiceNotFoundException`. Missing-service errors include close-name suggestions.

```php
try {
    $facade = Gacela::getRequired(UserFacade::class);
} catch (ServiceNotFoundException $e) {
    // Typo'd service name? The message contains suggestions.
}
```

`Locator::getRequiredSingleton()` is the equivalent shortcut when working with the locator directly.

### Gacela::container()

Returns the application container. Prefer Facades in application code; direct access is intended for tooling and focused tests.

### Gacela::resetCache()

Clears in-process and file-backed resolution caches so the next `Gacela::bootstrap()` starts clean. It does **not** clear an external backend registered through `CacheableConfig::setStorage()`; use the method-cache API for that storage. See [`resetInMemoryCache()`](https://gacela-project.com/docs/customization.md#reset-inmemorycache) for the bootstrap-time equivalent.

---

Source: https://gacela-project.com/docs/upgrading.md

# Upgrade from Gacela 1.21 to 2.0

Gacela 2.0 raises the PHP floor, moves to `gacela-project/container` 2.x, removes three deprecated aliases, and makes undeclared pillar accessors visible to static analysis. Version 1.21.0 is the final 1.x release.

## Before upgrading

Prepare the application while it still runs on 1.21:

```bash
composer require gacela-project/gacela:^1.21
vendor/bin/gacela doctor
vendor/bin/gacela cache:clear
```

Run the test suite with `error_reporting(E_ALL)` so Gacela deprecations are visible. Search explicitly for the trait removal, which cannot emit a use-time deprecation:

```bash
rg "DocBlockResolverAwareTrait" src/
```

Then require the new major:

```bash
composer require gacela-project/gacela:^2.0
```

## Requirements

- PHP is now **8.3 or newer**, up from 8.1.
- `gacela-project/container` is now `^2.0.2`.
- Symfony development integrations support `^7.0 || ^8.0`; projects pinned to Symfony 6 must upgrade.

## Removed APIs

| Removed in 2.0 | Replacement |
|---|---|
| `AbstractDependencyProvider` | `AbstractProvider` |
| `GacelaConfig::addMappingInterface()` | `GacelaConfig::addBinding()` |
| `DocBlockResolverAwareTrait` | `ServiceResolverAwareTrait` |

### Rename dependency providers completely

Change the class, parent, and filename:

```diff
-// src/MyModule/MyModuleDependencyProvider.php
-final class MyModuleDependencyProvider extends AbstractDependencyProvider
+// src/MyModule/MyModuleProvider.php
+final class MyModuleProvider extends AbstractProvider
```

The filename matters because Gacela discovers pillars by convention. A class renamed without its file silently stops resolving. Running `doctor` on 1.21 detects the mismatch before the old resolver is removed.

`provideModuleDependencies()` remains the imperative registration method. `#[Provides]` remains the attribute-first alternative.

### Rename bindings and the resolver trait

```diff
-$config->addMappingInterface(MyInterface::class, MyImplementation::class);
+$config->addBinding(MyInterface::class, MyImplementation::class);

-use Gacela\Framework\DocBlockResolverAwareTrait;
+use Gacela\Framework\ServiceResolverAwareTrait;
```

Both are mechanical renames with the same behavior.

## Declare pillar accessors

The PHPStan suppression for undeclared magic accessors is gone. Declare each accessor with `#[ServiceMap]`:

```php
use Gacela\Framework\ServiceResolver\ServiceMap;
use Gacela\Framework\ServiceResolverAwareTrait;

#[ServiceMap(method: 'getFacade', className: BillingFacade::class)]
final class BillingController
{
    use ServiceResolverAwareTrait;
}
```

A `@method BillingFacade getFacade()` annotation still helps IDEs, but runtime resolution through docblocks or scanned `use` statements is deprecated in 2.0 and will be removed in 3.0. Add the attribute even when retaining the docblock.

Psalm users must register the 2.0 plugin separately from the existing XInclude:

```xml
<plugins>
    <pluginClass class="Gacela\Psalm\Plugin"/>
</plugins>
```

## Container compatibility

Gacela's container now decorates the final 2.x container and continues to implement `ContainerInterface`. Code type-hinting the concrete inner container should accept its interface instead:

```diff
-function configure(\Gacela\Container\Container $container): void
+function configure(\Gacela\Container\ContainerInterface $container): void
```

Module containers are now scopes of one application container. App-wide configuration is walked once per bootstrap, while Provider registrations and instances remain isolated per module scope.

## Other targeted changes

- `ConsoleFacade::getContainerStats()` and `ConsoleFactory::getContainerStats()` now return a final readonly `ContainerStats` object, not an array. Use properties such as `registeredServices` and `processMemoryBytes`, plus `processMemoryFormatted()`; this replaces the misleading `memoryUsageFormatted()` name.
- `CacheWarmedEvent::failedCount()` now counts actual resolution failures. Use the new `skippedCount()` for pillar classes a module simply does not contain.
- Typed class constants on `AbstractSetupGacela` and `ConfigInterface` can expose incompatible overrides at compile time.
- `Gacela::resetCache()` no longer clears a cache backend registered through `CacheableConfig::setStorage()`.

## New in 2.0

- `GacelaConfig::loadDefinitions()` loads wiring from arrays, PHP files, or JSON files.
- `GacelaConfig::afterResolving()` runs idempotent callbacks after top-level container resolution.
- `GacelaConfig::tag()` groups services into lazy iterables.
- `Gacela\Framework\Attribute\Inject` is the preferred import and supports constructor parameters, properties, and setters.
- `#[Lazy]` is honored by `AbstractFactory::make()`; native lazy behavior requires PHP 8.4 and falls back safely to eager construction on 8.3.
- Dependency-tree output now follows applied bindings and marks nodes as `binding`, `instance`, `autowired`, or `unresolvable`.

After migration, run the test suite, PHPStan or Psalm, and `vendor/bin/gacela doctor --strict`.

---

Source: https://gacela-project.com/docs/facade.md

# Facade

The [Facade](https://en.wikipedia.org/wiki/Facade_pattern) is the **entry point** of your module. It exposes what the module can do through a clean, public API while hiding the internal classes, services, and wiring behind simple method calls.

::: tip Why use a Facade?
Other modules, controllers, and commands never reach into your module's internals. They call the Facade, which delegates to the [Factory](https://gacela-project.com/docs/factory.md) to build the right objects and run the logic. This keeps your module's domain encapsulated and easy to refactor.
:::

## Start from the caller

Write the call you want consumers to make before designing the implementation. The caller should know the Facade and nothing behind it.

```php [app.php]
<?php

declare(strict_types=1);

use App\Comment\CommentFacade;
use Gacela\Framework\Gacela;

require __DIR__ . '/vendor/autoload.php';

Gacela::bootstrap(__DIR__);

$score = (new CommentFacade())->getSpamScore('Lorem ipsum!');

echo "Spam score: {$score}" . PHP_EOL;
```

[View the complete entry point](https://github.com/gacela-project/gacela-example/blob/main/comment-spam-score/app.php).

## Define the boundary

Turn the caller's desired operation into a Facade method. Extend `AbstractFacade` and delegate the implementation through `getFactory()`.

```php [src/Comment/CommentFacade.php]
<?php

declare(strict_types=1);

namespace App\Comment;

use Gacela\Framework\AbstractFacade;

/**
 * @extends AbstractFacade<CommentFactory>
 */
final class CommentFacade extends AbstractFacade
{
    public function getSpamScore(string $comment): int
    {
        return $this->getFactory()
            ->createSpamChecker()
            ->getSpamScore($comment);
    }
}
```

[View the complete Facade](https://github.com/gacela-project/gacela-example/blob/main/comment-spam-score/src/Comment/CommentFacade.php). Keep this API small: add a method because a real caller needs the capability, not because an internal service happens to expose it.

## Accessing the Facade from controllers and commands

In your infrastructure layer (controllers, CLI commands, etc.) you often can't extend `AbstractFacade`. Use `ServiceResolverAwareTrait` together with the `#[ServiceMap]` attribute to let Gacela resolve the Facade lazily through the Locator singleton. No constructor injection needed.

### Recommended: `#[ServiceMap]` attribute

```php
<?php

use Gacela\Framework\ServiceResolver\ServiceMap;
use Gacela\Framework\ServiceResolverAwareTrait;

#[ServiceMap(method: 'getFacade', className: RunFacade::class)]
final class TestCommand extends Command
{
    use ServiceResolverAwareTrait;

    protected function execute(InputInterface $in, OutputInterface $out): int
    {
        // getDependencies() is a method on RunFacade
        $dependencies = $this->getFacade()->getDependencies($paths);
        // ...
    }
}
```

`#[ServiceMap]` is repeatable. Declare as many resolvable services as the class needs. Full reference: [Service Map](https://gacela-project.com/docs/service-map.md).

### Migration aid: DocBlock `@method`

Keep a `@method` annotation alongside the attribute when your IDE needs it. Using a docblock as the **runtime** source still works in 2.0, but raises `E_USER_DEPRECATED` and will be removed in 3.0.

```php
<?php

use Gacela\Framework\ServiceResolverAwareTrait;

/**
 * @method RunFacade getFacade()
 */
final class TestCommand extends Command
{
    use ServiceResolverAwareTrait;

    protected function execute(InputInterface $in, OutputInterface $out): int
    {
        $dependencies = $this->getFacade()->getDependencies($paths);
        // ...
    }
}
```

Add the attribute even when retaining the docblock:

```php
/** @method RunFacade getFacade() */
#[ServiceMap(method: 'getFacade', className: RunFacade::class)]
final class TestCommand extends Command
{
    use ServiceResolverAwareTrait;
}
```

::: warning Removed in 2.0
`DocBlockResolverAwareTrait` no longer exists. Replace it with `ServiceResolverAwareTrait`; the API is otherwise unchanged. See [Upgrade to 2.0](https://gacela-project.com/docs/upgrading.md).
:::

#### Direct construction or Service Map?

Construct a Facade directly when your code owns the entry point, as in the Quickstart. Use `#[ServiceMap]` when another framework creates the controller or command and constructor injection is not practical. Service Map resolves through Gacela's Locator and reuses the registered Facade.

#### Resolution behavior

`ServiceResolverAwareTrait` maps the declared method to the class in `#[ServiceMap]` and resolves it lazily. It works for any Gacela-resolvable class, although cross-module calls should target a Facade.

---

Source: https://gacela-project.com/docs/factory.md

# Factory

The [Factory](https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)) is responsible for **creating the internal objects** of your module and wiring their dependencies, pulling values from [Config](https://gacela-project.com/docs/config.md) and services from the [Provider](https://gacela-project.com/docs/provider.md).

::: tip Key points
- The Factory creates and assembles the classes inside your module
- Only the [Facade](https://gacela-project.com/docs/facade.md) accesses the Factory (via `getFactory()`)
- Dependencies from other modules come through the [Provider](https://gacela-project.com/docs/provider.md), not the Factory
:::

## Start from the object you need

After a Facade delegates an operation, design the application or domain service that will fulfill it. Its constructor makes the required collaborators explicit:

```php [src/Comment/Domain/SpamChecker.php]
<?php

declare(strict_types=1);

namespace App\Comment\Domain;

use Symfony\Contracts\HttpClient\HttpClientInterface;

final class SpamChecker
{
    public function __construct(
        private HttpClientInterface $client,
        private string $endpoint,
    ) {}

    public function getSpamScore(string $comment): int
    {
        // Business logic using $this->client and $this->endpoint.
        return 0;
    }
}
```

## Construct it in the Factory

Now make the Factory satisfy that constructor. Configuration and wiring stay here instead of leaking into the service or Facade.

```php [src/Comment/CommentFactory.php]
<?php

declare(strict_types=1);

namespace App\Comment;

use App\Comment\Domain\SpamChecker;
use Gacela\Framework\AbstractFactory;
use Symfony\Contracts\HttpClient\HttpClient;

/**
 * @extends AbstractFactory<CommentConfig>
 */
final class CommentFactory extends AbstractFactory
{
    public function createSpamChecker(): SpamChecker
    {
        return new SpamChecker(
            HttpClient::create(),
            $this->getConfig()->getSpamCheckerEndpoint(),
        );
    }
}
```

[View the complete Factory](https://github.com/gacela-project/gacela-example/blob/main/comment-spam-score/src/Comment/CommentFactory.php).

## Auto-wiring dependencies into the Factory

Gacela auto-wires Factory constructor dependencies. Concrete classes are instantiated automatically (recursively resolving their own dependencies). For interfaces, you need to tell Gacela which implementation to use by defining a [binding](https://gacela-project.com/docs/bindings.md):

```php
<?php # gacela.php

return function (GacelaConfig $config) {
    // Class binding: Gacela instantiates Concrete (and auto-wires its deps)
    $config->addBinding(InterfaceToConcrete::class, Concrete::class);

    // Callable binding: lazy-loaded, you control the instantiation
    $config->addBinding(InterfaceToCallable::class, fn() => new Concrete());
};
```

The difference between these two styles:

- **Class binding** (`Concrete::class`): Gacela creates a new instance on the fly, auto-wiring its constructor dependencies recursively
- **Callable binding** (`fn() => ...`): You control instantiation. The closure is lazy-loaded, it only runs when the dependency is needed

Real example: [symfony-gacela-example/gacela.php](https://github.com/gacela-project/symfony-gacela-example/blob/main/gacela.php#L16)

For a per-parameter alternative to constructor auto-wiring, see the [`#[Inject]` attribute](https://gacela-project.com/docs/inject.md).

## Sharing a single instance

Plain `create...()` methods build a fresh object on every call. When a dependency should instead be built once and reused, use `singleton()`:

```php
protected function singleton(string $key, callable $creator): mixed;
```

It memoises the result of `$creator` under `$key` and returns the **same instance** on every later call within the module. The creator is lazy — it only runs on first access.

```php
<?php # src/Comment/CommentFactory.php

final class CommentFactory extends AbstractFactory
{
    public function createSpamChecker(): SpamChecker
    {
        return $this->singleton(
            SpamChecker::class,
            fn (): SpamChecker => new SpamChecker(
                HttpClient::create(),
                $this->getConfig()->getSpamCheckerEndpoint(),
            ),
        );
    }
}
```

::: tip Key points
- `create...()` methods build a new instance every call; `singleton()` builds once and reuses it
- `singleton()` is generic (`@template T`, `@return T`), so its inferred return type matches `$creator` without a cast
:::

---

Source: https://gacela-project.com/docs/provider.md

# Provider

The Provider handles **cross-module dependencies**. When your module needs something from another module, the Provider is where you wire that connection, always through the other module's [Facade](https://gacela-project.com/docs/facade.md).

::: tip Factory vs Provider
- **Factory** → creates objects *inside* your module (intra-module)
- **Provider** → brings in dependencies *from other modules* (inter-module)
:::

::: warning `register()` is final
Do not override `AbstractProvider::register()`. Register services through `#[Provides]` or `provideModuleDependencies()`.
:::

## Start from the consuming service

Let the service constructor show that the Sales module needs the Comment module. The Factory asks for the other module's Facade interface; it does not decide how to locate it.

```php [src/Sales/SalesFactory.php]
<?php

declare(strict_types=1);

namespace App\Sales;

use App\Comment\CommentFacadeInterface;
use Gacela\Framework\AbstractFactory;

final class SalesFactory extends AbstractFactory
{
    public function createOrderCommentSaver(): OrderCommentSaver
    {
        return new OrderCommentSaver(
            $this->getProvidedDependency(CommentFacadeInterface::class),
        );
    }
}
```

## Satisfy the boundary in the Provider

Now connect that interface to the Comment module's Facade. `#[Provides]` keeps the dependency local to the Sales module and resolves it lazily.

```php [src/Sales/SalesProvider.php]
<?php

declare(strict_types=1);

namespace App\Sales;

use App\Comment\CommentFacade;
use App\Comment\CommentFacadeInterface;
use Gacela\Framework\AbstractProvider;
use Gacela\Framework\Attribute\Provides;
use Gacela\Framework\Container\Container;

final class SalesProvider extends AbstractProvider
{
    #[Provides(CommentFacadeInterface::class)]
    public function commentFacade(Container $container): CommentFacadeInterface
    {
        return $container->getLocator()->getRequired(CommentFacade::class);
    }
}
```

## Complete call path

The caller still sees only the Sales Facade. The dependency becomes visible only when following the implementation inward: **Facade → Factory → Provider → Comment Facade**.

```php
<?php # src/Sales/SalesFacade.php

namespace App\Sales;

use Gacela\Framework\AbstractFacade;

/**
 * @method SalesFactory getFactory()
 */
final class SalesFacade extends AbstractFacade
{
    public function saveComment(Comment $comment): int
    {
        return $this->getFactory()
            ->createOrderCommentSaver()
            ->save($comment);
    }
}
```

## More `#[Provides]` patterns

`#[Provides]` also accepts string IDs and non-Facade services. Each method is wrapped in a lazy closure and receives `Container` automatically when declared in the signature.

```php
<?php # src/Sales/SalesProvider.php

use Gacela\Framework\AbstractProvider;
use Gacela\Framework\Attribute\Provides;
use Gacela\Framework\Container\Container;

final class SalesProvider extends AbstractProvider
{
    #[Provides('COMMANDS')]
    public function commands(): array
    {
        return [new SyncCommand()];
    }

    #[Provides('FACADE_COMMENT')]
    public function commentFacade(Container $container): CommentFacade
    {
        return $container->getLocator()->get(CommentFacade::class);
    }
}
```

With `#[Provides]`, `provideModuleDependencies()` becomes non-abstract. Providers can go attribute-only or mix both styles.

### Mixing with `provideModuleDependencies()`

You can use attributes alongside the traditional method. Attribute-registered services are resolved first, then `provideModuleDependencies()` runs as before:

```php
final class SalesProvider extends AbstractProvider
{
    #[Provides('COMMANDS')]
    public function commands(): array
    {
        return [new SyncCommand()];
    }

    public function provideModuleDependencies(Container $container): void
    {
        $container->set('LEGACY_SERVICE', fn () => new LegacyAdapter());
    }
}
```

---

Source: https://gacela-project.com/docs/config.md

# Config

Config turns application key-values into typed module settings. The [Factory](https://gacela-project.com/docs/factory.md) uses those settings while constructing services, keeping file and environment access out of domain code.

::: info
The examples below use PHP config files by default (`config/*.php`). See [Bootstrap > Application Config](https://gacela-project.com/docs/bootstrap.md#application-config) for other formats and custom readers.
:::

## The config file

Define the application values:
```php
<?php # config/default.php

return [
    'AKISMET-KEY' => 'your-akismet-key',
];
```

## Expose typed module settings

Wrap raw keys in methods named for their meaning inside the module:
```php
<?php # src/Comment/CommentConfig.php

use Gacela\Framework\AbstractConfig;

final class CommentConfig extends AbstractConfig
{
    public function getSpamCheckerEndpoint(): string
    {
        return sprintf(
            'https://%s.rest.akismet.com/1.1/comment-check',
            $this->getString('AKISMET-KEY'),
        );
    }
}
```

## Typed config accessors

`AbstractConfig` provides typed accessors with validation and static-analysis-friendly return types:

| Method | Returns |
| --- | --- |
| `getString(string $key, ?string $default = null)` | `string` |
| `getInt(string $key, ?int $default = null)` | `int` |
| `getFloat(string $key, ?float $default = null)` | `float` |
| `getBool(string $key, ?bool $default = null)` | `bool` |
| `getArray(string $key, ?array $default = null)` | `array` |

```php
<?php # src/Comment/CommentConfig.php

use Gacela\Framework\AbstractConfig;

final class CommentConfig extends AbstractConfig
{
    public function getApiKey(): string
    {
        return $this->getString('AKISMET-KEY');   // required: throws if missing or non-string
    }

    public function getMaxRetries(): int
    {
        return $this->getInt('MAX_RETRIES', 3);    // optional: 3 when absent
    }
}
```

::: info
`$default` is `null` by default, which makes the key **required**: a missing key throws immediately instead of failing silently later on. Pass a non-null `$default` to make the key optional — it's returned whenever the key is absent.
:::

::: tip Fail fast on the wrong type
Unlike a cast, typed accessors throw when a value has the wrong type. `getFloat()` also accepts integers. The generic `get()` remains available for other value shapes.
:::

## Use Config from the Factory

The Factory passes typed settings into ordinary PHP services:
```php
<?php # src/Comment/CommentFactory.php

use Gacela\Framework\AbstractFactory;

/**
 * @extends AbstractFactory<CommentConfig>
 */
final class CommentFactory extends AbstractFactory
{
    public function createSpamChecker(): SpamChecker
    {
        return new SpamChecker(
            HttpClient::create(),
            $this->getConfig()->getSpamCheckerEndpoint(),
        );
    }
}
```

## The Facade uses the Factory

The Factory is used by the module's Facade, completing the chain: **Facade → Factory → Config**:

```php
<?php # src/Comment/CommentFacade.php

namespace App\Comment;

use Gacela\Framework\AbstractFacade;

/**
 * @extends AbstractFacade<CommentFactory>
 */
final class CommentFacade extends AbstractFacade
{
    public function getSpamScore(string $comment): int
    {
        return $this->getFactory()
            ->createSpamChecker()
            ->getSpamScore($comment);
    }
}
```

## Config files for different environments

Gacela loads a file suffixed with the current `APP_ENV` after the default source:
```php
<?php
Gacela::bootstrap($appRootDir, function (GacelaConfig $config): void {
    $config->addAppConfig('config/default.php');
});
```

```php
<?php # config/default.php

return [
    'AKISMET-KEY' => 'default-akismet-key',
];
```

```php
<?php # config/default-prod.php

return [
    'AKISMET-KEY' => 'production-akismet-key',
];
```

The resolved value for `'AKISMET-KEY'` depends on the environment:
- No `APP_ENV` set → `default-akismet-key`
- `APP_ENV=prod` → `production-akismet-key` (overrides the default)

## Inspecting the merged config

`Config::getInstance()->getAllValues()` returns the whole merged configuration as a key-value array — every `config/*.php` file plus environment overrides, already resolved. The [`debug:config`](https://gacela-project.com/docs/gacela-script.md#debug-config) command prints the same data as a table.

---

Source: https://gacela-project.com/docs/bindings.md

# Bindings and container services

Use application-wide bindings when a dependency policy applies across modules. For a dependency owned by one module, prefer its [Provider](https://gacela-project.com/docs/provider.md). All bindings are configured through `GacelaConfig`, either in `gacela.php` or the `Gacela::bootstrap()` closure.

| Need | API | Lifetime |
|---|---|---|
| Map an interface or ID to a service | `addBinding()` | Shared in its container scope |
| Create a new value for every resolution | `addFactory()` | New instance |
| Defer an expensive factory | `addLazy()` | New instance; deferred |
| Store a closure as a value | `addProtected()` | The closure itself |
| Use a different implementation for one consumer | `when()->needs()->give()` | Follows the supplied service |
| Collect implementations | `tag()` | Lazy iterable |

## addBinding

```php
addBinding(string $key, string|object|callable $value);
```

Define a map between a type (class or interface) and the concrete class that you want to create (or use) when a certain type is found during the process of **auto-wiring** in a Gacela `Plugin` or `Locator's container` from any `Provider`.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->addBinding(AbstractString::class, StringClass::class);
  $config->addBinding(ClassInterface::class, new ConcreteClass(/* args */));
  $config->addBinding(ComplexInterface::class, new class() implements Foo {/** logic */});
  $config->addBinding(FromCallable::class, fn() => new StringClass('From callable'));
};
```

In the example above, whenever `AbstractString` is found then `StringClass` will be resolved.

### Runtime values from bootstrap

```php
addExternalService(string $key, $value);
```

Use external services to share runtime objects between the bootstrap closure and `gacela.php`. For example:

```php
<?php # index.php

$instance = ...;

Gacela::bootstrap(__DIR__, function (GacelaConfig $config) use ($instance) {
  $config->addExternalService('concreteClass', ConcreteClass::class);
  $config->addExternalService('concreteInstance', $instance);
});
```

Read the same instance from `gacela.php`:
```php
<?php # gacela.php

return static function (GacelaConfig $config): void {
  $instance = $config->getExternalService('concreteInstance');

  $config->addBinding(AnInterface::class, $instance);
  $config->addBinding(AnotherInterface::class, $instance);
};
```

In the example above, both `AnInterface` and `AnotherInterface` resolve to the same shared `$instance` pulled from `getExternalService('concreteInstance')`.

## Factory Services

```php
addFactory(string $id, Closure $factory);
```

Unlike regular bindings (which are singletons), factory services return a new instance every time they are resolved from the container.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->addFactory('session', fn () => new SessionHandler());
};
```

Every call to `$container->get('session')` returns a fresh `SessionHandler`. The closure may type-hint `Container` to resolve its own dependencies.

## Lazy Services

```php
addLazy(string $id, Closure $factory);
```

Runtime behaviour is the same as `addFactory` — the closure is deferred out of bootstrap and runs on **every** resolve, returning a new instance each time — but the name documents the intent: skip building an expensive service until something first asks for it.

```php
<?php # gacela.php

use Gacela\Framework\Container\Container;

return function (GacelaConfig $config) {
  $config->addLazy(ReportBuilder::class, fn (Container $c) =>
    new ReportBuilder($c->get(DatabaseInterface::class))
  );
};
```

Nothing is built at bootstrap; the first `$container->get(ReportBuilder::class)` invokes the closure, and each later resolve builds a fresh instance. Reach for `addLazy` over `addFactory` when the intent is deferring a costly construction; they are otherwise interchangeable.

Gacela 2.0 also honors the container's `#[Lazy]` class attribute and `Container::lazy()`. These return an instance whose constructor is deferred until the object is used on PHP 8.4+. On PHP 8.3 the same declaration is accepted but construction is eager. Unlike `addLazy()`, the class-level lazy service follows the class's normal lifetime rather than acting as a fresh-instance factory.

```php
use Gacela\Container\Attribute\Lazy;

#[Lazy]
final class ExpensiveReport
{
    // ...
}
```

The attribute is honored by normal container resolution and `AbstractFactory::make()`.

## Protected Services

```php
addProtected(string $id, Closure $service);
```

Store a closure **without invoking it**. Useful for callable configurations or lazy factories you want to trigger by hand.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->addProtected('db.factory', fn () => new Database());
};
```

```php
$factory = $container->get('db.factory'); // the closure itself
$db      = $factory();                    // invoke when needed
```

Protected services cannot be extended via `extendService()`.

## Resolution hooks

```php
afterResolving(string $id, Closure $callback);
```

Run a callback against a resolved object without replacing it:

```php
$config->afterResolving(
    LoggerAwareInterface::class,
    static fn (LoggerAwareInterface $service) => $service->setLogger($logger),
);
```

The id may be an interface, so one hook can cover every implementation. Hooks fire in registration order for top-level `get()`, `getOrFail()`, and `make()` resolutions, but not for a nested constructor dependency.

A hook runs **once per resolution, not once per instance**. Fetching a shared service three times runs the callback three times on the same object, so callbacks must be safe to repeat. A callback that throws evicts the affected instance. Use `extendService()` when you need to replace or decorate the returned object, and an event listener when you only need to observe resolution.

## Service Aliases

```php
addAlias(string $alias, string $id);
```

Reference the same service with a different name (useful for short names or backward-compatibility).

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->addBinding(LoggerInterface::class, FileLogger::class);
  $config->addAlias('logger', LoggerInterface::class);
};
```

Both `$container->get(LoggerInterface::class)` and `$container->get('logger')` resolve to the same instance.

## Contextual Bindings

```php
when(string|array $concrete)->needs(string $abstract)->give(string|object|callable $concrete);
```

Provide different implementations of an interface depending on **which class is requesting it**.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->when(UserController::class)
    ->needs(LoggerInterface::class)
    ->give(FileLogger::class);

  $config->when(AdminController::class)
    ->needs(LoggerInterface::class)
    ->give(DatabaseLogger::class);

  // Bind multiple consumers at once
  $config->when([ApiController::class, WebController::class])
    ->needs(CacheInterface::class)
    ->give(RedisCache::class);
};
```

Contextual bindings win over the global `addBinding()` for the same interface. For a per-parameter alternative driven by an attribute, see [`#[Inject]`](https://gacela-project.com/docs/inject.md).

### Binding scalar parameters by name

```php
when(string $concrete)->needs(string $parameterName)->give(mixed $value);
```

`needs()` accepts a parameter name string of the form `'$parameterName'` (note the leading `$`), binding a scalar value to that constructor parameter **by name** instead of by type.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->when(RetryingHttpClient::class)
    ->needs('$maxRetries')   // constructor parameter named $maxRetries
    ->give(30);              // inject the scalar 30
};
```

Class and interface names passed to `needs()` bind by type; a `'$name'` string binds that scalar constructor parameter by name instead. `give()` accepts the scalar directly (int, string, bool, array, etc.) and injects it as-is.

Contextual bindings apply to Gacela pillar classes (Factories, Configs, and Providers) as well as ordinary autowired classes.

## Definitions as data

`loadDefinitions()` registers wiring from an inline array, a PHP file returning an array, or a JSON file:

```php
$config->loadDefinitions([
    LoggerInterface::class => FileLogger::class,
    Database::class => ['singleton' => DatabasePool::class],
    'db.dsn' => ['value' => 'pgsql://localhost/app'],
    'logger' => ['alias' => LoggerInterface::class],
    Metrics::class => [
        'singleton' => Metrics::class,
        'tags' => ['reporters'],
    ],
]);

$config->loadDefinitions(__DIR__ . '/config/services.json');
```

Sources apply in declaration order and **after** imperative registrations, so later sources override earlier ones and definitions override `addBinding()`. Tags accumulate instead of replacing prior entries. Paths are used exactly as passed, so use `__DIR__`; missing, unreadable, or invalid files throw.

Definitions loaded through `GacelaConfig` are app-wide. A Provider can keep definitions within its own module scope with `$container->load([...])` or `$container->loadFile(__DIR__ . '/services.php')`. Each registered id emits `BindingRegisteredEvent`, like an imperative binding. YAML is not built in; parse it yourself and pass the resulting array:

```php
$config->loadDefinitions(Yaml::parseFile(__DIR__ . '/services.yaml'));
```

## Service tags

Group services under a label when a consumer needs every implementation:

```php
$config->tag(
    [NotEmptyValidator::class, EmailValidator::class],
    'validators',
);
```

Resolve the iterable with `$container->tagged('validators')`. `taggedByKey()` and `taggedKeys()` are also forwarded by Gacela 2.0. App-wide tags reach every module scope; a tag added with `$container->tag()` from a Provider stays local to that module. Repeated registrations accumulate and duplicate ids are yielded once.

Use tags for an unkeyed set you iterate. Use [`addHandlerRegistry()`](https://gacela-project.com/docs/extensions.md#handler-registry) when callers select one handler by business key.

## Advanced container surface

Gacela 2.0 forwards the complete container 2.x API. Most applications should use the higher-level configuration above, but advanced integrations can call:

- `provides()`, `taggedByKey()`, `taggedKeys()`, `lazy()`, and `createScope()`.
- `writeCompiledCache()`, `writeCompiledFactories()`, `useCompiledFactories()`, and `compileReport()` for opt-in compiled constructor plans.
- `getStats()` for the legacy untyped array or `stats()` for the stable `ContainerStats` object.

Compiled plans are intentionally off by default: loading a 300-class plan file measured slower than reflecting those classes in the 2.0 release tests. The shared in-process `PlanCache` removes repeated reflection across module scopes without disk I/O. `resetStaticCaches()` is available for explicit low-level cleanup; normal application code should use `Gacela::resetCache()` or `cache:clear`.

## Array access on the container

```php
Container implements ArrayAccess
```

The main [`Container`](https://gacela-project.com/docs/bootstrap.md#gacela-container) implements PHP's `ArrayAccess`, giving terse sugar over the usual `get()` / `set()` / `has()` operations.

```php
<?php

$container = Gacela::container();

$container[LoggerInterface::class] = FileLogger::class; // assignment   → register a binding
$logger = $container[LoggerInterface::class];           // offsetGet    → resolve the service
isset($container[LoggerInterface::class]);              // offsetExists → can get() resolve it?
unset($container[LoggerInterface::class]);              // offsetUnset  → remove the binding
```

It is purely ergonomic. In container 2.x, `has()` follows PSR-11 semantics: it returns true when `get()` can resolve the id, including an autowirable unregistered class. Use `provides()` when you specifically need to know whether this container owns a binding or instance.

---

Source: https://gacela-project.com/docs/inject.md

# Inject attribute

Use `#[Inject]` when ordinary type-based autowiring cannot express the dependency: to force a concrete implementation, mark container-owned wiring for tooling, or inject a property/setter on a class whose constructor you cannot change.

## Quick start

```php
use Gacela\Framework\Attribute\Inject;

final class CatalogService
{
    public function __construct(
        #[Inject] private readonly LoggerInterface $logger,
        #[Inject(RedisCache::class)] private readonly CacheInterface $cache,
    ) {}
}
```

- A bare `#[Inject]` resolves the parameter by its type (same as autowiring, but explicit).
- `#[Inject(RedisCache::class)]` forces a specific implementation regardless of the global binding.

`Gacela\Framework\Attribute\Inject` is the preferred 2.0 import. It extends the container attribute, so both imports can coexist while an application migrates.

## Property and setter injection

Gacela 2.0 also supports properties and one-argument setter methods. This is useful for vendor or framework classes whose constructor is fixed:

```php
final class CatalogController extends VendorController
{
    #[Inject]
    private LoggerInterface $logger;

    #[Inject(RedisCache::class)]
    public function setCache(CacheInterface $cache): void
    {
        $this->cache = $cache;
    }
}
```

Private, protected, and inherited properties work. Constructor injection remains preferable for application-owned classes because dependencies stay visible in the signature.

Readonly, untyped, scalar-typed, and static properties cannot be injected. A promoted property is handled through its constructor parameter and is not injected twice. Property/setter cycles still throw `CircularDependencyException`.

## Resolution order

For a parameter `$p` on `Consumer`, the container resolves in this order:

1. A runtime override passed to `make()` under `$p`'s name.
2. A named contextual binding: `when(Consumer::class)->needs('$p')->give(...)`.
3. The explicit target in `#[Inject(Target::class)]`.
4. The parameter's default value.
5. A type-based contextual binding for `Consumer`.
6. A global `addBinding()` for the type.
7. Recursive autowiring when the type is an instantiable class.
8. `DependencyNotFoundException` when nothing can resolve it.

::: warning Defaults win over type bindings
`__construct(?Engine $engine = null)` resolves to `null` even when `Engine` has a global binding, because defaults are checked first. Remove the default or use `#[Inject]` when the container should fill the parameter. Nullability alone does not produce `null`: `?Engine $engine` without a default still throws if unresolved.
:::

## Inspecting with `debug:dependencies`

The `debug:dependencies` command tags `#[Inject]` parameters so you can verify wiring at a glance:

```bash
vendor/bin/gacela debug:dependencies App\\Catalog\\CatalogService --tree
```

```
✓ $logger  LoggerInterface   (inject)
✓ $cache   CacheInterface    (inject -> App\Cache\RedisCache)
```

The one-level view describes constructor parameters. `--tree` follows transitive dependencies using the container's applied bindings and contextual bindings. Each node is marked `binding`, `instance`, `autowired`, or `unresolvable`; cycles are marked and cut. The command reports broken graphs instead of throwing so it remains useful as a diagnostic.

## When to use `#[Inject]` vs bindings

| Scenario | Approach |
|----------|----------|
| Global default for an interface | `addBinding()` in `gacela.php` |
| One class needs a different implementation | `#[Inject(Concrete::class)]` on the parameter |
| Multiple classes need the same override | `when()->needs()->give()` contextual binding |
| Constructor is controlled by a vendor/framework | `#[Inject]` on a property or setter |

`#[Inject]` is opt-in. Classes without it continue to resolve through ordinary autowiring and bindings.

## Symfony integration

In Symfony apps, the `gacela-project/symfony-bridge` package routes `#[Inject]` parameters through Gacela's container via a compiler pass. See [Symfony bridge](https://gacela-project.com/docs/other-frameworks.md#symfony-bridge) for setup.

---

Source: https://gacela-project.com/docs/service-map.md

# Service Map

Gacela resolves sibling pillars (Facade → Factory → Config → Provider) by convention. The **`#[ServiceMap]` attribute** declares that another class can resolve a Gacela service on demand—for example, a controller accessing a Facade without constructor injection.

`#[ServiceMap]` is the required forward-compatible runtime declaration. It is understood by the bundled PHPStan extension and the 2.0 Psalm plugin.

## Basic usage

```php
use Gacela\Framework\ServiceResolver\ServiceMap;
use Gacela\Framework\ServiceResolverAwareTrait;

#[ServiceMap(method: 'getFacade', className: UserFacade::class)]
final class UserController
{
    use ServiceResolverAwareTrait;

    public function show(int $id): array
    {
        return $this->getFacade()->findUser($id);
    }
}
```

The attribute is repeatable. Declare every resolvable service the class needs:

```php
#[ServiceMap(method: 'getFacade',     className: UserFacade::class)]
#[ServiceMap(method: 'getCatalog',    className: CatalogFacade::class)]
#[ServiceMap(method: 'getLogger',     className: LoggerInterface::class)]
final class DashboardController
{
    use ServiceResolverAwareTrait;

    public function index(): array
    {
        return [
            'user'    => $this->getFacade()->current(),
            'top'     => $this->getCatalog()->popular(),
        ];
    }
}
```

Each `__call()` dispatch is cached. The resolver pool is static across the process, so repeated calls are essentially free.

## DocBlock migration aid

An IDE-friendly `@method` can live beside the attribute:

```php
/** @method UserFacade getFacade() */
#[ServiceMap(method: 'getFacade', className: UserFacade::class)]
final class UserController
{
    use ServiceResolverAwareTrait;
}
```

If the attribute is absent, 2.0 can still resolve from the docblock or scan the caller's imports, but that cold resolution raises `E_USER_DEPRECATED`; both fallbacks are removed in 3.0. `DocBlockResolverAwareTrait` itself was removed in 2.0—use `ServiceResolverAwareTrait`.

## Relationship with the container

`#[ServiceMap]` is a thin sugar on top of the Locator. The service is ultimately resolved through the main container, respecting every binding, alias, contextual binding and `AnonymousGlobal` declaration registered in `gacela.php`.

If you are authoring a class managed by another container (Symfony, Laravel), prefer constructor injection with [`#[Inject]`](https://gacela-project.com/docs/inject.md). `#[ServiceMap]` is targeted at classes instantiated outside of Gacela where constructor injection is not practical.

## Limitations

- The `__call()` dispatch means IDEs need the attribute (or `@method`) to autocomplete. Both are read by PhpStorm's Symfony plugin out of the box.
- Protected services (`addProtected()`) cannot be resolved through `#[ServiceMap]`. They are stored as raw closures and the container will not instantiate them.
- PHPStan reports an accessor that declares neither `#[ServiceMap]` nor `@method`. Psalm needs the [2.0 plugin](https://gacela-project.com/docs/static-analysis.md#psalm) to infer the attribute's return type instead of treating the call as `mixed`.

---

Source: https://gacela-project.com/docs/extensions.md

# Extensions and plugins

Use the narrowest extension point that matches the job:

| Need | Extension point |
|---|---|
| Run setup after bootstrap | Plugin |
| Decorate or alter one service | `extendService()` |
| Add a reusable configuration bundle | `extendGacelaConfig()` |
| Resolve keyed domain handlers | Handler registry |

## Plugins

```php
addPlugin(callable|class-string $plugin);
addPlugins(array $list);
```

Run custom logic right after bootstrapping gacela by adding plugins using the `addPlugin` method.

```php
<?php # index.php

Gacela::bootstrap(__DIR__, function (GacelaConfig $config) {
  // using a callable
  $config->addPlugin(function (RouterInterface $router) {
    $router->configure(function (Routes $routes) {
      $routes->get('/uri', YourController::class, 'uriAction');
    });
  });

  // or using a class name
  $config->addPlugin(ApiRoutesPlugin::class);
});
```

The class must be invokable, and it has autoload capabilities: all dependencies will be resolved automatically as soon as you have defined them using [bindings](https://gacela-project.com/docs/bindings.md). The same applies to the callable arguments above.

For example, having this other class `ApiRoutesPlugin` somewhere else:
```php
<?php # ApiRoutesPlugin.php

final class ApiRoutesPlugin
{
  public function __invoke(RouterInterface $router): void
  {
    $router->configure(function (Routes $routes): void {
      $routes->get('{name}', HelloController::class);
    });
  }
}
```

## Extend Service

```php
extendService(string $id, Closure $service);
```

Extend any service functionality. The `extendService()` receives the service name that will be defined in any `Provider`, and a `callable` which receives the service itself as 1st arg, and the `Container` as 2nd arg.

### An example

Consider we have a module with these `Provider`, `Factory` and `Facade`.

The `Provider` has a service defined `'ARRAY_OBJ'` which is an `ArrayObject` with values `[1, 2]` (see `Module/Provider.php`)

We "extend" that service `'ARRAY_OBJ'` and appending `3` (see `gacela.php`)

Its state when using the Facade and resolving that will be `[1, 2, 3]` (see `index.php`)

```php
<?php 

/************************************************************************/
# Module/Provider.php
final class Provider extends AbstractProvider
{
  public const ARRAY_OBJ = 'ARRAY_OBJ';

  public function provideModuleDependencies(Container $container): void
  {
    $container->set(self::ARRAY_OBJ, new ArrayObject([1, 2]));
  }
}

/************************************************************************/
# Module/Factory.php
final class Factory extends AbstractFactory
{
  public function getArrayAsObject(): ArrayObject
  {
    return $this->getProvidedDependency(Provider::ARRAY_OBJ);
  }
}

/************************************************************************/
# Module/Facade.php
final class Facade extends AbstractFacade
{
  public function getArrayAsObject(): ArrayObject
  {
    return $this->getFactory()->getArrayAsObject();
  }
}

/************************************************************************/
# gacela.php
return function (GacelaConfig $config) {
  $config->extendService(
    Provider::ARRAY_OBJ,
    function (ArrayObject $arrayObject, Container $container) {
      $arrayObject->append(3);
    }
  );
};

/************************************************************************/
# index.php
$facade = new Module\Facade();
$facade->getArrayAsObject(); // === new ArrayObject([1, 2, 3])
```

## Extend Gacela Config

```php
extendGacelaConfig(string $configClass);
extendGacelaConfigs(array $list);
```

Extend `GacelaConfig` from different places using the `extendGacelaConfig` method.

The class must be invokable, and it will receive the GacelaConfig object. For example:

```php
<?php # index.php

Gacela::bootstrap(__DIR__, function (GacelaConfig $config) {
  $config->extendGacelaConfig(RouterConfig::class);
});
```

The invokable config class, defined elsewhere:

```php
<?php

final class RouterConfig
{
  public function __invoke(GacelaConfig $config): void
  {
    $router = new Router();

    $config->addBinding(Router::class, $router);
    $config->addBinding(RouterInterface::class, $router);
  }
}
```

## Handler Registry

```php
addHandlerRegistry(string $registryKey, array<string|int,class-string> $handlers);
```

Declare a build-time dispatch table. The registry is resolvable from the container under `$registryKey` and returns a `HandlerRegistry` that lazy-instantiates each handler through the container on first access. Registries are frozen after boot. There is no runtime `register()` method.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->addHandlerRegistry(PaymentGatewayInterface::class, [
    'stripe' => StripeGateway::class,
    'paypal' => PaypalGateway::class,
  ]);
};
```

## Health Check Registration

```php
addHealthCheck(class-string|ModuleHealthCheckInterface $check);
```

Register a per-module health check. All registered checks are aggregated by the `doctor` command and the `HealthChecker`. See the full [Module health checks](https://gacela-project.com/docs/health-checks.md) page.

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->addHealthCheck(DatabaseHealthCheck::class);
  $config->addHealthCheck(new CacheHealthCheck($redis));
};
```

---

Source: https://gacela-project.com/docs/customization.md

# Module customization

These options change Gacela's naming and discovery conventions. Keep the defaults for new applications; customize them when integrating an established structure or overriding a vendor module.

## Custom pillar suffixes

The defaults are `Facade`, `Factory`, `Provider`, and `Config`. Register alternatives when the project already uses different names:

```php [gacela.php]
use Gacela\Framework\Bootstrap\GacelaConfig;

return static function (GacelaConfig $config): void {
    $config
        ->addSuffixTypeFacade('EntryPoint')
        ->addSuffixTypeFactory('Creator')
        ->addSuffixTypeProvider('Binder')
        ->addSuffixTypeConfig('Settings');
};
```

Gacela will then recognize this module:

```text
ExampleModule/
├── EntryPoint.php  # Facade role
├── Creator.php     # Factory role
├── Binder.php      # Provider role
└── Settings.php    # Config role
```

Custom suffixes are additive. Default suffixes continue to resolve.

## Project namespace priority

`setProjectNamespaces()` gives application classes priority over matching vendor module classes.

```php [gacela.php]
return static function (GacelaConfig $config): void {
    $config->setProjectNamespaces(['App']);
};
```

Given both files below, a vendor `ModuleA\Facade` resolves the application Factory because `App` has priority:

```text
src/App/ModuleA/Factory.php
vendor/acme/package/src/ModuleA/Factory.php
```

Use this for targeted vendor customization while preserving the vendor Facade API. Mirror only the module path and pillar being replaced.

## Module scan paths

Restrict discovery when modules live under known directories:

```php [gacela.php]
return static function (GacelaConfig $config): void {
    $config->setAppModulePaths(['src']);
};
```

This speeds up `list:modules`, `debug:modules`, `cache:warm`, and `doctor` by excluding unrelated directories. Paths may be absolute or relative to the application root.

## Lifecycle listeners

Use `registerGenericListener()` for all events or `registerSpecificListener()` for one event class. Listeners are best suited to tracing, profiling, and metrics; they should not contain business behavior.

```php [gacela.php]
return static function (GacelaConfig $config): void {
    $config->registerSpecificListener(
        ResolvedClassCreatedEvent::class,
        static function (ResolvedClassCreatedEvent $event): void {
            // Record resolution telemetry.
        },
    );
};
```

See [Events](https://gacela-project.com/docs/events.md) for the event catalog and typed payloads.

## Reset InMemoryCache

`resetInMemoryCache()` clears state before bootstrap. Prefer [`GacelaTestCase`](https://gacela-project.com/docs/testing.md#gacelatestcase) or `ContainerFixture` in tests because they also clean up after each test.

```php [gacela.php]
return static function (GacelaConfig $config): void {
    $config->resetInMemoryCache();
};
```

For long-running processes that must clear all runtime and file-backed resolution caches, use [`Gacela::resetCache()`](https://gacela-project.com/docs/bootstrap.md#gacela-resetcache).

---

Source: https://gacela-project.com/docs/caching.md

# Caching

Gacela caches at three different levels. Each solves a different problem. They compose, they don't replace one another.

| Layer | What it caches | Where | Typical use |
|---|---|---|---|
| [Framework resolution](#layer-1-framework-resolution-cache) | Resolved facades, factories, configs, merged config | Memory or disk | Always on, pick the mode per environment |
| [Cacheable methods](#layer-2-cacheable-facade-methods) | Return values of facade methods | Memory (pluggable) | Expensive, deterministic reads |
| [Value primitives](#layer-3-value-primitives) | Arbitrary key → value data, optionally with a dependency graph | Disk | Your code needs its own cache (compilers, pipelines, parsed artifacts) |

## Layer 1: Framework resolution cache

Gacela resolves classes by convention: `Facade` → `Factory` → `Provider` → `Config`. Those lookups walk namespaces and files, and the merged configuration is reassembled from every `config/*.php` file. All of it is memoised once per process, and can additionally be persisted to disk between runs.

- **In-memory** (default): `InMemoryCache` holds resolved class names for the life of the process.
- **On-disk**: `ClassNamePhpCache`, `CustomServicesPhpCache`, and `MergedConfigCache` persist the same data in project-scoped PHP files. Filenames include an application-root hash, preventing applications that share a cache directory from serving each other's data; merged config files are also scoped by `APP_ENV`.

Module containers are scopes of one application container in 2.0. They also share an in-process constructor-plan cache, so classes used by several modules are reflected once while bindings, tags, instances, and Provider registrations remain scoped. Persisting constructor plans was measured as a net loss and is not enabled automatically.

Configure at bootstrap:

```php
use Gacela\Framework\Bootstrap\GacelaConfig;
use Gacela\Framework\Gacela;

Gacela::bootstrap(__DIR__, static function (GacelaConfig $config): void {
    $config->enableFileCache();                  // use the default cache dir
    // $config->enableFileCache('var/cache');    // relative to the app root
    // $config->setFileCache(false);             // explicitly off
    // $config->resetInMemoryCache();            // wipe static caches (tests)
});
```

A configured path is resolved relative to the application root—even a leading slash does not escape it. Use the `GACELA_CACHE_DIR` environment variable for a genuinely absolute directory outside the project; it takes precedence and is used verbatim.

With the file cache enabled, the merged configuration **auto-warms on the first miss**: the first bootstrap persists the app- and environment-scoped merged-config file, so later bootstraps skip globbing and parsing config files—no manual `cache:warm` is required for that layer.

In a **read-only environment** (e.g. a read-only project root inside a build sandbox) the file caches degrade gracefully to in-memory instead of failing the bootstrap: writes become no-ops, no raw PHP warnings are emitted, and any pre-warmed cache files already on disk stay readable. Warm-at-build / run-read-only deployments keep their cache hits.

Typical wiring:

- **Development**: file cache **off**. Edits take effect immediately.
- **Production**: file cache **on**, pre-populated with `vendor/bin/gacela cache:warm`, directory baked into the image. Re-deploy (or `cache:clear`) to refresh.
- **Tests**: call `resetInMemoryCache()` between suites so resolution state doesn't bleed.

See also: [Opcache preload](https://gacela-project.com/docs/opcache-preload.md) for getting PHP itself to cache Gacela's own source files.

## Layer 2: Cacheable facade methods

Cache the *result* of a facade method with the `#[Cacheable]` attribute. `CacheableTrait` is built into `AbstractFacade`, no extra `use` needed. Full reference: [Cacheable methods](https://gacela-project.com/docs/cacheable-methods.md).

```php
use Gacela\Framework\AbstractFacade;
use Gacela\Framework\Attribute\Cacheable;

final class CatalogFacade extends AbstractFacade
{
    #[Cacheable(ttl: 3600)]
    public function getPopularProducts(): array
    {
        return $this->cached(fn (): array =>
            $this->getFactory()->createRepository()->fetchPopular(),
        );
    }
}
```

Storage is `InMemoryCacheStorage` by default, which means entries die with the request on PHP-FPM. For cross-request caching swap in a shared backend (APCu, Redis, PSR-16) via `CacheableConfig::setStorage()`.

```php
CatalogFacade::clearMethodCacheFor('getPopularProducts'); // one method, any args
CatalogFacade::clearMethodCache();                        // the whole shared store
```

`clearMethodCache()` is not facade-scoped: it calls `clear()` on the shared storage backend. `Gacela::resetCache()` only clears Gacela's default in-memory method cache and deliberately leaves a custom APCu/Redis backend registered through `CacheableConfig::setStorage()` alone.

## Layer 3: Value primitives

When *your code* needs a cache (compiled artifacts, parsed data, or a build pipeline), use `Gacela\Framework\Cache\FileCache`:

```php
use Gacela\Framework\Cache\FileCache;

$cache = new FileCache('/var/cache/myapp');

$cache->put('user:42', $user, ttl: 600);
$cache->get('user:42');     // $user, or null after TTL expiry
$cache->forget('user:42');
$cache->clear();
```

- One `.php` file per key (SHA1-hashed), written atomically via staged `.tmp` + `rename`.
- `writeContentsAtomically(string $file, string $content): bool` — atomically writes already-rendered content to a path, with the same staged-`.tmp` + `rename` guarantees as `put()`. The higher-level `writeAtomically()` wraps it.
- TTL per entry; `ttl: 0` means forever.
- `beginBatch()` / `commitBatch()` defer writes behind a single index-locked flush. Useful for warming many entries at once.
- `stats()` returns entry count, total bytes, and oldest/newest timestamps.
- Safe against torn reads: concurrent readers see either the previous file or the new one, never a half-written one.

### ScopedCache: dependency-aware decorator

When invalidating one entry should cascade to every downstream entry that derived from it, wrap `FileCache` in `ScopedCache`:

```php
use Gacela\Framework\Cache\FileCache;
use Gacela\Framework\Cache\ScopedCache;

$cache = new ScopedCache(new FileCache('/var/cache/myapp'));

$cache->put('ns:core', $envCore);
$cache->put('file:a.php', $compiledA);
$cache->put('fragment:a#1', $fragment);

$cache->dependsOn('file:a.php', 'ns:core');
$cache->dependsOn('fragment:a#1', 'file:a.php');

$cache->invalidate('ns:core');          // cascades: file:a.php and fragment:a#1 also go
$cache->invalidateLeaf('file:a.php');   // only this key; dependents stay valid
```

- `get` / `put` / `has` delegate straight to the underlying `FileCache`. Zero overhead on the hot path.
- The dependency graph is persisted alongside the values (`.gacela-scoped-cache-graph.php`) and survives process restarts.
- Cycles are rejected eagerly at `dependsOn()`: self, two-node, and transitive.
- Single-writer concurrency: multiple processes racing on `dependsOn()` may lose edges added between load and persist.

## Picking a layer

- Make Gacela's own resolution faster → Layer 1, `enableFileCache()` + `cache:warm`.
- Memoise a specific facade method → Layer 2, `#[Cacheable]`.
- Cache arbitrary application data → Layer 3, `FileCache`.
- Same, but invalidation must cascade → Layer 3, `ScopedCache`.

---

Source: https://gacela-project.com/docs/cacheable-methods.md

# Cacheable facade methods

Cache the result of a facade method for a given TTL using the `#[Cacheable]` attribute.

`AbstractFacade` includes `CacheableTrait`, so Facades can use `#[Cacheable]` and `$this->cached()` directly.

## Quick start

```php
use Gacela\Framework\Attribute\Cacheable;
use Gacela\Framework\AbstractFacade;

final class CatalogFacade extends AbstractFacade
{
    #[Cacheable(ttl: 3600)]
    public function getPopularProducts(): array
    {
        return $this->cached(fn (): array =>
            $this->getFactory()->createRepository()->fetchPopular(),
        );
    }
}
```

Subsequent calls within the TTL return the cached value without invoking the callback.

## How it works

`#[Cacheable]` is metadata only. The real caching happens inside `$this->cached(...)`, which:

1. Reads the attribute via reflection (memoised per `Class::method`).
2. Builds a cache key from the class, method, and arguments.
3. Returns the cached value on hit, or runs the callback and stores the result on miss.

By default, the method name and arguments are inferred from the caller's stack frame. Pass them explicitly for performance-sensitive paths or calls routed through a helper; see [Opting out of backtrace](#opting-out-of-backtrace).

::: tip Generic return type
`cached()` is generic (`@template T`), so static analysis infers the return type from the callback without a call-site annotation or cast.
:::

## Arguments shape the cache key

Calls with different arguments are cached separately.

```php
#[Cacheable(ttl: 600)]
public function findUser(int $id): User
{
    return $this->cached(fn (): User =>
        $this->getFactory()->createRepository()->find($id),
    );
}

$facade->findUser(1); // runs callback, caches under key ending in "::1"
$facade->findUser(1); // cache hit
$facade->findUser(2); // runs callback, separate entry
```

Single `int` or `string` arguments become part of the key directly (`Facade::method::42`). Other types (arrays, objects, multiple args) fall back to `md5(serialize(...))`.

## Custom key templates

Use `key` with `{N}` placeholders to interpolate the Nth argument into the cache key. Useful for shared keys across modules or for readable keys in an external cache.

```php
#[Cacheable(ttl: 3600, key: 'user:{0}')]
public function getUser(int $id): array
{
    return $this->cached(fn (): array =>
        $this->getFactory()->createRepository()->find($id),
    );
}
```

A bare string with no placeholders is args-agnostic. Every call shares the same entry regardless of arguments.

## Clearing the cache

```php
// Clear all entries for a specific method (any args)
CatalogFacade::clearMethodCacheFor('getPopularProducts');

// Clear the whole shared storage backend, across every facade
CatalogFacade::clearMethodCache();
```

`clearMethodCacheFor()` matches on the exact `Class::method::` prefix. Passing `'get'` does **not** clear every method whose name starts with `get`.

`clearMethodCache()` calls `clear()` on the shared backend and is not scoped to the facade class. Prefer the method-specific operation unless clearing all application entries is intentional.

Custom key templates do not contain the normal `Class::method::` prefix, so `clearMethodCacheFor()` cannot find them. Invalidate those keys through the configured storage backend.

`Gacela::resetCache()` clears only the default in-process method storage. It does not clear an external backend registered through `CacheableConfig::setStorage()`; call `clearMethodCache()` when that is the intended scope.

## Pluggable storage backend

By default, cache lives in process memory via `InMemoryCacheStorage`. On PHP-FPM that means entries die with the request. Fine for batch jobs and long-running workers, but effectively a no-op for typical web traffic.

Swap in any backend that implements `CacheStorageInterface` (e.g. APCu, Redis, a PSR-16 adapter):

```php
use Gacela\Framework\Attribute\CacheableConfig;

CacheableConfig::setStorage(new RedisCacheStorage($redis));
```

```php
interface CacheStorageInterface
{
    public function has(string $key): bool;
    public function get(string $key, mixed $default = null): mixed;
    public function set(string $key, mixed $value, int $ttl): void;
    public function delete(string $key): void;
    public function clear(): void;
    public function deleteByPrefix(string $prefix): void;
}
```

Call `CacheableConfig::setStorage()` once at bootstrap. All facades using `CacheableTrait` share the same backend.

## TTL overrides per method

Override the TTL declared on the attribute without changing code. Useful for tuning hot paths per environment.

```php
CacheableConfig::setTtlOverrides([
    CatalogFacade::class . '::getPopularProducts' => 60,   // tighten in staging
    UserFacade::class . '::getUser' => 86400,              // loosen in prod
]);
```

The override applies on the next `set()`; existing entries keep their original expiry until evicted.

## Opting out of backtrace

`cached()` calls `debug_backtrace()` (limit 2) to infer the method name and arguments. The cost is negligible next to typical "expensive" methods (DB, HTTP). Pass `$method` and `$args` explicitly when:

- The cached operation itself is very fast and the overhead matters.
- The method takes very large arguments (frame-construction cost scales with arg count).
- `cached()` is called from a private helper rather than the attributed method itself.

```php
#[Cacheable(ttl: 3600)]
public function getUser(int $id): array
{
    return $this->cached(
        fn (): array => $this->getFactory()->createRepository()->find($id),
        __METHOD__,
        [$id],
    );
}
```

## Caching `null`

A method that returns `null` is cached correctly. Repeated calls do **not** re-invoke the callback. `CacheableTrait` distinguishes "cached null" from "cache miss" via a sentinel, so `Optional`-style return types work as expected.

## Limitations

- **Per-process by default.** Entries in `InMemoryCacheStorage` do not survive the request on PHP-FPM. Use a shared backend (APCu, Redis) if you need cross-request caching.
- **Serialization.** The default key and miss detection rely on `serialize()` for non-scalar arguments. Arguments containing closures or resources cannot be serialized and will throw.
- **Memoised attribute metadata.** The `#[Cacheable]` attribute is reflected once per `Class::method` and cached for the lifetime of the process. Changing the attribute at runtime has no effect; change the code and redeploy.

---

Source: https://gacela-project.com/docs/opcache-preload.md

# Opcache preload

Gacela ships a preload script that loads its core files into shared memory at PHP startup, removing their per-request compilation cost and lowering per-request memory. Measure the benefit on your own workload.

**Requires** PHP 8.3+ with opcache enabled.

## Setup

Add to `php.ini` (or your FPM pool config):

```ini
opcache.enable=1
opcache.preload=/path/to/project/vendor/gacela-project/gacela/resources/gacela-preload.php
opcache.preload_user=www-data
```

Restart PHP-FPM:

```bash
sudo systemctl restart php8.3-fpm
```

Verify in the logs: `Gacela Opcache Preload: 32 files preloaded successfully, 0 failed`.

## Preload your own files

Create `config/app-preload.php`:

```php
<?php
$root = dirname(__DIR__);

opcache_compile_file($root . '/src/User/UserFacade.php');
opcache_compile_file($root . '/src/Product/ProductFacade.php');
```

Wire it via env var in your FPM pool:

```ini
env[GACELA_PRELOAD_USER_FILES] = /path/to/project/config/app-preload.php
```

## Deployment

Preloaded files are snapshotted at startup. Restart PHP-FPM after every deploy:

```bash
composer install --no-dev --optimize-autoloader
vendor/bin/gacela cache:warm
sudo systemctl restart php8.3-fpm
```

## When to use it

- **Use it** for high-traffic production apps on PHP 8.3+.
- **Skip it** in local development (you'd need to restart after every change) or for very low-traffic sites.

## Troubleshooting

| Symptom                 | Check                                                                  |
|-------------------------|------------------------------------------------------------------------|
| Files not preloading    | `php -v` ≥ 8.3, `php -i \| grep opcache.enable`, preload file readable |
| Permission denied       | `opcache.preload_user` must match the PHP-FPM user (`ps aux \| grep php-fpm`) |

## Docker

```dockerfile
FROM php:8.3-fpm
RUN docker-php-ext-install opcache
COPY docker/opcache.ini /usr/local/etc/php/conf.d/
```

```ini
# docker/opcache.ini
opcache.enable=1
opcache.preload=/var/www/html/vendor/gacela-project/gacela/resources/gacela-preload.php
opcache.preload_user=www-data
```

## See also

- [PHP Opcache Documentation](https://www.php.net/manual/en/book.opcache.php)
- [Caching](https://gacela-project.com/docs/caching.md): other layers (framework resolution, cacheable methods, file cache primitives)

---

Source: https://gacela-project.com/docs/gacela-script.md

# CLI reference

Gacela ships a small CLI that assists you while building, inspecting and tuning modules in your application.

::: info
The CLI needs `symfony/console` 7 or 8. Gacela suggests rather than requires it, so add the package to applications that use the binary.
:::

All commands below are invoked through `vendor/bin/gacela`. Run it without arguments to list the installed commands, or `vendor/bin/gacela help <command>` for one command's complete options.

## Project setup

### `init`

Create the `gacela.php` bootstrap file required by every other command:

```bash
vendor/bin/gacela init [--force|-f]
```

`--force` overwrites an existing file.

## Module discovery

### `list:modules`

Render every module discovered under your project namespaces.

```bash
vendor/bin/gacela list:modules [--detailed|-d] [<filter>]
```

- `filter`: substring to narrow the output
- `-d`, `--detailed`: render each module's contents in detail

Scope which directories this (and `debug:modules`, `cache:warm`, `doctor`) scans with [`setAppModulePaths()`](https://gacela-project.com/docs/bootstrap.md#application-module-paths).

### `debug:modules`

Walk every discovered module and inspect the constructor of each pillar (Facade, Factory, Config, Provider). Complements `list:modules` (structural view) and `debug:dependencies` (single-class deep-dive).

```bash
vendor/bin/gacela debug:modules [--detail|-d] [<filter>]
```

- Default output groups by module with per-pillar resolvable/unresolvable counts.
- `--detail` includes every parameter, not just unresolvable ones.
- `filter` accepts a namespace substring (e.g. `App\\Shop`) or a directory (e.g. `src/`).

### `debug:dependencies`

Inspect a single class's constructor and report each parameter's resolvability through the container.

```bash
vendor/bin/gacela debug:dependencies <class|file> [--tree]
```

- Accepts a fully qualified class name or a path to a PHP file declaring the class.
- Each parameter is tagged (`bound → target`, `autowirable`, `has default`, or `unresolvable` with a reason).
- Parameters annotated with [`#[Inject]`](https://gacela-project.com/docs/inject.md) show up tagged `inject`, with the override concrete rendered inline when present.
- `--tree` appends the transitive dependency graph after applying bindings and contextual bindings. Nodes are marked `binding`, `instance`, `autowired`, or `unresolvable`; cycles are shown and cut.

### `debug:module`

Inspect a single module: its resolved Facade, Factory, Config and Provider, the container bindings it registers, and its dependency tree. Complements `debug:modules` (all modules, structural) and `debug:dependencies` (single class).

```bash
vendor/bin/gacela debug:module <module> [-j|--json] [-t|--tree]
```

- `module`: module name, or a part of it (required)
- `-j`, `--json`: output machine-readable JSON
- `-t`, `--tree`: only print the dependency tree

### `debug:graph`

Render the whole-app module dependency graph — which module imports which (edges via cross-module Facade usage).

```bash
vendor/bin/gacela debug:graph [<filter>] [-f|--format=text|mermaid|graphviz|json] [--check]
```

- `filter`: only include modules matching this substring
- `-f`, `--format`: `text` (default), `mermaid`, `graphviz`, or `json`
- `--check`: exit non-zero when an unreviewed dependency cycle exists
- `--allowed-cycles <file>`: JSON allowlist of reviewed cycles and their reasons
- `--compare-to <graph.json>`: diff the current graph against saved JSON output

The `mermaid` / `graphviz` formats are handy for architecture diagrams. Use `--check` in CI.

### `debug:container`

Inspect the container's **user bindings and plugins only** (framework-internal services are excluded).

```bash
vendor/bin/gacela debug:container [<class>] [-s|--stats] [-t|--tree]
```

- No arguments (or `-s`, `--stats`): print container statistics — registered services, frozen services, factory services, bindings, cached dependencies, and **process** memory usage.
- `<class>` (or `-t`, `--tree` with a class): render the dependency tree for that fully qualified class name. Passing a class implies `--tree`; `--tree` without a class errors.
- `-s`, `--stats` always takes precedence: `debug:container SomeClass --stats` prints statistics, not the dependency tree, even though a class was given.

## Caching & production

### `cache:warm`

Pre-resolve all module classes, write the persistent caches and (optionally) the merged configuration cache. Run this once per deploy in production.

```bash
vendor/bin/gacela cache:warm [-c|--clear] [-a|--attributes]
```

- `-c`, `--clear`: clear existing cache before warming (same as running `cache:clear` first)
- `-a`, `--attributes`: pre-scan and cache `#[ServiceMap]` attributes

Under the hood `cache:warm` batches file writes via `AbstractPhpFileCache::beginBatch()` / `commitBatch()` and flushes with atomic `rename()`, so a single write replaces the previous _N modules × 4 resolvers_ full-file rewrites.

### `cache:clear`

Remove every Gacela cache file.

```bash
vendor/bin/gacela cache:clear
```

Clears the project-scoped class-name, custom-service, and merged-config cache files, cacheable-method entries, and the container's in-process reflection memos.

## Configuration health

### `doctor`

Aggregate environmental and wiring health checks with per-check remediation hints. Bundled checks include cache staleness, suffix mismatches, and filename/class mismatches, plus any `ModuleHealthCheckInterface` registered through `GacelaConfig::addHealthCheck()`.

```bash
vendor/bin/gacela doctor [<filter>] [--strict]
```

- `filter`: restrict module-scoped checks to a namespace substring.
- By default warnings still exit `0`; `--strict` makes warnings fail too and is the recommended CI mode.

### `validate:config`

Validate the current Gacela configuration for errors and best practices.

```bash
vendor/bin/gacela validate:config
```

- Reports missing `gacela.php` (warning).
- Walks every registered binding and emits type-mismatch warnings with the expected interface/class, the actual type chain, and a fix hint.
- Interface-keyed bindings are checked as well (previously skipped).

### `debug:config`

Print the effective merged configuration as a table, after every `config/*.php` file and environment override is resolved.

```bash
vendor/bin/gacela debug:config [<filter>]
```

- `filter`: only show keys containing this substring.
- Backed by `Config::getAllValues()`, so it reflects exactly what your modules see at runtime.

## Profiling

### `profile:report`

Generate a performance report from the in-memory `Profiler`. Enable the profiler (`Profiler::getInstance()->enable()`) early in your bootstrap, run your code, then dump the report.

```bash
vendor/bin/gacela profile:report [--format=table|json|summary] [--sort=duration|memory|operation]
```

- `--format`: `table` (default), `json`, or `summary`.
- `--sort`: `duration` (default), `memory`, or `operation`.

## Code generation

### `make:file`

Generate a `Facade`, `Factory`, `Config`, `Provider`, or any combination of them.

```bash
vendor/bin/gacela make:file [-s|--short-name] <path> <filenames>...
```

- `path`: file path, e.g. `App/TestModule/TestSubModule`
- `filenames`: any combination of `facade`, `factory`, `config`, `provider`
- `-s`, `--short-name`: drop the module prefix from the generated class name

```bash
vendor/bin/gacela make:file App/TestModule facade factory provider
```

### `make:module`

Generate a full module: `Facade`, `Factory`, `Config`, and `Provider`.

```bash
vendor/bin/gacela make:module [-s|--short-name] [-t|--template=basic|service|minimal] [--minimal] [--with-tests] <path>
```

- `-s`, `--short-name`: drop the module prefix from the generated class name
- `-t`, `--template`: `basic` (four pillars), `service` (four pillars plus a wired Domain service), or `minimal` (Facade and Factory only).
- `--minimal`: shorthand for `--template=minimal`.
- `--with-tests`: also scaffold a `GacelaTestCase`-based facade test (only valid with `--template=service`).

```bash
vendor/bin/gacela make:module -s App/TestModule
```

```bash
vendor/bin/gacela make:module --template=service --with-tests App/Checkout
```

---

Source: https://gacela-project.com/docs/health-checks.md

# Module health checks

Report each module's operational status and aggregate them into a single system health view. Great for `/health` HTTP endpoints, container orchestrators and the `doctor` CLI.

## Quick start

### 1. Implement `ModuleHealthCheckInterface`

```php
use Gacela\Framework\Health\HealthStatus;
use Gacela\Framework\Health\ModuleHealthCheckInterface;

final class DatabaseHealthCheck implements ModuleHealthCheckInterface
{
    public function __construct(private readonly PDO $pdo) {}

    public function checkHealth(): HealthStatus
    {
        $this->pdo->query('SELECT 1');

        return HealthStatus::healthy('Database operational');
    }

    public function getModuleName(): string
    {
        return 'Database';
    }
}
```

### 2. Register the check

Register from `gacela.php` to have the Doctor command pick it up automatically, alongside cache-staleness, suffix-mismatch, and filename-mismatch checks:

```php
<?php # gacela.php

return function (GacelaConfig $config) {
    $config->addHealthCheck(DatabaseHealthCheck::class);
    $config->addHealthCheck(new CacheHealthCheck($redis));
};
```

### 3. Run the checks

```php
use Gacela\Framework\Health\HealthChecker;

$checker = new HealthChecker([
    new DatabaseHealthCheck($pdo),
    new CacheHealthCheck($redis),
]);

$report = $checker->checkAll();
```

…or shell out to the CLI:

```bash
vendor/bin/gacela doctor
```

Pass an optional namespace filter to restrict module checks. In CI, use `vendor/bin/gacela doctor --strict` so warnings also produce a failing exit code.

## Status levels

| Level       | When to use                                  |
|-------------|----------------------------------------------|
| `healthy`   | Everything works as expected                 |
| `degraded`  | Works but slow or using fallbacks            |
| `unhealthy` | Critical failure                             |

```php
HealthStatus::healthy('API responding in 50ms');
HealthStatus::degraded('High latency', ['avg_ms' => 500]);
HealthStatus::unhealthy('Unreachable', ['retries' => 3]);
```

## HTTP endpoint

```php
public function healthCheck(): Response
{
    $report = $this->healthChecker->checkAll();

    $status = match ($report->getOverallLevel()) {
        HealthLevel::HEALTHY, HealthLevel::DEGRADED => 200,
        HealthLevel::UNHEALTHY => 503,
    };

    return new JsonResponse($report->toArray(), $status);
}
```

`$report->toArray()`:

```php
[
    'overall' => 'degraded',
    'modules' => [
        'Database'   => ['level' => 'healthy',  'message' => '...', 'metadata' => [...]],
        'PaymentAPI' => ['level' => 'degraded', 'message' => '...', 'metadata' => [...]],
    ],
]
```

## Report API

```php
$report->isHealthy();                              // bool
$report->hasUnhealthyModules();                    // bool
$report->getOverallLevel();                        // HealthLevel
$report->getResults();                             // array<string, HealthStatus>
$report->getResultsByLevel(HealthLevel::UNHEALTHY);
$report->toArray();
```

## Best practices

- **Be fast**: checks should complete in under a second. Prefer a quick ping (`SELECT 1`) over full queries.
- **Include metadata**: latency, error codes, retry counts help diagnose issues.
- **Let exceptions propagate**: `HealthChecker` converts any `Throwable` into an `unhealthy` result with exception, file, and line metadata.
- **Pick the right level**: reserve `unhealthy` for real outages; use `degraded` for slow-but-working.

## API reference

### `ModuleHealthCheckInterface`

```php
public function checkHealth(): HealthStatus;
public function getModuleName(): string;
```

### `HealthStatus`

```php
HealthStatus::healthy(string $message = 'Module is healthy', array $metadata = []): self
HealthStatus::degraded(string $message, array $metadata = []): self
HealthStatus::unhealthy(string $message, array $metadata = []): self

$status->level;       // HealthLevel
$status->message;     // string
$status->metadata;    // array
$status->isHealthy(): bool
$status->isDegraded(): bool
$status->isUnhealthy(): bool
$status->toArray(): array
```

### `HealthChecker`

```php
$checker->checkAll(): HealthCheckReport
$checker->count(): int
```

---

Source: https://gacela-project.com/docs/events.md

# Events

Gacela dispatches **read-only lifecycle events** as it boots, resolves services, reads config and manages caches. Listen to them for tracing, profiling, debugging or metrics — without touching your module code.

::: tip Zero-cost when nobody listens
Event dispatch is free when nothing listens. Every dispatch site first checks `hasListeners()` and skips building the event entirely when there are no listeners.
:::

## Registering listeners

Listeners are registered on `GacelaConfig`, in `gacela.php` or the `Gacela::bootstrap()` closure.

### A generic listener — every event

```php
registerGenericListener(callable $listener);
```

```php
<?php # gacela.php

use Gacela\Framework\Event\GacelaEventInterface;

return function (GacelaConfig $config) {
  $config->registerGenericListener(
    function (GacelaEventInterface $event): void {
      error_log($event->toString());
    }
  );
};
```

### A specific listener — one event type

```php
registerSpecificListener(string $event, callable $listener);
```

```php
<?php # gacela.php

use Gacela\Framework\Event\Bootstrap\GacelaBootstrapFinishedEvent;

return function (GacelaConfig $config) {
  $config->registerSpecificListener(
    GacelaBootstrapFinishedEvent::class,
    function (GacelaBootstrapFinishedEvent $event): void {
      error_log(sprintf('Bootstrap took %.2f ms', $event->durationMs()));
    }
  );
};
```

Every event implements `GacelaEventInterface`, which exposes `toString(): string` for logging. Concrete events add typed accessors — see the catalog below.

## Lifecycle event catalog

The high-level events dispatched over a bootstrap, in the order you meet them.

### `Gacela\Framework\Event\Bootstrap`

| Event | Dispatched when | Accessors |
|---|---|---|
| `GacelaBootstrapStartedEvent` | `Gacela::bootstrap()` begins | `appRootDir(): string` |
| `GacelaBootstrapFinishedEvent` | bootstrap has finished | `durationMs(): float` |

### `Gacela\Framework\Event\Config`

| Event | Dispatched when | Accessors |
|---|---|---|
| `ConfigInitializedEvent` | the merged configuration is assembled | `keyCount(): int` |
| `ConfigKeyReadEvent` | a config key is read | `key(): string` |
| `ConfigKeyNotFoundEvent` | a requested config key is missing | `key(): string` |

### `Gacela\Framework\Event\Container`

| Event | Dispatched when | Accessors |
|---|---|---|
| `BindingRegisteredEvent` | a binding, alias or contextual binding is registered | `id(): string` |
| `ServiceResolvedEvent` | a service id is instantiated (once per id) | `id(): string` |

### `Gacela\Framework\Event\Provider`

| Event | Dispatched when | Accessors |
|---|---|---|
| `ProviderRegisteredEvent` | a module's Provider is registered | `providerClass(): string`, `moduleName(): string` |

### `Gacela\Framework\Event\Cache`

| Event | Dispatched when | Accessors |
|---|---|---|
| `CacheClearedEvent` | a cache file is removed (`cache:clear`) | `cacheFile(): string` |
| `CacheWarmedEvent` | `cache:warm` finishes | `moduleCount(): int`, `failedCount(): int`, `skippedCount(): int` |

`failedCount()` counts pillar classes found but not resolved. `skippedCount()` counts pillars a module does not contain, which is a valid module shape. Alert on failures, not skips.

## Recipes

### Time the bootstrap

```php
<?php # gacela.php

use Gacela\Framework\Event\Bootstrap\GacelaBootstrapFinishedEvent;

return function (GacelaConfig $config) {
  $config->registerSpecificListener(
    GacelaBootstrapFinishedEvent::class,
    fn (GacelaBootstrapFinishedEvent $e) => Metrics::timing('gacela.bootstrap_ms', $e->durationMs()),
  );
};
```

### Log every resolved class

```php
<?php # gacela.php

use Gacela\Framework\Event\ClassResolver\AbstractGacelaClassResolverEvent;
use Gacela\Framework\Event\GacelaEventInterface;

return function (GacelaConfig $config) {
  $config->registerGenericListener(function (GacelaEventInterface $event): void {
    if ($event instanceof AbstractGacelaClassResolverEvent) {
      error_log($event->toString());
    }
  });
};
```

### Alert on missing config keys

```php
<?php # gacela.php

use Gacela\Framework\Event\Config\ConfigKeyNotFoundEvent;

return function (GacelaConfig $config) {
  $config->registerSpecificListener(
    ConfigKeyNotFoundEvent::class,
    fn (ConfigKeyNotFoundEvent $e) => error_log("Missing config key: {$e->key()}"),
  );
};
```

## Lower-level resolver & cache events

Beyond the lifecycle events above, Gacela dispatches fine-grained events during class resolution and cache bookkeeping. Reach for these when tracing *why* a class resolved the way it did. The class-resolution events share the `AbstractGacelaClassResolverEvent` base, so a single `instanceof` catches them all.

#### `Gacela\Framework\Event\ClassResolver`
- `AbstractGacelaClassResolverEvent` (base type)
- `ResolvedClassCreatedEvent`
- `ResolvedClassCachedEvent`
- `ResolvedCreatedDefaultClassEvent`
- `ResolvedClassTriedFromParentEvent`

#### `Gacela\Framework\Event\ClassResolver\ClassNameFinder`
- `ClassNameValidCandidateFoundEvent`
- `ClassNameInvalidCandidateFoundEvent`
- `ClassNameCachedFoundEvent`
- `ClassNameNotFoundEvent`

#### `Gacela\Framework\Event\ClassResolver\Cache`
- `ClassNameCacheCachedEvent`
- `ClassNamePhpCacheCreatedEvent`
- `ClassNameInMemoryCacheCreatedEvent`
- `CustomServicesCacheCachedEvent`
- `CustomServicesPhpCacheCreatedEvent`
- `CustomServicesInMemoryCacheCreatedEvent`

#### `Gacela\Framework\Event\ConfigReader`
- `ReadPhpConfigEvent`

## Disabling events

Turn the whole system off — no listeners fire, and Gacela swaps in a no-op dispatcher:

```php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->disableEventListeners();
};
```

This setting wins over registrations: listeners remain configured but silently do not run. Check `disableEventListeners()` first when a production listener appears inactive.

## Custom dispatcher

Gacela's default dispatcher implements `EventDispatcherInterface`:

```php
interface EventDispatcherInterface
{
    public function dispatch(object $event): void;

    // Whether any listener would receive an event of the given class,
    // so hot-path dispatch sites can skip allocating the event.
    public function hasListeners(string $eventClass): bool;
}
```

## See also

- [Testing](https://gacela-project.com/docs/testing.md) — `GacelaTestCase` records these events and turns them into assertions (`assertServiceResolved()`, `assertBindingRegistered()`).
- [Module Customization](https://gacela-project.com/docs/customization.md#listening-to-internal-events) — where listeners fit among the other `gacela.php` hooks.
- [Bootstrap](https://gacela-project.com/docs/bootstrap.md) — the full `GacelaConfig` surface.

---

Source: https://gacela-project.com/docs/testing.md

# Testing

Gacela ships two PHPUnit helpers for tests: `GacelaTestCase`, the recommended base class for tests that bootstrap a Gacela app, and `ContainerFixture`, the lower-level trait it builds on. PHPUnit is a suggested development dependency, not a Gacela runtime dependency, so require it in your application when using these helpers.

## GacelaTestCase

`GacelaTestCase` is the recommended base class for tests that bootstrap a Gacela app. It extends PHPUnit's `TestCase`, uses the [`ContainerFixture`](#containerfixture) trait internally, and takes care of teardown for you. Reach for `ContainerFixture` directly only when you can't extend this class.

### Setup

```php
use Gacela\Framework\Testing\GacelaTestCase;

final class CheckoutTest extends GacelaTestCase
{
    public function test_facade_resolves_payment_gateway(): void
    {
        $this->bootstrapGacelaWithConfig(__DIR__, ['retries' => 3]);

        (new CheckoutFacade())->pay();

        $this->assertServiceResolved(PaymentGateway::class);
    }
}
```

No `#[Before]` or `resetContainer()` call needed. `bootstrapGacela()` / `bootstrapGacelaWithConfig()` reset the in-memory cache before bootstrapping, and `tearDown()` resets the container and clears recorded events automatically, so state never leaks between tests.

### Available methods

| Method | Description |
|--------|-------------|
| `bootstrapGacela(string $appRootDir, ?Closure $configFn = null)` | Bootstrap Gacela from a clean in-memory state and start recording lifecycle events dispatched from this point onward. Optional closure receives `GacelaConfig` for extra setup |
| `bootstrapGacelaWithConfig(string $appRootDir, array $configKeyValues)` | Bootstrap with the given config key-values in one call (calls `addAppConfigKeyValues()` internally). The most common override in tests |
| `recordedGacelaEvents()` | All `GacelaEventInterface` events recorded since the last bootstrap, in dispatch order |
| `recordedGacelaEventsOf(string $eventClass)` | The recorded events of one type, in dispatch order |
| `assertServiceResolved(string $serviceId)` | Assert the container instantiated the given service id since the last bootstrap |
| `assertBindingRegistered(string $id)` | Assert a binding, alias or contextual binding was registered under the given id since the last bootstrap |

::: tip Event-backed assertions
`assertServiceResolved()` and `assertBindingRegistered()` read from Gacela's own lifecycle events (`ServiceResolvedEvent` and `BindingRegisteredEvent`), recorded automatically from `bootstrapGacela()` onward. See the [events catalog](https://gacela-project.com/docs/events.md) for the full list, and fall back to `recordedGacelaEvents()` / `recordedGacelaEventsOf()` for anything the two helpers don't cover.
:::

### Asserting on recorded events

Use `recordedGacelaEventsOf()` for anything more specific than "was a service resolved" — counting events, or reading a payload off one:

```php
use Gacela\Framework\Event\Config\ConfigKeyReadEvent;
use Gacela\Framework\Event\Container\ServiceResolvedEvent;
use Gacela\Framework\Testing\GacelaTestCase;

final class CheckoutEventsTest extends GacelaTestCase
{
    public function test_payment_gateway_is_resolved_once(): void
    {
        $this->bootstrapGacela(__DIR__);

        (new CheckoutFacade())->pay();
        (new CheckoutFacade())->pay();

        self::assertCount(1, $this->recordedGacelaEventsOf(ServiceResolvedEvent::class));
    }

    public function test_retries_key_is_read_from_config(): void
    {
        $this->bootstrapGacelaWithConfig(__DIR__, ['retries' => 3]);

        (new CheckoutFacade())->pay();

        $events = $this->recordedGacelaEventsOf(ConfigKeyReadEvent::class);

        self::assertSame('retries', $events[0]->key());
    }
}
```

### Asserting on bindings

```php
use Gacela\Framework\Testing\GacelaTestCase;

final class LoggingBindingTest extends GacelaTestCase
{
    public function test_logger_binding_is_registered(): void
    {
        $this->bootstrapGacela(__DIR__, function (GacelaConfig $config) {
            $config->addBinding(LoggerInterface::class, NullLogger::class);
        });

        $this->assertBindingRegistered(LoggerInterface::class);
    }
}
```

## ContainerFixture

The trait provides helpers to reset, snapshot and restore the container state so tests don't bleed into each other.

### Setup

```php
use Gacela\Framework\Testing\ContainerFixture;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\TestCase;

final class MyTest extends TestCase
{
    use ContainerFixture;

    #[Before]
    protected function setUpContainer(): void
    {
        $this->resetContainer();
    }
}
```

### Available methods

| Method | Description |
|--------|-------------|
| `resetContainer()` | Wipe the container and all static caches. Clean slate for the next test |
| `captureContainerState()` | Return a `ContainerSnapshot` of config values and the in-memory class-name cache (not resolved service instances) |
| `restoreContainerState(ContainerSnapshot $snapshot)` | Restore a snapshot previously returned by `captureContainerState()` |
| `containerTempDir()` | Return a per-test temporary directory, removed at process shutdown (or synchronously via `cleanupContainerTempDirs()`) |

### Snapshot and restore

Use `captureContainerState()` / `restoreContainerState()` when a test mutates the container but subsequent assertions need the original state:

```php
public function testServiceOverride(): void
{
    $snapshot = $this->captureContainerState();

    Gacela::bootstrap(__DIR__, function (GacelaConfig $config) {
        $config->addBinding(LoggerInterface::class, NullLogger::class);
    });

    // ... assertions with NullLogger ...

    $this->restoreContainerState($snapshot);

    // container is back to its pre-override state
}
```

### Temporary directories

`containerTempDir()` returns a unique temporary directory for the current test. Use it for file-cache tests, artifact storage, or anything that writes to disk:

```php
public function testFileCacheWrite(): void
{
    $cache = new FileCache($this->containerTempDir());
    $cache->put('key', 'value', ttl: 60);

    self::assertSame('value', $cache->get('key'));
    // temp dirs are removed at process shutdown; call cleanupContainerTempDirs() for synchronous per-test cleanup
}
```

## Test hygiene

- Prefer `resetContainer()` in a `#[Before]` method over `setUp()`. It makes the intent explicit and works alongside other `setUp` logic.
- For integration tests that need the full bootstrap, call `Gacela::bootstrap()` inside the test and `resetContainer()` in teardown.
- `ContainerFixture` replaces the older pattern of calling `$config->resetInMemoryCache()` inside `gacela.php` for tests.

---

Source: https://gacela-project.com/docs/static-analysis.md

# Static analysis

Gacela ships PHPStan rules, Psalm configuration, and a 2.0 Psalm plugin for dynamic pillar accessors and module architecture.

## PHPStan

Include in your `phpstan.neon`:

```neon
includes:
    - vendor/gacela-project/gacela/phpstan-gacela.neon
```

`phpstan-gacela.neon` types declared accessors and enables architectural rules:

- **Naming conventions** — a `Facade` / `Factory` / `Provider` / `Config` class must extend the matching Gacela abstract (`SuffixExtendsRule`).
- **`FacadeOnlyDelegatesRule`** — a Facade only delegates to its Factory instead of holding business logic.
- **`FactoryDoesNotCallFacadeRule`** — a Factory never calls back into a Facade.
- **`CrossModuleViaFacadeRule`** — opt-in (commented out in the shipped config): enforces that modules communicate only through Facades. See [Enforcing module boundaries](#enforcing-module-boundaries).

### Typed pillar accessors

`#[ServiceMap]` gives a magic accessor a real return type:

```php
#[ServiceMap(method: 'getFacade', className: CheckoutFacade::class)]
final class CheckoutController
{
    use ServiceResolverAwareTrait;

    public function __invoke(): Response
    {
        return $this->getFacade()->placeOrder();
    }
}
```

PHPStan now checks `placeOrder()` and every call reached through the Facade. A native `@method CheckoutFacade getFacade()` annotation is also understood, though the attribute remains the forward-compatible runtime declaration.

### Typed provided dependencies

The class-string form of `getProvidedDependency()` returns the named type:

```php
$clock = $this->getProvidedDependency(Clock::class); // inferred as Clock
```

A plain string key still returns `mixed` because no type is encoded in the key. Factories themselves may also declare constructor dependencies; pillar construction goes through the container and is autowired:

```php
final class CheckoutFactory extends AbstractFactory
{
    public function __construct(private readonly Clock $clock) {}
}
```

### Facade interfaces

`FacadeInterfaceInSyncRule` is enabled by default for a `FooFacade` that explicitly implements `FooFacadeInterface`. It reports public Facade methods missing from that interface, preventing consumers typed against the interface from silently seeing a smaller API. Facades with no matching interface are ignored.

### Enforcing module boundaries

`CrossModuleViaFacadeRule` ships commented out in `phpstan-gacela.neon`. Uncomment it and pass your namespaces to enable it:

```neon
services:
    -
        class: Gacela\PHPStan\Rules\CrossModuleViaFacadeRule
        tags: [phpstan.rules.rule]
        arguments:
            rootNamespace: App\Modules
            modulePathSegments: 1
            sharedNamespaces:
                - App\Modules\Shared
```

- `rootNamespace` (string, required) — your project's module root, e.g. `App\Modules`.
- `modulePathSegments` (int, default `1`) — how many namespace segments beneath the root identify a single module.
- `sharedNamespaces` (list of strings, default `[]`) — shared kernels exempt from the boundary: references *into* them are always allowed, and classes *inside* them aren't checked.

The rule covers construction, static calls, class constants, and static properties. Namespace matching respects boundaries, so `App\Modules\Shared` does not accidentally exempt `App\Modules\SharedFoo`.

### Dependency cycles and graph review

Use the CLI graph as an architecture gate:

```bash
vendor/bin/gacela debug:graph --check
```

Reviewed cycles can be recorded with a required reason:

```json
[
    {
        "modules": ["App\\Billing", "App\\Invoicing"],
        "reason": "Reviewed temporary boundary while extracting a shared kernel"
    }
]
```

```bash
vendor/bin/gacela debug:graph --check --allowed-cycles=allowed-module-cycles.json
```

The allowlist is self-invalidating: an entry that no longer matches a real cycle fails, preventing stale exceptions from becoming permanent mute buttons.

To make architecture changes visible in a pull request, save JSON on the base branch and compare it on the feature branch:

```bash
vendor/bin/gacela debug:graph --format=json > base-graph.json
vendor/bin/gacela debug:graph --compare-to=base-graph.json > graph-diff.md
```

The diff is GitHub-flavored Markdown with a Mermaid diagram. An unchanged graph writes nothing and exits successfully; an unreadable or invalid baseline exits non-zero.

Accurate module return types across `getFactory()`, `getConfig()` and `getProvidedDependency()` come from the `@template` annotations on Gacela's abstract classes plus the `@extends` on your concrete module classes — independent of this config.

::: warning Declare every dynamic accessor
Add `#[ServiceMap]` or a native `@method` annotation for each `getFacade()`-style accessor. Otherwise PHPStan reports an undefined method. A declared return type also enables analysis of every subsequent call.
:::

## Psalm

```xml
<?xml version="1.0"?>
<psalm
    xmlns:xi="http://www.w3.org/2001/XInclude"
    xmlns="https://getpsalm.org/schema/config"
>
    <projectFiles>
        <directory name="src"/>
    </projectFiles>

    <plugins>
        <pluginClass class="Gacela\Psalm\Plugin"/>
    </plugins>

    <xi:include href="vendor/gacela-project/gacela/psalm-gacela.xml"/>

    <issueHandlers>
        <InvalidArgument>
            <errorLevel type="suppress">
                <directory name="src" />
            </errorLevel>
        </InvalidArgument>
    </issueHandlers>
</psalm>
```

The `InvalidArgument` suppression is required because Gacela resolves concrete types at runtime that Psalm can't infer statically. Suppress inline if you prefer narrower scope:

```php
/** @psalm-suppress InvalidArgument */
return new YourService($this->getConfig());
```

The plugin reads `#[ServiceMap(method: 'getFacade', className: MyFacade::class)]` and gives the magic accessor its real return type. Without it, `psalm-gacela.xml` can suppress the undefined magic call but the result becomes `mixed`, disabling checks on subsequent method calls.

The plugin cannot be delivered by the XInclude because `<plugins>` belongs elsewhere in the Psalm document, so the explicit block is required.

## Troubleshooting

- **PHPStan can't find the file**: verify the include path resolves relative to your `phpstan.neon`.
- **Psalm ignores the include**: ensure `xmlns:xi="http://www.w3.org/2001/XInclude"` is declared, then `vendor/bin/psalm --clear-cache`.

## See also

- [PHPStan: ignoring errors](https://phpstan.org/user-guide/ignoring-errors)
- [Psalm configuration](https://psalm.dev/docs/running_psalm/configuration/)
- [Gacela `#[ServiceMap]`](https://gacela-project.com/docs/service-map.md)

---

Source: https://gacela-project.com/docs/other-frameworks.md

# Framework integration

Gacela runs beside Laravel or Symfony rather than replacing them. The host framework owns HTTP, console, and lifecycle integration; Gacela owns module boundaries. Bridge only the services that cross between those responsibilities.

::: tip Where to bootstrap
- **Symfony** — `public/index.php` and `bin/console`
- **Laravel** — `bootstrap/app.php`
:::

## Example projects

Cloneable minimal integrations:

- **Laravel** — [gacela-project/laravel-gacela-example](https://github.com/gacela-project/laravel-gacela-example)
- **Symfony** — [gacela-project/symfony-gacela-example](https://github.com/gacela-project/symfony-gacela-example)

## Laravel

Bootstrap Gacela in `bootstrap/app.php`, then call module [Facades](https://gacela-project.com/docs/facade.md) from Laravel entry points. Keep Laravel services behind interfaces registered through [bindings](https://gacela-project.com/docs/bindings.md). The [Laravel example](https://github.com/gacela-project/laravel-gacela-example) contains the complete setup.

## Symfony

### Symfony bridge preview

The bridge teaches Symfony's container to honor Gacela's [`#[Inject]`](https://gacela-project.com/docs/inject.md) attribute on Symfony-managed commands and controllers.

::: warning Preview, not an installable release
`gacela-project/symfony-bridge` currently lives in the [Gacela repository](https://github.com/gacela-project/gacela/tree/main/symfony-bridge) and is not published on Packagist. Do not add it to production Composer requirements yet. The stable approach is constructor injection with services explicitly shared between the two containers.
:::

When evaluating the bridge from the monorepo, register its compiler pass in the kernel or bundle:

```php
use Gacela\SymfonyBridge\GacelaInjectCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class AppKernel extends Kernel
{
    protected function build(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new GacelaInjectCompilerPass());
    }
}
```

At compile time, the pass rewrites `#[Inject]` constructor parameters to resolve through Gacela. It rejects parameters that already have an explicit Symfony argument, preventing ambiguous ownership.

See the [Inject attribute](https://gacela-project.com/docs/inject.md) page for the full `#[Inject]` reference.

### Share Symfony's Doctrine EntityManager

Bind `EntityManagerInterface::class` to Symfony's managed service so Gacela modules share the same connection and transactions:

```php
<?php # public/index.php

// ...
$kernel = new \App\Kernel($_SERVER['APP_ENV']);

Gacela::bootstrap($appRootDir, function (GacelaConfig $config) use ($kernel) {
    $config->addBinding(ProductRepositoryInterface::class, ProductRepository::class);

    $config->addBinding(
        EntityManagerInterface::class,
        static fn () => $kernel->getContainer()->get('doctrine.orm.entity_manager'),
    );
});
// ...
```

Modules that type-hint `EntityManagerInterface` now receive Symfony's managed instance. Symfony remains responsible for its lifecycle and configuration.

---

Source: https://gacela-project.com/docs/extra.md

# Single-file modules

Use this pattern when a directory-per-module structure would add more ceremony than clarity. For application modules expected to grow, use the conventional layout from the [Quickstart](https://gacela-project.com/docs/quickstart.md).

## Gacela in a file

`Gacela::addGlobal()` lets you bind Gacela pillar classes (Facade, Factory, Provider, Config) to a shared context. When no context is passed, the current file is used. This means you can wire a full module in a single file using anonymous classes.

::: tip When is this useful?
Prototyping, one-off scripts, or small CLI tools where a full directory-per-module structure would be overkill.
:::

### 1. Bootstrap and domain classes

```php
<?php declare(strict_types=1);
# file: local/gacela-in-a-file.php

require __DIR__ . '/../vendor/autoload.php';

use Gacela\Framework\AbstractConfig;
use Gacela\Framework\AbstractFacade;
use Gacela\Framework\AbstractFactory;
use Gacela\Framework\AbstractProvider;
use Gacela\Framework\Bootstrap\GacelaConfig;
use Gacela\Framework\Container\Container;
use Gacela\Framework\Gacela;

Gacela::bootstrap(__DIR__, function (GacelaConfig $config) {
    $config->addAppConfigKeyValue('default-name', 'Gacela');
});
```

Two simple domain classes (these would normally live in your module's `Domain/` or `Application/` directory):

```php
final class Printer
{
    public function print(string $str): void
    {
        echo $str;
    }
}

final class Greeter
{
    public function __construct(
        private readonly Printer $printer,
        private readonly string $defaultName,
    ) {}

    public function greet(string $name): void
    {
        if ($name === '') {
            $name = $this->defaultName;
        }
        $this->printer->print("Hello, {$name}!\n");
    }
}
```

### 2. Wire the Gacela pillars as anonymous classes

Each anonymous class is bound to the same file context via `addGlobal()`, so they auto-resolve each other:

```php
// Facade: the entry point
$facade = new class() extends AbstractFacade {
    public function greet(string $name): void
    {
        $this->getFactory()
            ->createGreeter()
            ->greet($name);
    }
};

// Factory: creates internal objects, pulls config and provided deps
Gacela::addGlobal(
    new class() extends AbstractFactory {
        public function createGreeter(): Greeter
        {
            return new Greeter(
                $this->getProvidedDependency('printer'),
                $this->getConfig()->getDefaultName(),
            );
        }
    },
);

// Provider: defines cross-module / external dependencies
Gacela::addGlobal(
    new class() extends AbstractProvider {
        public function provideModuleDependencies(Container $container): void
        {
            $container->set('printer', static fn () => new Printer());
        }
    },
);

// Config: reads from config files
Gacela::addGlobal(
    new class() extends AbstractConfig {
        public function getDefaultName(): string
        {
            return $this->get('default-name');
        }
    },
);
```

### 3. Use the Facade

```php
$facade->greet('World');  // Hello, World!
$facade->greet('');       // Hello, Gacela!
```

```bash
php local/gacela-in-a-file.php

Hello, World!
Hello, Gacela!
```

### How `addGlobal()` works

`Gacela::addGlobal()` binds a class to a context (2nd argument). When omitted, the current file path is used as the context. Because all four anonymous classes above share the same file context, the Facade automatically resolves its Factory, the Factory resolves the Provider and Config, just like a regular directory-based module.

## Related resources

- [Example project](https://github.com/gacela-project/gacela-example): A complete module example
- [API skeleton](https://github.com/gacela-project/api-skeleton): A skeleton to build an API using Gacela
- [Router](https://github.com/gacela-project/router): A minimalistic HTTP router
- [Container](https://github.com/gacela-project/container): A minimalistic dependency container

See how Gacela works with **Symfony**, **Laravel**, and [other frameworks](https://gacela-project.com/docs/other-frameworks.md).

---

Source: https://gacela-project.com/about-gacela.md

# About Gacela

Gacela is a lightweight module framework for PHP 8.3+. It gives every module one public entry point and keeps construction, cross-module wiring, and configuration behind that boundary.

The goal is practical: make a large codebase easier to navigate and change without forcing domain code to depend on Gacela.

## The problem it solves

Without an explicit boundary, one feature can reach into another feature's controllers, repositories, container IDs, or internal services. Those shortcuts make changes unpredictable because private implementation details become an accidental public API.

Gacela replaces that ambiguity with four recognizable roles:

| Role | One responsibility | Add it when |
|---|---|---|
| [Facade](https://gacela-project.com/docs/facade.md) | Expose the module's public capabilities | The module has a caller |
| [Factory](https://gacela-project.com/docs/factory.md) | Construct services owned by the module | The Facade delegates work |
| [Provider](https://gacela-project.com/docs/provider.md) | Supply another module's Facade or infrastructure | A service crosses a boundary |
| [Config](https://gacela-project.com/docs/config.md) | Expose application settings through typed getters | Construction needs configuration |

A small module may need only a Facade and Factory. Provider and Config are optional, not ceremony to create in advance.

## Design from the caller inward

Start with the operation a controller, command, script, or another module needs. Let that real use case determine the boundary:

```text
caller → Facade → Factory → application/domain service
                              ↓
                       Provider or Config
```

1. Write the call you want to make.
2. Express it as a focused Facade method.
3. Let the Factory build the service that fulfills it.
4. Add Provider or Config wiring only when the service reveals that need.

The [Quickstart](https://gacela-project.com/docs/quickstart.md) builds a complete module in exactly this order.

## Why decoupling?

Decoupling is useful when it makes change local and dependencies visible. It is not an instruction to wrap every class in an interface.

Consider a Billing module that sends an invoice. Billing should depend on a capability such as `CustomerFacadeInterface`, not on Customer's repository or database implementation:

```text
Billing → Customer Facade → Customer internals
```

That boundary creates concrete benefits:

- **Safer changes:** Customer can replace its storage or internal services without changing Billing.
- **Focused tests:** Billing can replace the Facade interface with a small test double.
- **Clear ownership:** a dependency on another module is visible in Billing's Provider.
- **Faster navigation:** developers know where to enter a module and where to inspect its wiring.
- **Framework independence:** domain and application services remain ordinary PHP objects.

### What stays decoupled

Gacela wiring belongs at the edges. Domain and application services do not need to extend Gacela classes or know about its container.

```php
final readonly class SendInvoice
{
    public function __construct(
        private InvoiceRepositoryInterface $invoices,
        private CustomerFacadeInterface $customers,
    ) {}
}
```

The Factory supplies module-owned collaborators. The Provider supplies `CustomerFacadeInterface`. The service remains explicit and testable.

### Boundaries are not layers

Gacela works with layered, hexagonal, vertical-slice, or other architectures. A module may contain Domain, Application, and Infrastructure directories, but Gacela does not require them. It standardizes communication **between modules**, not the internal design of each module.

## A typical module

```text
src/Billing/
├── Application/
├── Domain/
├── Infrastructure/
├── BillingFacade.php
├── BillingFactory.php
├── BillingProvider.php   # only when external dependencies exist
└── BillingConfig.php     # only when application settings exist
```

Names may use the shorter `Facade.php`, `Factory.php`, `Provider.php`, and `Config.php` convention shown in the [Quickstart](https://gacela-project.com/docs/quickstart.md). Pick one project convention and keep it consistent.

## When Gacela fits

Gacela is a strong fit when:

- a PHP application has several features or teams;
- module internals change more often than their public capabilities;
- cross-module dependencies are difficult to discover;
- the project needs enforceable boundaries without replacing its framework.

It may be unnecessary for a small script or a single cohesive component. Even there, the [single-file module pattern](https://gacela-project.com/docs/extra.md) is available when a lightweight boundary still helps.

## Continue

- [Build the first module](https://gacela-project.com/docs/quickstart.md)
- [Choose a dependency mechanism](https://gacela-project.com/docs/getting-dependencies.md)
- [Explore production code from Phel](https://gacela-project.com/used-in.md)

---

Source: https://gacela-project.com/used-in.md

# Gacela in production: Phel

[Phel](https://phel-lang.org/) 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.

::: info 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`](https://github.com/phel-lang/phel-lang/tree/f173cf522d1b492cf12fb5404fa56c6b4bd454a4).
:::

<div class="gz-case-study-stats">
  <div class="gz-case-study-stat"><strong>17+</strong><span>application modules</span></div>
  <div class="gz-case-study-stat"><strong>PHP 8.4</strong><span>declared platform</span></div>
  <div class="gz-case-study-stat"><strong>MIT</strong><span>open-source license</span></div>
</div>

## 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.

::: code-group

```php [Bootstrap]
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);
}
```

```php [Facade]
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);
    }
}
```

```php [Provider]
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);
    }
}
```

```php [Symfony command]
#[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;
    }
}
```

```php [Health check]
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](https://github.com/phel-lang/phel-lang/blob/f173cf522d1b492cf12fb5404fa56c6b4bd454a4/src/php/Phel.php), [RunFacade](https://github.com/phel-lang/phel-lang/blob/f173cf522d1b492cf12fb5404fa56c6b4bd454a4/src/php/Run/RunFacade.php), [RunProvider](https://github.com/phel-lang/phel-lang/blob/f173cf522d1b492cf12fb5404fa56c6b4bd454a4/src/php/Run/RunProvider.php), [CompileCommand](https://github.com/phel-lang/phel-lang/blob/f173cf522d1b492cf12fb5404fa56c6b4bd454a4/src/php/Run/Infrastructure/Command/CompileCommand.php), and [BuildHealthCheck](https://github.com/phel-lang/phel-lang/blob/f173cf522d1b492cf12fb5404fa56c6b4bd454a4/src/php/Build/Application/BuildHealthCheck.php).

## What to copy into your project

The useful pattern is the direction of dependencies, not Phel's exact filenames:

```text
entry point → Facade → Factory → application/domain service
                         ↓
                     Provider → another module's Facade interface
```

Start a new module with the [Quickstart](https://gacela-project.com/docs/quickstart.md), then use [Getting dependencies](https://gacela-project.com/docs/getting-dependencies.md) when it needs to communicate with another boundary.

## Explore Phel

- [Phel website](https://phel-lang.org/)
- [Source repository](https://github.com/phel-lang/phel-lang)
- [Phel on Packagist](https://packagist.org/packages/phel-lang/phel-lang)
