On this page
Extensions and plugins
Pick the narrowest extension point for the job:
| Need | Extension point |
|---|---|
| Run setup after bootstrap | Plugin |
| Decorate or alter one service | extendService() |
| Decorate a service in one module only | extendProviderService() |
| Collect every implementation of one interface | addPluginStack() |
| Add a reusable configuration bundle | extendGacelaConfig() |
| Resolve keyed domain handlers | Handler registry |
Plugins
addPlugin(callable|class-string $plugin);
addPlugins(array $list);A plugin runs custom logic right after Gacela boots. Add one with addPlugin().
<?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);
});A plugin class must be invokable. Gacela resolves its dependencies automatically, as long as you define them as bindings. The same applies to a callable's arguments.
For example, ApiRoutesPlugin in its own file:
<?php # ApiRoutesPlugin.php
final class ApiRoutesPlugin
{
public function __invoke(RouterInterface $router): void
{
$router->configure(function (Routes $routes): void {
$routes->get('{name}', HelloController::class);
});
}
}Plugin stacks Since 2.3
addPluginStack(string $contract, array $plugins);A plugin runs once at bootstrap. A plugin stack is different: it holds every implementation of one interface, in declaration order, resolved lazily and read back typed. Declare it with the interface it accepts:
return static function (GacelaConfig $config): void {
$config->addPluginStack(Discount::class, [
StaffDiscount::class,
TenPercentOff::class,
]);
};Read it in the Factory with getPluginStack(). It returns a PluginStack: countable, iterable, and typed through the
contract.
final class CheckoutFactory extends AbstractFactory
{
public function createDiscounts(): PluginStack
{
return $this->getPluginStack(Discount::class);
}
}public function priceOf(int $cents): int
{
foreach ($this->getFactory()->createDiscounts() as $discount) {
$cents = $discount->apply($cents);
}
return $cents;
}Order matters: members run in declaration order. Repeated addPluginStack() calls for one contract append. That
is how a project adds a member to a stack a package declared. A class declared by both keeps the position its first
declarer gave it.
Members resolve on first use, not at registration. A class that does not exist, or does not implement the contract,
throws at that point. Run doctor to find both at diagnostic time instead.
A member can also join from its own class with #[Plugin], with no line in gacela.php: Since 2.5
use Gacela\Framework\Attribute\Plugin;
#[Plugin(Discount::class, priority: 10)]
final class LoyaltyDiscount implements Discount {}You still declare the stack, empty if the attributes fill it: addPluginStack(Discount::class, []). Reading a stack
nobody declared throws, and the message names the #[Plugin] classes waiting for it. Since 2.6 Declared members
come first, then attribute members by priority, highest first. cache:warm --attributes stores them, and
debug:plugins lists every member and where it was declared. The
upstream guide (opens in a new tab)
covers scanning rules and caching.
Pick a stack when the contract is an interface and you want every implementation of it:
| Question the consumer asks | Use |
|---|---|
| Which implementation matches this key? | Handler registry |
| Give me all of these | Tags |
| Give me every implementation, typed | addPluginStack() |
Extend Service
extendService(string $id, Closure $service);extendService() alters any service. It takes the service id defined in any Provider, and a callable that
receives the service as its first argument and the Container as its second.
An example
Take a module with this Provider, Factory and Facade:
- The
Providerdefines the service'ARRAY_OBJ', anArrayObjectholding[1, 2](seeModule/Provider.php). gacela.phpextends'ARRAY_OBJ'and appends3.- Resolving it through the Facade returns
[1, 2, 3](seeindex.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 one Provider's service Since 2.3
extendProviderService(string $providerClass, string $id, Closure $service);extendService() wraps an id wherever it is registered. If two modules reuse an un-namespaced key such as
'LABEL', both get wrapped, which is rarely what you want. extendProviderService() names the Provider and wraps the
id only there:
return static function (GacelaConfig $config): void {
$config->extendProviderService(
CatalogProvider::class,
CatalogProvider::LABEL,
static fn (array $labels): array => [...$labels, 'wrapped'],
);
};The Catalog module now sees the wrapped value. A Checkout module registering its own 'LABEL' is untouched.
The closure takes the same two arguments as extendService(): the service and the module's Container. Extensions
stack in declaration order. Naming a Provider the application does not have changes nothing; it does not fail.
It also gives a narrower diagnostic. doctor reports an id the named Provider never set()s.
An app-wide extension on a mistyped id can only be reported as matching nothing anywhere.
Extend Gacela Config
extendGacelaConfig(string $configClass);
extendGacelaConfigs(array $list);extendGacelaConfig() extends GacelaConfig from other places. The class must be invokable, and it receives the
GacelaConfig object:
<?php # index.php
Gacela::bootstrap(__DIR__, function (GacelaConfig $config) {
$config->extendGacelaConfig(RouterConfig::class);
});The invokable config class, defined elsewhere:
<?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
addHandlerRegistry(string $registryKey, array<string|int,class-string> $handlers);Declare a build-time dispatch table. The container resolves it under $registryKey as a HandlerRegistry, which
instantiates each handler through the container on first access. Registries are frozen after boot: there is no runtime
register() method.
<?php # gacela.php
return function (GacelaConfig $config) {
$config->addHandlerRegistry(PaymentGatewayInterface::class, [
'stripe' => StripeGateway::class,
'paypal' => PaypalGateway::class,
]);
};Health Check Registration
addHealthCheck(class-string|ModuleHealthCheckInterface $check);Register a per-module health check. The doctor command and the HealthChecker aggregate every registered check.
The Module health checks page has the details.
<?php # gacela.php
return function (GacelaConfig $config) {
$config->addHealthCheck(DatabaseHealthCheck::class);
$config->addHealthCheck(new CacheHealthCheck($redis));
};