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/dbal: ^4.0
- 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.43
- 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)
- friendsofphp/php-cs-fixer: ^3.0
- igor-php/igor-php: ^0.10.1
- nowo-tech/brand-contracts: ^1.0
- 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
- ext-gd: Required (with PNG, JPEG and WebP support) when a section declares an image field (1.4)
- nowo-tech/brand-contracts: Required for the brand bridge (brand.enabled: true, 1.5): BrandProviderInterface for UiKit, Pwa, MailKit, LegalKit and SeoKit
- 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()
- symfony/yaml: To load YAML seed files with nowo:instance-settings:seed (1.7; JSON seed files need nothing)
Provides
None
Conflicts
- nowo-tech/brand-contracts: <1.0 || >=2.0
Replaces
None
- dev-main
- v1.8.0
- v1.7.1
- v1.7.0
- v1.6.0
- v1.5.0
- v1.4.0
- v1.3.1
- v1.3.0
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.0
- dev-feat/image-crop
- dev-fix/1.7.1-env-adopt
- dev-feat/is-3-seed-file
- dev-feat/is-1-is-2
- dev-feat/brand-s3
- dev-feat/s2-image-field
- dev-chore/symfony-security-2026-11-04
- dev-fix/1.3.1
- dev-feat/1.3.0
- dev-chore/req-gaps
This package is auto-updated.
Last update: 2026-11-09 23:36:17 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. - Seed from fallbacks (DB-only mode) — sections expose their
%env()%/ YAML fallbacks (SectionFallbackProviderInterface);nowo:instance-settings:seed-from-fallbacks(or the publicFallbackSeeder) copies them once where nothing is stored: idempotent, secrets encrypted, audited, names only in the output, safe-guarded against phantom MySQL migration transactions. Section notices show warnings (e.g. missing credentials) on the settings page. - Seed file with images (1.7) —
nowo:instance-settings:seedloads a committed, versioned seed file (values plus logo / icon paths) fill-only and idempotently; paths confined toseed.base_dir, images through the upload checks (no SVG), secrets refused. Optionalfiles.store: filesystemkeeps one stable image file per field (ready to commit) instead of database blobs. - Provider-dependent fields —
SettingField::withVisibleWhen('provider', ['smtp'])shows a field only for some values of another field (UiKitvisible-whenpeer: hidden, never disabled or cleared); saves validate and write only the fields visible for the submitted provider, so the other providers' values survive a switch. - Image fields (1.4) —
SettingField::image()for logos and icons: PNG / JPEG / still WebP detected from the content, limits checked from the header before decoding, every upload re-encoded with GD (EXIF and polyglot payloads dropped), PNG variants, stored in the database content-addressed per field, served from an immutable hashed URL outside the admin prefix (nosniff, sandbox CSP, ETag / 304; anonymous only forpublicfields), exported in envelope version 2 and fully re-validated on import, replaced images purged after a retention. Opt-in (files.enabled: true): hosts without image fields keep the 1.3 schema. SVG uploads are refused.getFile()/instance_setting_file(). - Brand bridge (1.5) — opt-in
brand.enabled: true:brandandappearancesections (name, texts, logos, mark, app icon, favicon, extra images; presets and seven colours per mode with WCAG contrast notices), thenowo-tech/brand-contractsBrandProviderInterface/BrandAssetReaderInterfacefor UiKit, Pwa, mail, legal and SEO,nowo_brand(), and UiKit's appearance provider when the host set none. Committed default assets (SVG allowed) as fallbacks; uploads raster only. - Crop step on image fields (1.8) — opt-in
files.crop: trueorSettingField::withCrop(): UiKit 2.43's crop dialog on the file input, with the aspect, minimum size, format and byte limit derived from the field's own rules (square icons crop square at their minimum size,max_bytes95 % ofmaxBytes). The server still checks and re-encodes every upload; off by default (byte-identical markup). - Embeddable sections (1.6) —
SettingsSectionEmbedder::handle()shows and saves selected sections inside a host route and layout (nav allowlist, host redirect route, template / layout / header overrides) with exactly the bundle route's checks (UI gate, section role, CSRF, secrets, image pipeline). The allowlist only narrows; redirects go to host route names, never URLs. - Theme cards (1.6) —
brand.preset_picker: cards(orSettingField::withThemeCards()on any choice field): an accessible radio group of cards with validated#rrggbbSVG swatches instead of a select. - Fail-closed wiring — the container refuses to compile without DoctrineEncrypt, with the UI on but no form CSRF, with an empty role list, with
allow_unauthenticatedoutsidekernel.debug, or withfiles.enabledand no GD. - Doctrine — table
instance_setting_value, plusinstance_setting_filewithfiles.enabled: true(1.4, image fields), with 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)
ext-gdwith PNG, JPEG and WebP support, only withfiles.enabled: true(image fields, 1.4) orbrand.enabled: true(1.5)nowo-tech/brand-contracts^1.0, only withbrand.enabled: true(1.5)- Doctrine ORM 3.3+ with DoctrineBundle,
nowo-tech/doctrine-encrypt-bundle^2.10 (secrets),nowo-tech/ui-kit-bundle^2.43 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% (1638/1638 statements, 1864/1864 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


