nowo-tech / instance-settings-bundle
Runtime instance settings for Symfony apps: one settings store split into tagged sections, encrypted secret fields, admin forms and import / export (portability).
Package info
github.com/nowo-tech/InstanceSettingsBundle
Type:symfony-bundle
pkg:composer/nowo-tech/instance-settings-bundle
Fund package maintenance!
Requires
- php: >=8.2 <8.6
- doctrine/doctrine-bundle: ^2.13 || ^3.0
- doctrine/orm: ^3.3
- doctrine/persistence: ^3.1 || ^4.0
- nowo-tech/doctrine-encrypt-bundle: ^2.10
- nowo-tech/form-kit-bundle: ^2.17
- nowo-tech/ui-kit-bundle: ^2.21
- psr/clock: ^1.0
- psr/event-dispatcher: ^1.0
- psr/log: ^2.0 || ^3.0
- symfony/clock: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher: ^7.4 || ^8.0
- symfony/form: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/mime: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/security-core: ^7.4 || ^8.0
- symfony/security-csrf: ^7.4 || ^8.0
- symfony/service-contracts: ^3.5
- symfony/translation-contracts: ^3.5
- symfony/twig-bundle: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
- twig/extra-bundle: ^3.12
- twig/string-extra: ^3.12
- twig/twig: ^3.12
Requires (Dev)
- doctrine/dbal: ^3.8 || ^4.0
- friendsofphp/php-cs-fixer: ^3.0
- igor-php/igor-php: ^0.10.1
- nowo-tech/phpstan-frankenphp: ^1.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.5
- rector/rector: ^2.0
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/css-selector: ^7.4 || ^8.0
- symfony/dom-crawler: ^7.4 || ^8.0
- symfony/filesystem: ^7.4 || ^8.0
- symfony/phpunit-bridge: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/translation: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
Suggests
- symfony/flex: For automatic recipe installation
- symfony/security-bundle: Required for the admin UI unless security.allow_unauthenticated is true (dev / demo only)
- symfony/translation: To return a TranslatableMessage (key + parameters) from SettingsSectionInterface::getIntro()
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-02 10:50:38 UTC
README
⭐ Found this useful? Install from Packagist · Give it a star on GitHub so more developers can find it.
Instance Settings Bundle — runtime settings an operator edits without a deploy: one database-backed store split into sections that the host and other bundles contribute (mailer, SMS provider, Mercure hub, feature flags…), typed getters with defaults, secret fields encrypted at rest with DoctrineEncryptBundle, a UiKit admin UI (one page per section, FormKit forms, CSRF, roles), and a versioned JSON export / import (web page and CLI) with secrets left out or sealed with the instance key. Tested on Symfony 7.4, 8.0 and 8.1 · PHP 8.2+ (Symfony 8.x requires PHP 8.4+).
This bundle is FrankenPHP worker mode friendly.
Reads cost one query per request and are memoized for the current main request only, so a saved change applies on the next request in every worker, also when the kernel is not reset between requests. See docs/FRANKENPHP-WORKER-AUDIT.md.
Screenshots
One tab per section; the encrypted DSN is never rendered back |
Versioned export (secrets sealed on request) and all-or-nothing import |
Features
- Sections — implement
SettingsSectionInterface(or extendAbstractSettingsSection) and declare fields with types, defaults, labels, constraints and flags; autoconfigured, ordered by#[AsSettingsSection(priority:)]. Bundles such as a mailer or an SMS kit contribute their own section without depending on each other. - Typed reads with defaults —
InstanceSettingsInterface::get('mailer.from'),getString(),getInt(),getFloat(),getBool(),getSection(); the stored value, else the field default. Twig:instance_setting('site.name'). - Encrypted secrets —
SettingField::secret()values are stored in an#[Encrypted]column (DoctrineEncrypt, Halite / Defuse), read only throughgetSecret(), never rendered back in forms (blank keeps, a checkbox clears), refused byget()and by Twig. - Worker-safe cache — every row is loaded with one query on the first read of a request, memoized for that main request, cleared by
kernel.resetand after each save;SettingsChangedEvent(field names only) lets listeners reset their own caches. - Validation — types, required fields, e-mail / http(s) URL / choice checks and your Symfony constraints run on every save and import, all-or-nothing; messages name fields, never values.
- Admin UI — UiKit settings scaffold (REQ-UI-003): a tab per section the user may open, FormKit form types, CSRF on every mutation,
security.access_roles+ per-sectionsecurity.area_roles+ custom access checker; extra section actions (e.g. "send a test e-mail"). - Config portability — versioned JSON envelope (
schema,version,exported_at): every portable plain field, secrets only sealed with the instance key (only an instance with the same key can import them), taggedPortabilityProviderInterfaceservices for data stored elsewhere; size-capped upload reader, credential-shaped keys refused, dry run, single transaction. Admin page plusnowo:instance-settings:export/nowo:instance-settings:import. - Fail-closed wiring — the container refuses to compile without DoctrineEncrypt, with the UI on but no form CSRF, with an empty role list, or with
allow_unauthenticatedoutsidekernel.debug. - Doctrine — one table
instance_setting_valuewith a configurabledoctrine.table_prefix, attribute mapping, migrations-friendly.
Installation
composer require nowo-tech/instance-settings-bundle
With Symfony Flex the recipe adds config/packages/nowo_instance_settings.yaml, the route import and a security.yaml reminder. Without Flex, register NowoDoctrineEncryptBundle, NowoUiKitBundle, NowoFormKitBundle and NowoInstanceSettingsBundle, then import the routes — see docs/INSTALLATION.md.
# config/routes/nowo_instance_settings.yaml nowo_instance_settings: resource: . type: nowo_instance_settings
Create the table with a migration (php bin/console doctrine:migrations:diff), then protect the prefix in your firewall:
# config/packages/security.yaml security: access_control: - { path: ^/admin/settings, roles: ROLE_ADMIN }
Quick start
use Nowo\InstanceSettingsBundle\Section\AbstractSettingsSection; use Nowo\InstanceSettingsBundle\Section\SettingField; use Symfony\Component\Validator\Constraints\Range; final class SiteSettingsSection extends AbstractSettingsSection { public function getName(): string { return 'site'; } public function getFields(): iterable { yield SettingField::string('name', 'Acme', 'site.name', required: true); yield SettingField::int('page_size', 25, 'site.page_size', constraints: [new Range(min: 5, max: 200)]); yield SettingField::secret('api_key', 'site.api_key'); } }
public function __construct(private InstanceSettingsInterface $settings) {} $name = $this->settings->getString('site.name'); // 'Acme' until an admin changes it $size = $this->settings->getInt('site.page_size'); $apiKey = $this->settings->getSecret('site.api_key'); // decrypted, or null
Open /admin/settings: one tab per section, the form built from the fields.
Requirements
- PHP 8.2+ (
>=8.2 <8.6; Symfony 8.x requires PHP 8.4+) - Symfony 7.4, 8.0 or 8.1 (FrameworkBundle, Form, Validator, Security CSRF, TwigBundle; SecurityBundle for the admin UI)
- Doctrine ORM 3.3+ with DoctrineBundle,
nowo-tech/doctrine-encrypt-bundle^2.10 (secrets),nowo-tech/ui-kit-bundle^2.17.3 andnowo-tech/form-kit-bundle^2.17 (admin UI),twig/extra-bundle
Configuration
Every key, its default and its validation are documented in docs/CONFIGURATION.md. Safe defaults: UI under /admin/settings for ROLE_ADMIN only, sealed secrets in exports only when asked for, 1 MiB import cap, no table prefix.
Usage
Contributing a section (the API a mailer or an SMS bundle builds on), typed getters, reacting to changes, secrets, export / import and portability providers: docs/USAGE.md.
Demo
demo/symfony8— Symfony 8.1 (FrankenPHP PHP 8.5, worker mode) with MySQL; host port 8079 by default. Sections Site, Mailer (encrypted DSN) and SMS (encrypted token); the home page reads them on every request.
Runtime mode is FRANKENPHP_MODE (worker default, or classic) — see docs/DEMO-FRANKENPHP.md.
make -C demo up-symfony8 # http://localhost:8079 make -C demo/symfony8 setup # encryption key, schema, demo settings make test-e2e # Playwright e2e against the running demo make demo-screenshots # refresh docs/images/demo/*.webp make demo-smoke # boot, HTTP 200, down (REQ-TEST-011)
Development
make up
make install
make test
make cs-check
make phpstan
make igor
make test-coverage-100
make release-check
Documentation
- Installation
- Configuration
- Usage
- Contributing
- Code of Conduct
- Changelog
- Upgrading
- Release
- Security
- Engram
- Spec-driven development
- GitHub Spec Kit
Additional documentation
- Templates (UiKit page contract)
- Architecture
- PSR evaluation (REQ-CS-007)
- Demo (FrankenPHP)
- FrankenPHP worker audit (kernel not reset)
- GitHub Actions CI requirements
Tests and coverage
- Tests: PHPUnit (PHP) — unit tests plus kernel integration tests (admin UI end to end with SecurityBundle, CSRF, encryption at rest on SQLite, import / export, opt-in routes, invalid configuration) and the UiKit page contract test; Playwright e2e on the demo
- PHP: 100% (1303/1303 statements, 1492/1492 elements; gate enforced in CI by
composer test-coverage-100, no exclusions) - TS/JS: N/A
- Python: N/A
License and author
MIT · Nowo.tech · Héctor Franco Aceituno


