On this page
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.
Both bridges ship inside the framework package, so there is nothing extra to require: composer require gacela-project/gacela brings them along. They are versioned with the framework and marked experimental, so their API
may still change between minors.
Symfony: the GacelaBundle Since 2.2
return [
Gacela\SymfonyBridge\GacelaBundle::class => ['all' => true],
];That alone gives you four things:
- Gacela bootstrapped from the kernel, with the project dir as the application root, honouring
gacela.php. Every boot bootstraps again, so a kernel rebooted inside one process (functional tests do it constantly) runs on its own configuration rather than the previous boot's. - Symfony services reachable from Gacela: the ones you list, and only those.
- Gacela's console commands in
bin/console, under agacela:prefix. cache:warmupwarms Gacela's caches too, so a deploy has one warmup step instead of two.
Plus the #[Inject] compiler pass, described below.
gacela:
app_root_dir: '%kernel.project_dir%' # where gacela.php lives
cache_dir: '%kernel.cache_dir%/gacela'
file_cache: true
project_namespaces: ['App']
external_services:
logger: 'monolog.logger'
entity_manager: 'doctrine.orm.entity_manager'
register_commands: true
command_prefix: 'gacela:'Every key is validated at compile time: a mistyped one fails the build instead of quietly configuring nothing.
cache_dir and file_cache left unset leave Gacela's own defaults in place.
External services
external_services maps a key to a Symfony service id. What the key is decides how far the service travels:
gacela:
external_services:
Psr\Log\LoggerInterface: 'monolog.logger' # a type: also bound
report_mailer: 'app.mailer' # a plain key: external service onlyA key that names a class or interface additionally becomes a Gacela binding, so it resolves on
its own: through Gacela::get(), through autowiring, through #[Inject].
A key that names no type stays an external service, which is what your own gacela.php reads when it declares
bindings, because a binding maps a type to an implementation and report_mailer is not one:
$config->addBinding(MailerInterface::class, $config->getExternalService('report_mailer'));Either way the service is fetched through a service locator when Gacela asks for it, so listing one does not construct it: booting the kernel stays as cheap as it was.
Commands
The prefix is not decoration: Symfony's MakerBundle owns the whole make:* namespace, so an unprefixed make:module
would collide with it.
bin/console gacela:make:module App/Blog
bin/console gacela:doctor
bin/console list gacelaSet register_commands: false to leave bin/console alone and keep using vendor/bin/gacela.
The #[Inject] compiler pass
Symfony autowires constructor parameters through its own container, and Gacela's #[Inject] attribute is recognised by
Gacela's container only. On a class managed by Symfony, most often a Command, writing #[Inject] therefore had no
effect: Symfony's autowire claimed the parameter first.
GacelaInjectCompilerPass walks every service definition at compile time, looks at each constructor parameter for
#[Inject], and rewrites the argument so Symfony resolves that slot through Gacela's container instead. If both
containers claim the same parameter, the build fails naming the service and parameter.
The bundle registers the pass for you. To use it without the bundle:
use Gacela\SymfonyBridge\GacelaInjectCompilerPass;
$container->addCompilerPass(new GacelaInjectCompilerPass());
$container->set('gacela.container', Gacela::container());The Gacela container must be registered as a Symfony service named gacela.container so the rewritten arguments can
resolve through it at runtime.
Laravel: the GacelaServiceProvider Since 2.2
return [
Gacela\LaravelBridge\GacelaServiceProvider::class,
];The same four things, against Laravel's lifecycle:
- Gacela bootstrapped when the application boots, with
base_path()as the application root, honouringgacela.php. Every boot bootstraps again, so an application rebooted inside one process runs on its own configuration. Note that Octane boots each worker once and reuses it: a request-scoped Laravel service listed inexternal_servicesstays whatever the worker's first boot captured. - Laravel services reachable from Gacela: the ones you list, and only those.
- Gacela's console commands in
artisan, under agacela:prefix. artisan optimizewarms Gacela's caches too, so a deploy has one optimize step instead of two.optimize:clearclears them again.
php artisan vendor:publish --tag=gacela-configreturn [
'enabled' => true,
'app_root_dir' => null, // where gacela.php lives; null means base_path()
'cache_dir' => null, // null leaves Gacela's own default in place
'file_cache' => null, // null leaves Gacela's own default in place
'project_namespaces' => ['App'],
'external_services' => [
'logger' => 'log',
Psr\Log\LoggerInterface::class => 'log',
],
'register_commands' => true,
'command_prefix' => 'gacela:',
];Every key is validated when the provider boots, naming a mistyped one instead of quietly configuring nothing. Laravel
has no compile step, so boot is the earliest the check can run. External services follow the same rule as the Symfony
bundle: a key that names a type is also bound, a plain key stays an external service, and either way the service is
fetched from Laravel's container lazily. Commands carry the gacela: prefix because artisan owns make:*.
#[Inject] on Laravel-resolved services
Laravel autowires constructor parameters through its own container, so Gacela's #[Inject] used to have no effect on a
class managed by Laravel: a controller, a job, a command. The bridge closes that gap twice over.
On a constructor parameter, use the bridge's attribute with an explicit class. It implements Laravel's
ContextualAttribute contract, so Laravel itself resolves the parameter through Gacela, and because it extends the
Gacela attribute, Gacela honours it too on the classes it builds. One attribute, both containers:
use Gacela\LaravelBridge\Attribute\Inject;
public function __construct(
#[Inject(ProductFacade::class)] private ProductFacade $facade,
) {
}The class is required there: Laravel hands a contextual attribute no parameter to read a type from. Leaving it off fails with directions, not with a silently autowired substitute.
On a property or a setter, the bare form works, since the type is on the member. The provider listens to
afterResolving and injects into every instance Laravel builds, honouring the attribute under either namespace:
use Gacela\Container\Attribute\Inject;
final class SyncStock implements ShouldQueue
{
#[Inject]
private ProductFacade $facade;
}A readonly property is refused by name, since it cannot be written after construction, and so is a static or
non-public setter.
Bootstrapping by hand
The bridges are convenience, not a requirement. Bootstrapping directly from your entry point keeps working and stays the right call when you want full control over the boundary:
Bind a host service explicitly so Gacela modules share the same instance. Symfony's Doctrine EntityManager is the classic case, sharing one connection and one transaction scope:
<?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.
Example projects
Cloneable minimal integrations: