On this page
Config
Config turns application key-values into typed module settings. The Factory uses those settings while constructing services, keeping file and environment access out of domain code.
The config file
Define the application values:
<?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 # 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 # 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
}
}Use Config from the Factory
The Factory passes typed settings into ordinary PHP services:
<?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 # 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
Gacela::bootstrap($appRootDir, function (GacelaConfig $config): void {
$config->addAppConfig('config/default.php');
});<?php # config/default.php
return [
'AKISMET-KEY' => 'default-akismet-key',
];<?php # config/default-prod.php
return [
'AKISMET-KEY' => 'production-akismet-key',
];The resolved value for 'AKISMET-KEY' depends on the environment:
- No
APP_ENVset →default-akismet-key APP_ENV=prod→production-akismet-key(overrides the default)
Config dimensions Since 2.3
APP_ENV is one axis. A project that also varies by region, tenant or brand needs more, and addConfigDimension()
declares each extra environment variable that selects configuration:
return static function (GacelaConfig $config): void {
$config->addAppConfig('config/*.php');
$config->addConfigDimension('APP_REGION');
$config->addConfigDimension('APP_TENANT');
};Declaration order is the order of the chain, and each layer refines the one before it. With APP_ENV=prod and
APP_REGION=eu, Gacela reads:
config/app.phpconfig/app-prod.phpconfig/app-prod-eu.php
A variable that is unset ends the chain, so APP_ENV=prod alone stops after the second layer. A local override file
is still read last and still wins.
The merged configuration cache is keyed by the whole tuple, so two regions never serve each other's values. Warm one cache per combination you deploy:
APP_REGION=eu vendor/bin/gacela cache:warm
APP_REGION=us vendor/bin/gacela cache:warmConfig values without files
addAppConfigKeyValue() and addAppConfigKeyValues() set configuration keys directly on GacelaConfig, in
gacela.php or the bootstrap closure:
<?php # gacela.php
return static function (GacelaConfig $config): void {
$config->addAppConfigKeyValue('retries', 3);
$config->addAppConfigKeyValues([
'db.dsn' => 'pgsql://localhost/app',
'features' => ['beta' => true],
]);
};Keys set this way are merged after every file source, so they override values from config/*.php, environment
files, and local overrides. Schema-declared defaults sit at the other end: a key nobody provides falls back to its
declaration, and any source, including these methods, wins over it.
This is also how tests override configuration:
GacelaTestCase::bootstrapGacelaWithConfig() passes its key-values through
addAppConfigKeyValues().
Declaring a config schema Since 2.2
validate:config checks the wiring: bindings, dependency cycles. Nothing checked
the configuration itself, so a missing or misspelled key surfaced as a runtime failure in whichever environment lacked
it: usually production, usually far from the file that should have carried it. Every call site already knows what it
expects, getInt('retries') says so, but that expectation was written nowhere a command could read before anything ran.
Declare it in gacela.php:
use Gacela\Framework\Bootstrap\GacelaConfig;
use Gacela\Framework\Config\Schema\ConfigType;
return static function (GacelaConfig $config): void {
$config->declareConfigSchema([
'db.dsn' => ConfigType::string()->required(),
'retries' => ConfigType::int()->default(3),
'features' => ConfigType::array()->required()->describe('feature flags, keyed by name'),
]);
};Types: string(), int(), float(), bool(), array().
required(): the key must be present after every source is merged.default($value): used when no source provides the key, never over one that does. A defaulted key is therefore never missing, and cannot also be required.describe($text): travels into the violation, where "wrong type" alone leaves the reader guessing.
A float accepts an int: timeout: 5 in a config file is about the value, not about PHP's literal syntax. Nothing
else is coerced; 'true' is not a bool.
Declarations merge per key, so declareConfigSchema() can be called more than once and an extended config can refine
one key without repeating the rest. The later declaration of a key wins.
Where it is checked
Nothing is checked while booting. The declaration is read by the commands you already run:
vendor/bin/gacela validate:config # non-zero when a declared key is unsatisfied
vendor/bin/gacela doctor # the same, as one more check in the deploy gate
vendor/bin/gacela debug:config # marks every key declared / undeclared / missingdebug:config is the counterweight in the other direction: a schema can only report the keys it declares, so the table
flags the ones it does not, and lists a declared key nothing provides even though it has no value to show.
Checking on boot, locally
$config->validateConfigSchemaOnBoot();Moves the report from a command you have to remember to run to the first thing that boots, and throws a
ConfigException listing every violation. It is off by default so bootstrap does no work a project did not ask for.
Leave it off in production, where the deploy gate has already answered the question.
Declared defaults are applied either way: a key with a default is not missing, it is provided by the declaration.
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
command prints the same data as a table, with each key marked against the declared schema.