Phalcon Kit applications are configured through app-owned config classes. Keep secrets in environment files and keep application structure in code:
- modules
- providers
- router defaults
- model aliases
- permissions and roles
- locale defaults
- service integrations
Think of configuration in three layers:
| Layer | Keep here | Example |
|---|---|---|
| Package defaults | Reusable framework behavior | default providers and router targets |
| App config | Reviewable application structure and policy | modules, aliases, permissions |
| Environment | Machine-specific values and secrets | database host, credentials, feature toggles |
App config should merge with package defaults intentionally. Environment values should provide data to config, not replace the config structure.
Official Phalcon references:
- Config: https://docs.phalcon.io/latest/config/
- Dependency injection: https://docs.phalcon.io/latest/di/
- Routing: https://docs.phalcon.io/latest/routing/
Example .env values:
APP_NAME="My App"
DATABASE_HOST=127.0.0.1
DATABASE_DBNAME=app
DATABASE_USERNAME=app
DATABASE_PASSWORD=appKeep secrets and machine-specific paths in environment files. Keep module registration, providers, aliases, and policy in config classes so they can be reviewed and versioned.
The default mysql driver uses MySQL connection settings, including a MySQL
collation and block_encryption_mode. MariaDB needs its own compatible options.
Add this independent driver inside your App config's defaults (before the parent
constructor), using PhalconKit\Support\Env:
'database' => [
'default' => 'mariadb',
'drivers' => [
'mariadb' => [
'adapter' => \PhalconKit\Db\Adapter\Pdo\Mysql::class,
'dialectClass' => \PhalconKit\Db\Dialect\Mysql::class,
'host' => Env::get('DATABASE_HOST', '127.0.0.1'),
'port' => (int)Env::get('DATABASE_PORT', 3306),
'dbname' => Env::get('DATABASE_DBNAME', ''),
'username' => Env::get('DATABASE_USERNAME', ''),
'password' => Env::get('DATABASE_PASSWORD', ''),
'charset' => 'utf8mb4',
'options' => [
\PDO\Mysql::ATTR_INIT_COMMAND =>
"SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci, sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'",
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::ATTR_STRINGIFY_FETCHES => false,
],
],
],
],Use a new driver name so MySQL-only options are not inherited or appended during
config merging. devtools.php uses the same selected driver for migrations.
For a local Unix socket, provide unix_socket in the driver descriptor instead
of host/port. Configure TLS options for remote database connections according to
your database deployment.
| Setting | Use |
|---|---|
APP_ENV / APP_DEBUG |
Environment label and debug output; keep debug false in shared environments |
APP_CACHE |
Application cache toggle; select appropriate backing services separately |
APP_TIMEZONE |
Application timestamps/timezone |
DATABASE_* |
Connection credentials and database name |
SECURITY_JWT_PASSPHRASE |
Private JWT signing key |
IDENTITY_AUTHORIZATION_HEADER |
Bearer header; defaults to X-Authorization |
IDENTITY_STATELESS |
Explicit choice of stateless identity semantics |
SESSION_COOKIE_SECURE |
HTTPS cookie flag; disable only for local HTTP testing |
RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN |
Exact allowed frontend origins |
SWOOLE_HOST / SWOOLE_PORT |
Optional worker listener; loopback behind a reverse proxy |
See Authentication for token/cookie examples and Application Security for deployment settings.
<?php
namespace App;
use PhalconKit\Support\Env;
final class Config extends \PhalconKit\Bootstrap\Config
{
public function __construct(array $data = [], bool $insensitive = false)
{
$data = $this->internalMergeAppend([
'app' => [
'name' => Env::get('APP_NAME', 'My App'),
],
'modules' => [
\PhalconKit\Mvc\Module::NAME_API => [
'className' => \App\Modules\Api\Module::class,
'path' => APP_PATH . 'Modules/Api/Module.php',
],
],
], $data);
parent::__construct($data, $insensitive);
}
}Use internalMergeAppend() when the app wants to keep Phalcon Kit defaults and
append or override only the app-owned parts.
Modules define runtime boundaries. Common module names are:
frontendadminapioauth2cliws
Register app modules explicitly:
'modules' => [
\PhalconKit\Mvc\Module::NAME_API => [
'className' => \App\Modules\Api\Module::class,
'path' => APP_PATH . 'Modules/Api/Module.php',
],
],Expected MVC request failures raised as
PhalconKit\Exception\HttpException use the dedicated
router.httpException target. Its defaults keep the current module context and
dispatch to errorAction():
'router' => [
'httpException' => [
'namespace' => Env::get('ROUTER_HTTP_EXCEPTION_NAMESPACE'),
'module' => Env::get('ROUTER_HTTP_EXCEPTION_MODULE'),
'controller' => Env::get('ROUTER_HTTP_EXCEPTION_CONTROLLER', 'error'),
'action' => Env::get('ROUTER_HTTP_EXCEPTION_ACTION', 'error'),
],
],Applications may override this route and controller to adapt their response
envelope. The dispatcher still owns status validation: only HttpException codes
from 400 through 599 are preserved, and invalid codes become HTTP 500. Generic
exceptions never derive transport status from their numeric exception code;
production failures use router.fatal, while debug mode rethrows them.
Provider overrides are config-first. Replace a core provider by keeping the core provider class as the key. Register new app services with the app provider as both key and value.
'providers' => [
\PhalconKit\Provider\Identity\ServiceProvider::class =>
\App\Provider\Identity\ServiceProvider::class,
\App\Provider\Firebase\ServiceProvider::class =>
\App\Provider\Firebase\ServiceProvider::class,
],Common provider categories include database, cache, session, identity, ACL, router, request/response, logger/loggers, mailer, Redis, Swoole, OpenAI, OAuth, filesystem, translation, view, Volt, URL, and helpers.
Use app providers when a service needs app configuration, app credentials, or a different implementation. Avoid replacing a core provider just to change one runtime option when config already supports it.
Storage-backed cache adapters can restrict PHP object
deserialization with cache.default.allowedClasses. PhalconKit defaults this
to true for compatibility with model and application object caches. Set
CACHE_ALLOWED_CLASSES=false when the cache stores only scalars and arrays, or
set an explicit class-name list in application config when cached objects are
required. Clear incompatible cache entries when tightening the policy.
Use eventsManager.listeners for app-owned listeners that should attach to the
shared Phalcon events manager during bootstrap:
'eventsManager' => [
'listeners' => [
'dispatch' => [
[
'class' => \App\Listeners\SecurityHeaders::class,
'priority' => 200,
],
[
'service' => 'auditDispatchListener',
'priority' => 100,
],
],
'db' => [
\App\Listeners\QueryCorrelation::class,
],
],
],Listeners are grouped by Phalcon event type, such as dispatch, db, model,
or view. A listener can be a class name, a DI service name, or an array with
class or service. Array definitions also support priority, arguments,
and enabled => false.
The bootstrap attaches these listeners after providers are registered and before modules/router setup. Core providers keep their existing built-in listener wiring; this config is for application listeners that should participate in the same shared event manager without replacing providers.
API-only applications can keep Phalcon Kit's normal session service available
while making identity itself stateless:
'identity' => [
'stateless' => true,
],or through the environment:
IDENTITY_STATELESS=trueStateless identity stores the small identity payload, such as userId and
asUserId, directly in the JWT claim instead of PHP session storage. The
session provider still registers and starts the configured Phalcon session
manager, so flash messages, OAuth2 state, locale persistence, and other session
consumers keep their normal behavior.
Do not combine stateless identity with identity.sessionFallback; fallback
storage is ignored when identity.stateless is enabled. Clients must replace
their JWT after login, logout, OAuth2 login, impersonation, and refresh
responses that include new token values. Old JWTs are not server-revoked unless
the application adds its own revocation strategy.
Use a provider when the service belongs in the DI container and is shared by controllers, tasks, models, or other services.
<?php
namespace App\Provider\Report;
use App\Service\ReportExporter;
use PhalconKit\Di\DiInterface;
use PhalconKit\Provider\AbstractServiceProvider;
final class ServiceProvider extends AbstractServiceProvider
{
protected string $serviceName = 'reportExporter';
#[\Override]
public function register(DiInterface $di): void
{
$di->setShared($this->getName(), function () use ($di) {
return new ReportExporter(
$di->getTyped('db', \Phalcon\Contracts\Db\Adapter\Adapter::class),
$di->getTyped('logger', \Phalcon\Contracts\Logger\Logger::class)
);
});
}
}Applications that use PhalconKit\Bootstrap normally do not need to change
anything: bootstrap creates a Phalcon Kit DI container before registering config,
providers, modules, and services.
Custom bootstraps and tests that pass their own container into
Bootstrap::setDI() must pass PhalconKit\Di\DiInterface, such as
PhalconKit\Di\Di, PhalconKit\Di\FactoryDefault, or
PhalconKit\Di\FactoryDefault\Cli. Native Phalcon\Di\Di is no longer the
provider/bootstrap boundary because it does not expose Phalcon Kit's typed
helpers.
Use the typed helpers when the service contract is known:
$config = $di->getConfig();
$view = $di->getTyped('view', \Phalcon\Mvc\ViewInterface::class);Native Phalcon DI signatures may still appear where Phalcon Kit extends native
Phalcon interfaces or classes. App-owned providers should use
PhalconKit\Di\DiInterface.
Register it in app config:
'providers' => [
\App\Provider\Report\ServiceProvider::class =>
\App\Provider\Report\ServiceProvider::class,
],Then use it from an injectable class:
$this->reportExporter->exportProject($projectId);Applications can map framework model roles to app model classes. This keeps identity, permissions, and scaffolded resources decoupled from a fixed model namespace.
'models' => [
'user' => \App\Models\User::class,
'role' => \App\Models\Role::class,
'workspace' => \App\Models\Workspace::class,
],Permission config maps features to components, component methods, optional query behaviors, and roles.
'permissions' => [
'features' => [
'manageLocation' => [
'components' => [
\App\Modules\Api\Controllers\LocationController::class => ['*'],
\App\Models\Location::class => ['*'],
],
],
],
'roles' => [
'admin' => [
'features' => ['manageLocation'],
],
],
],The identity/security system can enforce permissions across controllers, actions, models, methods, CLI tasks, and WebSocket tasks.
Controller/action attributes are enabled by default and are merged into the same
permission graph at runtime. Disable attribute scanning for config-only
applications that want to avoid controller reflection. The default bootstrap
config reads ACL_ATTRIBUTES:
'acl' => [
'attributes' => Env::get('ACL_ATTRIBUTES', true),
],For row-level controller conditions and role inheritance, read Identity And Permissions.
Configuration failures should surface during bootstrap, before a request reaches business code. After changing modules, providers, aliases, or permissions:
composer validate --strict --no-check-publish
composer phpunit
php -S 127.0.0.1:8000 -t public public/index.phpThen exercise one route that resolves each changed service or policy. For a new provider, add a focused test that builds the container and asserts the service implements its intended contract.
Continue with Developer Cookbook for provider and endpoint recipes, or Troubleshooting when bootstrap cannot resolve the expected configuration.