Aller au contenu

omnibridge/symfony

The Symfony host of omnisense/omnibridge's contracts (urls, translations, access, sessions, tokens, environment) - adapters over Symfony's router, translator, security and kernel, their container wiring and OmnibridgeBundle - and the framework packages omnisense/omnibase no longer requires. A Symfony site requires it beside the core.

Étage
Pont
Version
1.x
Vérifié en réel
Son README ne le dit pas
Licence
MIT
composer require omnibridge/symfony:1.x-dev

The Symfony host of omnisense/omnibridge's contracts, and the Symfony side of omnisense/omnibase: the adapters of the host's contracts over Symfony's own services (router, translator, security, CSRF, kernel), their container wiring, and the framework packages the core no longer requires. The mirror of omnibridge/laravel for Laravel.

A Symfony site requires it beside the core:

composer require omnisense/omnibase omnibridge/symfony

omnibase's BaseBundle refuses to start without it. A Laravel site does not require it: it requires omnibridge/laravel instead. A Symfony application without omnibase registers OmnibridgeBundle (Installation).

Beside the contracts, the package wires four families of the Omnisense catalogue into Symfony when they are installed (each only suggested):

Namespace Family What it wires Configured by
Omnibridge\Symfony\Auth\ omnisense/omniauth one Symfony authenticator for every way of signing in, the second factor's token, voter, gate and page, the ceremonies' routes config/packages/omniauth.yaml (Signing in)
Omnibridge\Symfony\Map\ omnisense/omnimap the map providers, the map cache, the Twig functions, the cache's routes and controller config/packages/omnimap.yaml (Maps)
(config/services.php) omnisense/omnidrive Omnidrive\Drives over the application's Flysystem storages nothing: the storages' own configuration (Storages and static export)
Omnibridge\Symfony\Static\ omnibridge/static the console command omnibridge:static:build nothing (Storages and static export)

The contracts

Each adapter is bound under its contract's name and reached through Omnibridge\Runtime (Runtime::urls()...) or by autowiring the contract.

Contract (Omnibridge\Contract\) Symfony (Omnibridge\Symfony\) Laravel (Omnibridge\Laravel\) Answers
Urls RouterUrls RouterUrls the router's path()/url(), the asset packages' asset(); generate() for any reference type
Translations TranslatorTranslations CatalogueTranslations the translator's trans(); exists() by its transExists() or its catalogue
Environment Contract\KernelEnvironment Contract\AppEnvironment kernel.environment, kernel.debug, kernel.secret
Access Contract\SecurityAccess Contract\GateAccess the authorization checker, the token's user
Sessions Contract\SecuritySessions Contract\GuardSessions the token forgotten, the remember-me cookie cleared (its domain asked of a closure)
Tokens Contract\CsrfTokens Contract\SessionTokens the CSRF token manager's token of an intent
PSR-20 clock, PSR-14 events the framework's own services Contract\DateClock, Contract\DispatcherEvents listed when the container has them
Locales, Files, Routes not bound here (see below) Contract\AppLocales, Contract\DiskFiles, Contract\RouterRoutes

Locales, Files and Routes have no generic Symfony answer: an application's languages, storages and route names are its own. omnibase binds them to its Localizer, its Flysystem storages and its router (Base\Bridge\Symfony\*, below). Laravel's bridge binds them, and omnibase's own contracts, itself, because Laravel has none of omnibase's services.

The pages' Twig functions on the contracts: omnibridge/twig's AccessExtension (is_granted() on Access, csrf_token() on Tokens) is registered here, after twig-bridge's SecurityExtension and CsrfExtension (TwigBundle adds the bridge's extensions first), so a page asks the same code under Symfony as under Laravel (omnibridge/laravel's View\Twig), answered by the same services. Without SecurityBundle or CSRF protection the contract is missing and its function left out.

Also here: FileLinks, the links of the error pages and the profiler mapped from the container to the developer's machine (APP_PATH), Symfony's own FileLinkFormatter on a mapped format (Contracts).

The wiring

  • config/services.php (OmnibridgeBundle::SERVICES): the six adapters, each aliased by its contract's name and tagged omnibridge.adapter.
  • DependencyInjection\ContractsPass: the service locator Omnibridge\Runtime reads (omnibridge.contracts), of the contracts whose service exists; an adapter whose framework service is missing (no SecurityBundle, no CSRF protection) is removed with the aliases that lead to it, so such a container still compiles. An application may run it with its own list, former names and id.
  • OmnibridgeBundle, for a Symfony application without omnibase: imports the services, adds the pass, hands the locator to Runtime at boot and takes it back at shutdown.

An omnibase site does not register OmnibridgeBundle: omnibase's BaseBundle imports the same config/services.php (giving RouterUrls and SecuritySessions omnibase's router), runs the same pass with omnibase's own contracts added (Nouns, Media, Pictures, Texts, the parameters, the mailer) and seeds the registry itself.

What moved from omnibase, what stayed

Plan "omnibase agnostique", step A1. The rule: an adapter moved when it adapts a contract to Symfony's own services and nothing of omnibase; it stayed when it reads omnibase's own services or models. This package does not require omnisense/omnibase, so nothing is circular: the core's Symfony bundle stands on this package, never the reverse.

Moved:

Former name Now Why
Base\Bridge\Symfony\RouterUrls Omnibridge\Symfony\RouterUrls a URL generator and asset packages, any Symfony router
Base\Bridge\Symfony\TranslatorTranslations Omnibridge\Symfony\TranslatorTranslations Symfony's translator contracts; omnibase's transExists() read when present
Base\Bridge\Symfony\KernelEnvironment Omnibridge\Symfony\Contract\KernelEnvironment three kernel parameters
Base\Bridge\Symfony\SecurityAccess Omnibridge\Symfony\Contract\SecurityAccess Security's checker and token storage
Base\Bridge\Symfony\SecuritySessions Omnibridge\Symfony\Contract\SecuritySessions the token storage and the cookie; omnibase's router now a closure for the domain
Base\Bridge\Symfony\CsrfTokens Omnibridge\Symfony\Contract\CsrfTokens the CSRF token manager
Base\Bridge\Symfony\FileLinks Omnibridge\Symfony\FileLinks ErrorHandler's formatter, APP_PATH
Base\DependencyInjection\Compiler\Pass\ContractsPass (its building) Omnibridge\Symfony\DependencyInjection\ContractsPass a locator of contracts; omnibase's pass keeps its list and delegates

The former names were aliases of the new classes (declared by omnibase's src/Resources/Aliases.php, their service ids aliases of the new ones) until omnibase 4's final pass, which removed them (omnibase commit 02682623): name the classes of the "Now" column.

Stayed in the core, Base\ (omnibase's domain under Symfony, D3):

Class Why
Base\Bridge\Symfony\LocalizerLocales omnibase's Localizer (its xx-XX locales, its fallbacks)
Base\Bridge\Symfony\FlysystemFiles omnibase's Flysystem service and MediaService's image route
Base\Bridge\Symfony\RouterRoutes omnibase's router (localized route names) and IconProvider
Base\Bridge\Symfony\MediaPictures, ServiceMedia, WysiwygTexts omnibase's own contracts over its Twig extensions and MediaService
Base\Bridge\Symfony\SymfonyScreenContext, ScreenResponder, AccountScreens omnibase's screens (Base\Screen), its settings, Themizer, account builder
Base\Bridge\Symfony\NotificationMessages the Symfony half of omnibase's Notification entity
Base\Bridge\Symfony\SettingBagWarmer omnibase's SettingBag
Base\Bridge\Symfony\SecurityUser, TwoFactorUser what omnibase's User declares to Security and scheb/2fa (C7b)
Base\Service\ParameterBag, HotParameterBag omnibase's parameters, the settings stored in its database over the container's
Base\BaseBundle, its extension, configuration and compiler passes omnibase's bundle
forms, fields, controllers, subscribers Symfony by nature, omnibase's (D3)

The framework packages

composer.json requires what omnibase's Symfony side needs, so that the core does not: PHP 8.2 or later, omnisense/omnibridge and omnibridge/twig 1.x, Symfony 6, 7 or 8 (config, console, dependency-injection, doctrine-bridge, dotenv, error-handler, form, framework-bundle, http-foundation, http-kernel, messenger, routing, security-bundle, security-core, security-csrf, security-http, twig-bridge, twig-bundle), doctrine/doctrine-bundle 2 or 3, symfony/monolog-bundle 3.8 or 4, twig/extra-bundle 3. The suite runs on Symfony 8.1 and PHP 8.4.

How this manual is organised

  • Installation: with omnibase, without it, the bundle's lifecycle.
  • Contracts: each adapter, the services and their ids, ContractsPass, the Twig functions, FileLinks.
  • Signing in: Omnibridge\Symfony\Auth\ - the full omniauth configuration tree, the services, the authenticator, the second factor, the routes.
  • Maps: Omnibridge\Symfony\Map\ - the full omnimap configuration tree, the services, the Twig functions, the cache's routes and controller.
  • Storages and static export: Omnidrive\Drives, omnibridge:static:build.
  • Errors, Testing, Verified, and not.

Licence

MIT since 2026-10-09; earlier versions remain published under LGPL-3.0-or-later.

Proposer une modification docs/index.md

Installation

Requirements

  • PHP 8.2 or later.
  • Symfony 6, 7 or 8: the components listed in The framework packages, which Composer installs with the package.
  • symfony/asset (suggested): RouterUrls::asset() goes through the application's asset packages; without it, the path is answered as it is.
  • symfony/clock (6.2 or later) for a PSR-20 clock in the registry; without it Runtime::clock() is omnibridge's SystemClock.

The optional families: omnisense/omniauth (with omniauth/otp, omniauth/webauthn, omniauth/google for TOTP, passkeys and Google), omnisense/omnimap (with omnimap/google, omnimap/openstreetmap), omnisense/omnidrive, omnibridge/static. Each is wired only when its classes exist.

On an omnisense/omnibase site

composer require omnisense/omnibase omnibridge/symfony

Nothing to register: omnibase's BaseBundle imports this package's config/services.php, runs ContractsPass with omnibase's own contracts added (locator base.contracts), registers the omniauth extension (OmniauthExtension::register()) and seeds Omnibridge\Runtime itself. Do not add OmnibridgeBundle to config/bundles.php there.

In a Symfony application without omnibase

composer require omnibridge/symfony

There is no Flex recipe: add the bundle by hand.

// config/bundles.php
return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    // ...
    Omnibridge\Symfony\OmnibridgeBundle::class => ['all' => true],
];

OmnibridgeBundle (an AbstractBundle) has no configuration of its own (no omnibridge: key):

Moment What it does
loadExtension() imports config/services.php (OmnibridgeBundle::SERVICES)
build() adds ContractsPass (default list, locator omnibridge.contracts); registers the omniauth extension when Omniauth\Omniauth exists and the omnimap extension when Omnimap\Registry exists
boot() Runtime::setContainer() with the locator omnibridge.contracts
shutdown() Runtime::setContainer(null), when the registry still holds this kernel's locator

The bundle order does not matter for the contracts: the pass runs at compile time, after every bundle has loaded. For the Twig functions, TwigBundle must be registered (it is required).

A run of the package's test kernel (tests/Fixtures/TestKernel: FrameworkBundle, SecurityBundle, TwigBundle, OmnibridgeBundle), booted with and without SecurityBundle in the package's image (PHP 8.4, Symfony 8.1.9, 2026-10-27):

<?php

require 'vendor/autoload.php';

use Omnibridge\Contract\Access;
use Omnibridge\Contract\Tokens;
use Omnibridge\Runtime;
use Omnibridge\Symfony\DependencyInjection\ContractsPass;
use Omnibridge\Symfony\Tests\Fixtures\TestKernel;

foreach ([true, false] as $security) {
    $kernel = new TestKernel('test', true, $security);
    $kernel->boot();
    $locator = $kernel->getContainer()->get(ContractsPass::SERVICE);
    echo $security ? "With SecurityBundle\n" : "Without SecurityBundle\n";
    echo '  locator: ', implode(', ', array_map(static fn ($id) => substr($id, strrpos($id, '\\') + 1), array_keys($locator->getProvidedServices()))), "\n";
    echo '  path: ', Runtime::urls()->path('page', ['slug' => 'le-fuji']), "\n";
    echo '  environment: ', Runtime::environment()->name(), ', debug ', var_export(Runtime::environment()->isDebug(), true), "\n";
    echo '  Access: ', Runtime::has(Access::class) ? get_class(Runtime::access()) : 'none', "\n";
    echo '  Tokens: ', Runtime::has(Tokens::class) ? get_class(Runtime::tokens()) : 'none', "\n";
    echo '  clock: ', get_class(Runtime::clock()), "\n";
    $kernel->shutdown();
    echo '  after shutdown, container: ', null === Runtime::getContainer() ? 'none' : 'kept', "\n";
}
With SecurityBundle
  locator: Urls, Environment, Translations, Access, Sessions, Tokens, ClockInterface, EventDispatcherInterface
  path: /page/le-fuji
  environment: test, debug true
  Access: Omnibridge\Symfony\Contract\SecurityAccess
  Tokens: Omnibridge\Symfony\Contract\CsrfTokens
  clock: Symfony\Component\Clock\Clock
  after shutdown, container: none
Without SecurityBundle
  locator: Urls, Environment, Translations, ClockInterface, EventDispatcherInterface
  path: /page/le-fuji
  environment: test, debug true
  Access: none
  Tokens: none
  clock: Symfony\Component\Clock\Clock
  after shutdown, container: none

Without SecurityBundle (and with csrf_protection: false), SecurityAccess, SecuritySessions and CsrfTokens are removed at compile time: the registry has no Access, Sessions nor Tokens, and Runtime::access() would throw MissingContractException. Locales, Files and Routes are never in this locator: bind them yourself (Contracts).

In a service

Every contract is an alias of its adapter, so a service autowires it:

use Omnibridge\Contract\Urls;

final class Mailer
{
    public function __construct(private readonly Urls $urls) {}

    public function link(string $slug): string
    {
        return $this->urls->url('article', ['slug' => $slug]);
    }
}

The aliases are private, as Symfony's default: a service gets them by injection, not by $container->get(). Omnibridge\Runtime reaches them through the public locator.

Proposer une modification docs/installation.md

Contracts

The adapters of omnisense/omnibridge's contracts over Symfony's services, defined in config/services.php (OmnibridgeBundle::SERVICES). What each contract means is in the core's manual (Contracts); this page says how Symfony answers.

The services

Service id (class) Alias Arguments Tags
Omnibridge\Symfony\RouterUrls Omnibridge\Contract\Urls router, assets.packages (null when absent) omnibridge.adapter
Omnibridge\Symfony\TranslatorTranslations Omnibridge\Contract\Translations translator omnibridge.adapter
Omnibridge\Symfony\Contract\KernelEnvironment Omnibridge\Contract\Environment %kernel.environment%, %kernel.debug%, %kernel.secret% omnibridge.adapter
Omnibridge\Symfony\Contract\SecurityAccess Omnibridge\Contract\Access security.authorization_checker, security.token_storage omnibridge.adapter
Omnibridge\Symfony\Contract\SecuritySessions Omnibridge\Contract\Sessions security.token_storage omnibridge.adapter
Omnibridge\Symfony\Contract\CsrfTokens Omnibridge\Contract\Tokens security.csrf.token_manager omnibridge.adapter
Omnibridge\Twig\AccessExtension - Omnibridge\Contract\Access, Omnibridge\Contract\Tokens (each null when absent) twig.extension
Omnidrive\Flysystem\OperatorDrives Omnidrive\Drives (public) a locator of the services tagged flysystem.storage, by their storage attribute - (only when omnidrive is installed)
Omnibridge\Symfony\Static\BuildCommand - kernel, router console.command (only when omnibridge/static is installed)
omnibridge.contracts (ServiceLocator, public) - the contracts found, by interface name container.service_locator (made by ContractsPass)

An application rebinds a contract by aliasing its interface again (services.yaml: Omnibridge\Contract\Urls: '@App\MyUrls'): the locator takes whatever service the name leads to.

The adapters

RouterUrls

path() is the router's generate() with ABSOLUTE_PATH, url() with ABSOLUTE_URL - the same strings Twig's path() and url() print. asset() is Packages::getUrl(), the path unchanged when symfony/asset is not installed. Beyond the contract, generate(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH) gives any of the router's reference types (a relative path, a network path): omnibase's Base\Contract\Link::to() asks it. An unknown route is the router's RouteNotFoundException. On an omnibase site the router argument is omnibase's advanced_router.

TranslatorTranslations

trans() is the translator's trans() with the same four arguments - what Twig's |trans answers. exists():

  1. the translator's own transExists($id, $domain, $locale) when it has one (omnibase's translator: the locale's catalogue and its fallbacks);
  2. otherwise, when the translator is a TranslatorBagInterface (Symfony's is), its catalogue of the locale: getCatalogue($locale)->has($id, $domain ?? 'messages'), which includes the fallback catalogues. An id written @domain.key is read as the key key of the domain domain;
  3. otherwise false.

KernelEnvironment

The three kernel parameters, read once at compile time. secret() is kernel.secret (framework.secret), marked #[\SensitiveParameter] in the constructor so a stack trace does not print it.

SecurityAccess

isGranted() is the authorization checker's (the role hierarchy, the application's voters, the access decision strategy); getUser() the token storage's token's user, null without a token.

SecuritySessions

signOut($user) answers false when no token is stored or when its user is not the very object given (!==). Otherwise it empties the token storage and expires the REMEMBERME cookie twice with PHP's setcookie(): once with no path nor domain, once on / and the domain the constructor's closure answers (omnibase gives its router's domain; empty without a closure). The cookies are sent with the response's headers, outside Symfony's Response.

CsrfTokens

csrf($intent) is the token manager's getToken($intent)->getValue() - what Twig's csrf_token() prints; isValid($intent, $token) is isTokenValid(new CsrfToken($intent, $token)), false for a null or empty token without asking.

ContractsPass

Omnibridge\Symfony\DependencyInjection\ContractsPass builds the locator the registry reads. Not final: an application's bundle runs it with its own list.

new ContractsPass(
    contracts: ContractsPass::CONTRACTS,   // the names the locator answers
    services: [],                          // a contract => the service id it is bound to, when not its own name
    former: [],                            // a former name => the contract it is listed as (the same service)
    id: ContractsPass::SERVICE,            // the locator's id, 'omnibridge.contracts'
);
Constant Value
SERVICE omnibridge.contracts
ADAPTER omnibridge.adapter
CONTRACTS Urls, Routes, Environment, Translations, Access, Sessions, Locales, Files, Tokens (all Omnibridge\Contract\), Psr\Clock\ClockInterface, Psr\EventDispatcher\EventDispatcherInterface

What process() does:

  1. Each service tagged omnibridge.adapter whose constructor needs a service that does not exist (a Reference that must be valid, in its arguments or in an inline definition among them) is removed, with every alias that leads to it, through other aliases too. So SecurityAccess goes without SecurityBundle, CsrfTokens without framework.csrf_protection.
  2. Each contract of the list whose id (its own name, or the one services gives) exists in the container is put in the locator. Routes, Locales and Files are in the list but bound by nobody here: they appear when the application (or omnibase) binds them. The clock appears when symfony/clock aliases Psr\Clock\ClockInterface (FrameworkBundle does when the component is installed); the dispatcher when Psr\EventDispatcher\EventDispatcherInterface is aliased (FrameworkBundle does).
  3. Each former name is added to the locator with its contract's service.
  4. The locator is registered public, tagged container.service_locator: nothing is constructed before a contract is asked.

omnibase's BaseBundle runs it with omnibase's list (its Nouns, Media, Pictures, Texts, the parameters, the mailer added) and the id base.contracts.

Binding what is not bound here

Locales, Files and Routes depend on how an application names its languages, its storages and its routes. Without omnibase, bind them yourself; the locator picks them up by their names:

// src/Bridge/RequestLocales.php
namespace App\Bridge;

use Omnibridge\Contract\Locales;
use Symfony\Component\HttpFoundation\RequestStack;

final class RequestLocales implements Locales
{
    public function __construct(private readonly RequestStack $requests) {}

    public function getLocale(): string
    {
        return str_replace('_', '-', $this->requests->getCurrentRequest()?->getLocale() ?? 'fr-FR');
    }

    public function getDefaultLocale(): string { return 'fr-FR'; }

    public function getAvailableLocales(): array { return ['fr-FR', 'en-GB']; }
}
# config/services.yaml
services:
    App\Bridge\RequestLocales: ~
    Omnibridge\Contract\Locales: '@App\Bridge\RequestLocales'

The Twig functions

Omnibridge\Twig\AccessExtension (omnibridge/twig) is registered with the contracts it finds:

Function Answered by Left out when
is_granted(attribute, subject = null) Access::isGranted() no Access (no SecurityBundle)
csrf_token(intent) Tokens::csrf() no Tokens (no CSRF protection)

TwigBundle registers twig-bridge's extensions first, so these two functions replace twig-bridge's is_granted and csrf_token of the same names, answered by the same Symfony services: a template behaves as it did, and asks the same code it asks under Laravel. path(), url(), asset() and |trans stay twig-bridge's: omnibridge/twig's ContractExtension is not registered under Symfony. The map functions are registered with omnimap (Maps).

Omnibridge\Symfony\FileLinks makes the links of the error pages and the profiler open the file on the developer's machine when the site runs in a container: APP_PATH (the project's path on the host, which a site's Makefile writes in .env.local) against kernel.project_dir (its path in the container), with Symfony's own means - FileLinkFormatter reads a mapping after the format, vscode://file/%f:%l&/srv/app/>/Users/me/site/.

Static method What it does
format(?string $ide, string $projectDir, ?string $hostDir): ?string the link format with the container-to-host mapping appended; $ide unchanged (null for none) when there is nothing to map (no host path, the same path, or a format that already maps). A name of ErrorRendererInterface::IDE_LINK_FORMATS (vscode, phpstorm...) is expanded first; no format at all is FileLinks::DEFAULT_FORMAT, vscode://file/%f:%l. The former syntax vscode://file/%f[%b:/host/path]:%l is read as the mapping
formatter(?FileLinkFormatter $inner, ?string $ide, string $projectDir, ?string $hostDir, ?RequestStack $requestStack = null): ?FileLinkFormatter the factory of a decoration of debug.file_link_formatter: a FileLinkFormatter on the mapped format (with the profiler's /_profiler/open?file=%f&line=%l#line%l for a file without a format), or $inner when nothing maps
export(?string $projectDir = null): void sets $_SERVER['SYMFONY_IDE'] and $_ENV['SYMFONY_IDE'] to the mapped format when APP_PATH is set and SYMFONY_IDE is not; the project directory defaults to Composer's root package's install path

FrameworkBundle's framework.ide defaults, in debug, to %env(default::SYMFONY_IDE)%: export() called before the kernel boots maps every link Symfony makes, including ErrorHandler's renderer for an error before the container is built. omnibase's BaseBundle calls FileLinks::export() in its constructor. OmnibridgeBundle calls neither method and registers no decoration: without omnibase, call export() early (in public/index.php and bin/console, after the autoloader), or decorate the formatter:

# config/services.yaml (dev)
services:
    app.file_link_formatter:
        class: Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter
        decorates: debug.file_link_formatter
        factory: ['Omnibridge\Symfony\FileLinks', 'formatter']
        arguments: ['@.inner', '%env(default::SYMFONY_IDE)%', '%kernel.project_dir%', '%env(default::APP_PATH)%', '@request_stack']

Run in the package's image (a dev kernel with FrameworkBundle, TwigBundle and OmnibridgeBundle, kernel.project_dir /srv/app, APP_PATH=/Users/me/site, 2026-10-27), the decorated formatter and format():

$kernel->getContainer()->get('app.file_link_formatter')->format('/srv/app/src/Kernel.php', 12);
FileLinks::format(null, '/srv/app', '/Users/me/site');
FileLinks::format('phpstorm', '/srv/app', '/Users/me/site');
FileLinks::format('vscode://file/%f[%b:/Users/me/other]:%l', '/srv/app', null);
vscode://file//Users/me/site/src/Kernel.php:12
vscode://file/%f:%l&/srv/app/>/Users/me/site/
phpstorm://open?file=%f&line=%l&/srv/app/>/Users/me/site/
vscode://file/%f:%l&/srv/app/>/Users/me/other/

Proposer une modification docs/contracts.md

Signing in (omniauth)

Where omnisense/omniauth is installed (it is only suggested), OmnibridgeBundle registers omniauth's extension (Auth\DependencyInjection\OmniauthExtension; an omnibase site's BaseBundle calls OmniauthExtension::register() too). With no config/packages/omniauth.yaml, nothing is wired. With one, every way of signing in goes through omniauth's conductor and one Symfony authenticator.

This page is the reference of the Symfony side: the configuration tree, the services, the routes, what the authenticator does with each request. What each method does (a password, a sign-in link, an e-mailed code, backup codes, TOTP, passkeys, identity providers), its stores and events are omniauth's own manual: omniauth's Symfony page and its method pages.

Setting it up

# config/packages/security.yaml
security:
    firewalls:
        main:
            custom_authenticators: [Omnibridge\Symfony\Auth\OmniauthAuthenticator]
            entry_point: Omnibridge\Symfony\Auth\OmniauthAuthenticator   # optional: start() sends to login_route
            remember_me: { secret: '%kernel.secret%' }                   # Security's own, used when omniauth says "remember"
            logout: { path: security_logout }
    access_control:
        - { path: ^/login/2fa, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS }

# config/routes/omniauth.yaml
omniauth: { resource: ., type: omniauth }

# config/packages/omniauth.yaml - each key turns its method on
omniauth:
    password: { routes: [security_login] }
    link: { route: security_loginWithToken }      # 15 minutes, once, hashed
    email_code: ~
    backup_codes: ~
    trusted_device: ~                              # scheb/2fa's cookie and tokens
    totp: { secret_key: '%env(OMNIAUTH_TOTP_KEY)%' }   # omniauth/otp; the secrets sealed at rest (sodium)
    passkey: { rp_id: '%env(RELYING_PARTY_ID)%' }  # omniauth/webauthn
    oauth: { providers: { google: { client_id: '...', client_secret: '...' } } }   # omniauth/google (the mechanics: omniauth's Omniauth\OAuth\)

totp.secret_key (base64 of 32 bytes, from the vault or the environment, never in the repository; the same on every host of one database - the Laravel twin's config/omniauth.php reads the same variable) seals the TOTP secrets at rest: omniauth/otp's SealedTotpSecrets over a SodiumSecretBox decorates the totp_secrets store, so the method and any page that reads or writes the store (omnibase's enrolment and its QR code) go through it. Unset, the secrets are kept as the store has them. A secret kept in clear before is read as it is until the application rewrites its rows (omnibase sites: a migration).

A provider's return address is, unless it sets one, the path of its check route: omniauth puts it on the host of each trip's request (the providers are built once per worker; an absolute address made here would stay on the first request's host). Each provider package's factory (Omniauth\OAuth\Provider\ProviderFactory, tag omniauth.oauth_provider_factory) makes its own kind; omniauth/google's is registered when it is installed.

The stores (Omniauth\Contract\Accounts... the passkeys, the sender) are the application's, by their contracts' names or those omniauth.yaml's stores names; omniauth's events go to Symfony's dispatcher (listen to Omniauth\Event\SignedIn...). The accounts must be Symfony users (UserInterface): the authenticator refuses any other with a LogicException.

The configuration tree

Dumped with Symfony's reference dumper (bin/console config:dump-reference omniauth) on a kernel of FrameworkBundle, SecurityBundle, TwigBundle and OmnibridgeBundle, in the package's image (PHP 8.4, Symfony 8.1.9, 2026-10-27):

# Default configuration for extension with alias: "omniauth"
omniauth:

    # Keys the e-mailed codes' hashes and the trusted devices' tokens (scheb/2fa signed them with kernel.secret)
    secret:               '%kernel.secret%'

    # The sign-in page: where a refused or abandoned attempt goes back to
    login_route:          security_login
    stores:
        accounts:             Omniauth\Contract\Accounts
        passwords:            Omniauth\Contract\Passwords
        link_tokens:          Omniauth\Contract\LinkTokens
        email_codes:          Omniauth\Contract\EmailCodes
        backup_codes:         Omniauth\Contract\BackupCodes
        trusted_version:      Omniauth\Contract\TrustedVersion
        totp_secrets:         Omniauth\Contract\TotpSecrets
        identities:           Omniauth\Contract\Identities
        passkeys:             Omniauth\WebAuthn\Contract\Passkeys
        sender:               Omniauth\Contract\Sender
    password:
        enabled:              true

        # The routes a sign-in form posts to
        routes:

            # Default:
            - security_login

        # The form's name, its fields read under it; null: at the top of the request
        form:                 null
        identifier:           _username
        password:             _password
        remember:             _remember_me
        target_path_parameter: _target_path
        default_target_path:  /

        # The class whose hasher security.yaml sets (password_hashers): the same hasher checks the passwords
        user_class:           null
        csrf:
            enabled:              true
            field:                _csrf_token
            intent:               authenticate
    link:
        enabled:              false
        lifetime:             900

        # The route of the link's page, which signs in when it carries a token
        route:                null
        parameter:            token
    email_code:
        enabled:              false
        digits:               6
        lifetime:             600
    backup_codes:
        enabled:              false
        count:                8
    trusted_device:
        enabled:              false
        lifetime:             2592000
        cookie:               trusted_device

        # The tokens' "fwl" claim; null: the firewall of the request
        firewall:             null
    totp:
        enabled:              false
        leeway:               0
        period:               30
        digits:               6
        issuer:               null
        server_name:          null

        # The key the secrets are sealed with at rest (omniauth/otp's SodiumSecretBox): base64 of 32 bytes, from the vault or the environment - '%env(OMNIAUTH_TOTP_KEY)%' -, never written in the repository; the same on every host of the database. Unset: the secrets are kept as the store has them
        secret_key:           null
    passkey:
        enabled:              false
        rp_id:                null
        rp_name:              null
        origins:              []
        timeout:              null

        # Security's remember-me after a passkey (the JSON posted may say otherwise: "remember")
        remember:             true
        options_path:         /login/passkey/options
        options_route:        passkey_login_options
        path:                 /login/passkey
        route:                passkey_login
    oauth:
        enabled:              false

        # Link an account by an address its provider vouches for: true, false, or a service (omniauth's EmailLinking); null: never one with a second factor
        link_by_email:        null

        # Where an identity no account is linked to goes (its PendingIdentity in the session); null: the sign-in page
        sign_up_route:        null
        providers:

            # Prototype
            name:
                client_id:            ''
                client_secret:        ''

                # An absolute address, or a path each trip's host completes; null: the path of check_route
                redirect_uri:         null
                scopes:               []
                hosted_domain:        null
                type:                 null

                # A service that is the provider (omniauth's Provider), in place of the settings above
                service:              null
                start_path:           null
                start_route:          null
                check_path:           null
                check_route:          null
    second_factor:
        after:

            # Defaults:
            - password
            - link
            - oauth
        form_path:            /login/2fa
        form_route:           2fa_login
        check_path:           /login/2fa/check
        check_route:          2fa_login_check
        template:             security/login_2fa.html.twig
        code:                 _auth_code
        trusted:              _trusted
        csrf:
            enabled:              true
            field:                _csrf_token
            intent:               two_factor
    throttle:
        enabled:              true

        # A PSR-6 pool (Symfony's cache pools are): the tries counted, and the TOTP steps used
        pool:                 cache.app

        # Tries per window in seconds, by scope (omniauth's CacheThrottle::LIMITS by default): { password: [5, 60] }
        limits:

            # Prototype
            scope:                []

    # The message of a failure, or of "failure:reason" ("%reason%" replaced), as the page translates it
    messages:

        # Prototype
        failure:              ~

A key written, even empty (email_code: ~), turns its method on (canBeEnabled()); password and the two csrf blocks and throttle are on unless enabled: false (canBeDisabled()).

Top level

Key Type Default Effect
secret string %kernel.secret% keys the e-mailed codes' hashes (EmailCodeMethod) and the trusted devices' tokens (TrustedDevices)
login_route string security_login where start(), an abandoned or expired second factor, and a refused non-password attempt go back to
stores.<name> service id each contract's own interface name the store each method reads: accounts (Omniauth\Contract\Accounts), passwords, link_tokens, email_codes, backup_codes, trusted_version, totp_secrets, identities, passkeys (Omniauth\WebAuthn\Contract\Passkeys), sender
messages.<failure> string - the message of a failure, or of failure:reason (%reason% replaced), over the defaults below

Methods

Key Options (default) Service Needs
password (on) routes ([security_login]): the routes a sign-in form posts to; form (null): the form's name, its fields read under it, null at the top of the request; identifier (_username), password (_password), remember (_remember_me), target_path_parameter (_target_path), default_target_path (/); user_class (null): the class whose hasher security.yaml's password_hashers sets, so the same hasher checks the passwords; csrf.enabled (true), csrf.field (_csrf_token), csrf.intent (authenticate) omniauth.method.password (PasswordMethod: accounts, passwords, the hasher or null) -
link lifetime (900, min 60), route (null): the route of the link's page, which signs in when it carries a token; parameter (token) omniauth.method.link (LinkMethod: accounts, link_tokens, sender, lifetime, clock) the route is the application's own; without route the method is mounted but no request is read as a link
email_code digits (6, 4 to 9), lifetime (600, min 60) omniauth.method.email_code (a second factor) -
backup_codes count (8, min 1) omniauth.method.backup_codes (a second factor) -
trusted_device lifetime (2592000, 30 days, min 60), cookie (trusted_device), firewall (null: the request's firewall; the tokens' fwl claim) omniauth.method.trusted_device (TrustedDevices) -
totp leeway (0), period (30), digits (6, 6 to 10), issuer (null), server_name (null), secret_key (null) omniauth.method.totp (a second factor; used steps kept in throttle.pool); with secret_key, omniauth.totp.box (lazy SodiumSecretBox) and omniauth.totp.sealed_secrets decorating the totp_secrets store omniauth/otp
passkey rp_id, rp_name (null), origins ([]), timeout (null), remember (true: Security's remember-me after a passkey, unless the posted JSON says "remember"), options_path (/login/passkey/options), options_route (passkey_login_options), path (/login/passkey), route (passkey_login) omniauth.method.passkey (PasskeyMethod with a RelyingParty) omniauth/webauthn
oauth link_by_email (null: NoSecondFactorLinking, an account linked by its address only when it has no second factor; true or false; or a service id implementing omniauth's EmailLinking); sign_up_route (null: the sign-in page); providers.<name> omniauth.method.oauth (OAuthMethod), omniauth.oauth.providers (public) omnisense/omniauth with Omniauth\OAuth\; a provider package

A method whose package is missing stops the compilation: omniauth.yaml turns "totp" on, which needs omniauth/otp: composer require omniauth/otp. (the same for passkey, oauth).

oauth.providers.<name>

Key Default Effect
client_id, client_secret '' a provider without client id (and without service) is not served
redirect_uri null an absolute address, or a path each trip's host completes; null: the path of check_route
scopes [] handed to the factory
hosted_domain null handed to the factory (Google's hd)
type null which factory makes it (google); the factory's own default otherwise
service null a service that is the provider (omniauth's Provider), in place of the settings above; another class is an InvalidArgumentException when the providers are built
start_path, start_route /connect/<name>, oauth_<name> the trip out
check_path, check_route /connect/<name>/check, oauth_<name>_check the return

Other keys are kept (ignoreExtraKeys(false)) and handed to the factory.

second_factor

Key Default Effect
after [password, link, oauth] (omniauth's EnrolledSecondFactors::AFTER) the first factors a second one follows, for an account that has one enrolled
form_path, form_route /login/2fa, 2fa_login the second factor's page
check_path, check_route /login/2fa/check, 2fa_login_check where its form posts
template security/login_2fa.html.twig the page's template, the application's
code _auth_code the code's field
trusted _trusted the "trust this device" field
csrf.enabled, csrf.field, csrf.intent true, _csrf_token, two_factor the form's token, checked through the Tokens contract

throttle

Key Default Effect
enabled true false: no Throttle given to the conductor
pool cache.app a PSR-6 pool: the tries counted, and the TOTP steps used
limits.<scope> omniauth's CacheThrottle::LIMITS: password 5 per 60 s, link.begin 3 per 3600 s, email-code 5 per 900 s, email-code.begin 3 per 900 s, backup-code 5 per 900 s, totp 5 per 900 s, any other (*) 5 per 60 s [tries, seconds], replacing the default scope by scope; a missing number is 5 or 60

The services

Id Class What it is
Omniauth\Omniauth (public) Omniauth the conductor: accounts, the methods, the state, the policy, the throttle (or null), event_dispatcher, clock (null when absent)
omniauth.methods (alias Omniauth\Method\Methods) Methods every service tagged omniauth.method
omniauth.second_factors Methods the services tagged omniauth.second_factor (email code, backup codes, TOTP)
omniauth.policy.default (alias Omniauth\Contract\SecondFactorPolicy) EnrolledSecondFactors the second-factor rule; an application decorates the alias
Omnibridge\Symfony\Auth\SessionState (alias Omniauth\Contract\State) the attempt in the visitor's session (through the request stack); nothing kept without a session
Omniauth\CacheThrottle (alias Omniauth\Contract\Throttle) over throttle.pool, when throttle.enabled
Omnibridge\Symfony\Auth\Contexts omniauth's Context of a request: origin, host, client IP, user agent, the trusted_device cookie, the firewall (trusted_device.firewall, else the request's firewall, else main)
Omnibridge\Symfony\Auth\OmniauthAuthenticator the authenticator the firewall names
Omnibridge\Symfony\Auth\PendingVoter tag security.voter
Omnibridge\Symfony\Auth\PendingTrustResolver decorates security.authentication.trust_resolver (ignored when absent)
Omnibridge\Symfony\Auth\SecondFactorGate kernel.event_subscriber
Omnibridge\Symfony\Auth\CeremonyController (public) tag controller.service_arguments
Omnibridge\Symfony\Auth\AuthRouteLoader tag routing.loader, type omniauth
omniauth.oauth.providers (public) Omniauth\OAuth\Provider\Providers OAuthProviders::create(): the configured providers through the factories, the service ones as they are
omniauth.oauth.factory.google Omniauth\Google\GoogleFactory when omniauth/google is installed; any class implementing ProviderFactory is autoconfigured with the tag omniauth.oauth_provider_factory
omniauth.method.<name> the method classes one per method turned on, tagged omniauth.method (and omniauth.second_factor for the second factors)

The routes

omniauth: { resource: ., type: omniauth } loads (AuthRouteLoader):

Name (default) Path (default) Methods Controller
2fa_login /login/2fa GET CeremonyController::secondFactor
2fa_login_check /login/2fa/check any CeremonyController::check (the authenticator answers the POST first)
passkey_login_options /login/passkey/options POST CeremonyController::passkeyOptions (with passkey)
passkey_login /login/passkey POST CeremonyController::check (the authenticator answers first)
oauth_<name> /connect/<name> GET CeremonyController::oauthStart (provider = name), per provider
oauth_<name>_check /connect/<name>/check GET CeremonyController::check (the authenticator answers first), per provider

The names and addresses are those scheb/2fa, web-auth/webauthn-symfony-bundle and hwi/oauth-bundle had, so the pages and scripts that name them do not change. The password form's route and the link's route are the application's.

The authenticator

OmniauthAuthenticator (AbstractAuthenticator, AuthenticationEntryPointInterface, not final) knows a sign-in request by its route (the router has run before the firewall):

Request Factor
POST to a route of password.routes password (identifier, password, remember read under form or at the top; the CSRF field checked)
GET of link.route with a non-empty link.parameter attribute link
POST to passkey.route passkey (the JSON body; remember from the JSON, else passkey.remember)
POST to second_factor.check_route the factor chosen on the page (else the first offered that is not a backup code, else TOTP); a code not shaped as that factor's digits is read as a backup code when backup codes are offered
a provider's check_route oauth (code, state, error from the query)

A method not turned on is never supported. Then Omniauth::verify(), and:

omniauth's answer Token Response
authenticated PostAuthenticationToken with the account's roles; RememberMeBadge when omniauth says remember; every passport carries an OmniauthBadge redirect to a posted or queried _target_path of this site (a path, or this host over http/https), else the firewall's saved target, else default_target_path; the trusted_device cookie set when omniauth gave one (HTTP only, SameSite Lax, secure on HTTPS, trusted_device.lifetime); a passkey's {"status": "ok", "errorMessage": ""}
second factor required PendingToken (the offered factors) redirect to second_factor.form_route; a passkey's JSON with redirect
refused SignInRefused password: back to the form's route, _security.last_error and _security.last_username in the session; link: no response, the route's token parameter emptied so the page answers as for no link; passkey: {"status": "failed", "errorMessage": ...}, 401; second factor: back to its page with the error, or to login_route when the attempt expired or is gone (the attempt abandoned, the token removed); other: login_route
identity unknown (oauth) SignInRefused with the identity the identity kept in the session (omnibridge.auth.pending_identity: provider, subject, email, emailVerified, name) and a redirect to oauth.sign_up_route, else login_route

start() (the entry point) redirects to login_route.

The messages (OmniauthAuthenticator::MESSAGES, overridden by messages), Symfony's security catalogue's keys so a page translates them as it did:

Failure Message
bad-credentials, invalid-link, refused, not-enrolled Invalid credentials.
bad-code, not-offered code_invalid
expired, no-attempt Authentication request could not be processed due to a system problem.
throttled Too many failed login attempts, please try again in %minutes% minute. (%minutes% given as 1)
account-refused Account is disabled.
csrf Invalid CSRF token.

A key failure:reason (refused:locked) wins over failure; %reason% in a message is replaced by the reason. An unknown failure falls back on bad-credentials's message.

The session keys: omnibridge.auth.factor (OmniauthAuthenticator::CHOSEN, the second factor chosen), omnibridge.auth.second_factor_error (SECOND_FACTOR_ERROR), omnibridge.auth.pending_identity (PENDING_IDENTITY), and Security's _security.last_username, _security.last_error.

Between two factors

  • PendingToken: a TokenInterface (not an AbstractToken, as scheb/2fa's TwoFactorToken), no role, the offered factors (offered()), serialised in the session like any token.
  • PendingVoter: grants IS_AUTHENTICATED_2FA_IN_PROGRESS to a PendingToken and to nobody else (it abstains otherwise), so an access_control line written for scheb/2fa still opens the page.
  • PendingTrustResolver: isFullFledged() is false for a PendingToken, so IS_AUTHENTICATED_FULLY is refused to it.
  • SecondFactorGate: on kernel.request (priority 7, after the firewall) and kernel.exception (priority 2, before the firewall's exception listener, for an AccessDeniedException anywhere in the chain), a visitor holding a PendingToken is redirected to second_factor.form_route - except on the second factor's own routes, a page access_control opens to PUBLIC_ACCESS or whose attributes the token is granted, and the firewall's sign-out path.

The second factor's page

CeremonyController::secondFactor() renders second_factor.template with the variables scheb/2fa's form controller gave, so a template written for scheb/2fa renders unchanged:

Variable Value
twoFactorProvider the factor shown, under scheb's names: totp, email (omniauth's email-code), backup-code
availableTwoFactorProviders the offered factors but backup codes, under the same names
authenticationError, authenticationErrorData the last refusal's message key and data, once
displayTrustedOption whether an ambient method (trusted devices) is mounted
authCodeParameterName, trustedParameterName second_factor.code, second_factor.trusted
isCsrfProtectionEnabled, csrfParameterName, csrfTokenId from second_factor.csrf
checkPathRoute, checkPathUrl the check route and its path
logoutPath the firewall's sign-out path, null without one

?preferProvider=email (or any offered factor) chooses the factor. The e-mailed code is sent when its page is first shown, and again once it expired. Without a PendingToken or an attempt, the page abandons the attempt and redirects to login_route. The answer is Cache-Control: no-store. Without TwigBundle it throws LogicException ("The second factor's page is a Twig template: install symfony/twig-bundle.").

passkeyOptions() answers the passkey's request options as web-auth/webauthn-symfony-bundle did ({"status": "ok", "errorMessage": "", ...options}, or failed and 400); oauthStart() redirects to the provider's authorization address (404 when no provider is served or the name is unknown, back to login_route on another failure); check() sends a stray GET of a check address back to its page.

Proposer une modification docs/omniauth.md

Maps (omnimap)

Where omnisense/omnimap is installed (suggested), OmnibridgeBundle registers omnimap's extension (Map\DependencyInjection\OmnimapExtension, alias omnimap), and config/packages/omnimap.yaml wires it: its providers (omnimap/google, omnimap/openstreetmap, registered when installed) over the application's http_client, a PSR-16 view of a cache pool and a PSR-18 client for geocoder-php; the map cache over a storage of the application's (an omnidrive drive, or any Omnimap\Snapshot\Storage); omnibridge/twig's MapExtension (omnimap(view), omnimap_export(), omnimap_suppress()); the cache's routes and their controller. With no omnimap.yaml, nothing is wired.

What a map, a view, a snapshot and a provider are is omnimap's own manual (omnimap's Symfony page, its cache); this page is the reference of the Symfony side.

Setting it up

# config/routes/omnimap.yaml
omnimap: { resource: ., type: omnimap }

# config/packages/omnimap.yaml
omnimap:
    provider: openstreetmap            # the page's map: a key of providers
    secret: '%env(MAP_SECRET)%'        # signs the pictures' names
    providers:
        openstreetmap: { user_agent: 'MySite/1.0 (+https://example.org)' }
        google: { api_key: '%env(GOOGLE_MAPS_CLIENT_KEY)%', secret: '%env(GOOGLE_MAPS_SECRET)%' }
    cache:
        drive: local.uploads           # an omnidrive drive (Omnidrive\Drives); or storage: an Omnimap\Snapshot\Storage service
        public_directory: '%kernel.project_dir%/public'   # tiles served are written there too
    routes:
        tile: { path: '/omnimap/{signature}/{id}.webp', name: omnimap_tile }

cache.drive needs omnisense/omnidrive (its Omnidrive\Drives is bound by this package, Storages).

The configuration tree

Dumped with bin/console config:dump-reference omnimap on the same kernel as omniauth's (PHP 8.4, Symfony 8.1.9, 2026-10-27):

# Default configuration for extension with alias: "omnimap"
omnimap:

    # The provider of the page's map: a key of "providers".
    provider:             ~ # Required

    # The provider that geocodes (Omnimap\GeocoderInterface): a key of "providers"; the page's provider by default.
    geocoder:             null

    # Signs the pictures' names (URL-safe base64; Google's URL signing secret fits).
    secret:               ''
    providers:

        # Prototype
        name:                 ~

    # A visitor never gets the live map: the picture kept, or an empty frame.
    cache_only:           true

    # Who may capture and delete pictures, and sees the live map while there is none.
    control:              ROLE_ADMIN
    cache:

        # The omnidrive drive the cache is kept on (Omnidrive\Drives: a storage's name, "local.uploads"); or storage.
        drive:                null

        # A service implementing Omnimap\Snapshot\Storage, when the cache is on no omnidrive drive.
        storage:              null
        directory:            omnimap
        pattern:              '{signature}/image-{id}'
        format:               png # One of "png"; "jpeg"; "jpg"; "webp"
        tilesize:             512

        # Whether the server makes a missing picture itself: auto (when the provider makes images), true, false.
        make:                 auto
        size:                 1512x938
        scale:                1

        # The PSR-6 pool keeping fetched tiles and geocoders' answers.
        pool:                 cache.app

        # The HTTP lifetime of a tile served by the route.
        max_age:              86400

        # Where a tile served is also written (the web server then serves it without PHP): the public folder.
        public_directory:     null

        # The largest side of a tile served, in pixels (none: as kept).
        public_size:          null
    html:
        class:                omnimap-snapshot
        suffix:               _snapshot
        missing:              ''
    routes:
        tile:
            path:                 '/omnimap/{signature}/{id}.webp'
            name:                 omnimap_tile
        metadata:
            path:                 '/omnimap/{signature}'
            name:                 omnimap_metadata
        export:
            path:                 '/omnimap/{signature}/export'
            name:                 omnimap_export
        suppress:
            path:                 '/omnimap/{signature}/suppress'
            name:                 omnimap_suppress

Key Type Default Effect
provider string, required, not empty - the provider of the page's map: a key of providers (Omnimap\MapRendererInterface)
geocoder string null (the page's provider) the provider that geocodes (Omnimap\GeocoderInterface)
secret string '' signs the pictures' names (URL-safe base64; Google's URL signing secret fits)
providers.<name> any (a map) - the provider's options, its factory's (omnimap/google, omnimap/openstreetmap); factory names another factory than the key
cache_only bool true a visitor never gets the live map: the picture kept, or an empty frame
control string ROLE_ADMIN the attribute (asked of the Access contract) that may capture and delete pictures, and sees the live map while there is none
cache.drive string null the omnidrive drive the cache is kept on (Omnidrive\Drives: a storage's name)
cache.storage service id null a service implementing Omnimap\Snapshot\Storage, in place of a drive; one of drive or storage is required ("omnimap.cache needs a drive ... or a storage ...")
cache.directory string omnimap the folder of the pictures in the storage
cache.pattern string {signature}/image-{id} a picture's file name
cache.format png, jpeg, jpg, webp png how pictures are kept
cache.tilesize int 512 the side of the tiles a picture is cut into; 0 or less: not cut
cache.make auto, true, false auto whether the server makes a missing picture itself: auto when the page's provider can make static maps, true always (another static provider of the registry when the page's cannot), false never
cache.size WxH 1512x938 the size of a picture the server makes
cache.scale int, 1 or 2 1 its scale
cache.pool service id cache.app the PSR-6 pool keeping fetched tiles and geocoders' answers (omnimap.cache, a Psr16Cache over it)
cache.max_age int 86400 the HTTP lifetime of a tile served by the route
cache.public_directory string null where a tile served is also written, at the route's own path, so the web server serves it without PHP the next time
cache.public_size int null the largest side of a tile converted to WebP
html.class, html.suffix, html.missing string omnimap-snapshot, _snapshot, '' omnimap's MapHtml (the picture's markup); src is the tile route's path
routes.<tile, metadata, export, suppress>.path, .name string below the cache's routes

The services

Id Class What it is
Omnimap\Registry (public) the providers: the factories tagged omnimap.provider_factory, the providers options by name
Omnimap\Google\GoogleFactory, Omnimap\OpenStreetMap\OpenStreetMapFactory, Omnimap\MapLibre\MapLibreFactory registered when their package is installed (OmnimapExtension::FACTORIES), Google's and OpenStreetMap's over http_client, omnimap.psr18 (or null), omnimap.cache, MapLibre's over http_client; tag omnimap.provider_factory
Omnimap\MapRendererInterface Registry::renderer(provider)
Omnimap\GeocoderInterface Registry::geocoder(geocoder ?? provider)
omnimap.cache Symfony\Component\Cache\Psr16Cache over cache.pool
omnimap.psr18 Symfony\Component\HttpClient\Psr18Client over http_client, when nyholm/psr7 or php-http/discovery is installed (geocoder-php's needs)
Omnimap\Snapshot\SnapshotStore over cache.storage, or a DriveStorage over Drives::get(cache.drive)
Omnimap\Snapshot\Snapshots (public) the renderer, a Signer of secret, the store, the static map (per cache.make), the size, the scale, the tile size
Omnimap\MapHtml the html options
Omnimap\MapPrinter (public) the page's map by omnimap's rule; a picture the server could not make is logged as a warning (omnimap: the map's picture could not be made: ...) on logger
Omnibridge\Twig\MapExtension when omnibridge/twig's class exists and TwigBundle is registered; tag twig.extension
Omnibridge\Symfony\Map\SnapshotController (public) tag controller.service_arguments
Omnibridge\Symfony\Map\MapRouteLoader tag routing.loader, type omnimap

A provider of another package is wired by its factory, tagged as the registry collects them:

# config/services.yaml - another provider's factory
services:
    App\Map\AcmeFactory:
        arguments: ['@http_client']
        tags: [omnimap.provider_factory]

The Twig functions

Omnibridge\Twig\MapExtension (omnibridge/twig), given the MapPrinter, the Urls, Access and Tokens contracts, control and the routes' names:

Function Output
omnimap(view) the picture kept, or the live map, by MapPrinter's rule (HTML); '' for a null view
omnimap_export(view, attributes = {}) the button that sends the live map's picture to the cache, for whoever Access grants control; posts to the export route with a token of intent omnimap-export; attributes.text is its content (default O)
omnimap_suppress(view, attributes = {}) the button that deletes the picture kept, for the same; intent omnimap-suppress; default text X
omnimap_signature(view) the view's signature (the picture's name)
{{ omnimap(view) }}
{{ omnimap_export(view, {class: 'item', text: 'Capture'}) }}
{{ omnimap_suppress(view, {text: 'Delete'}) }}

The routes and the controller

omnimap: { resource: ., type: omnimap } loads (MapRouteLoader, the tile first because its path may lie under the metadata's), signature matching [A-Za-z0-9_-]{1,128}:

Name Path Methods Action
omnimap_tile /omnimap/{signature}/{id}.webp (id: digits, default 0) GET, HEAD a tile of a picture kept
omnimap_export /omnimap/{signature}/export POST a picture captured by the browser
omnimap_suppress /omnimap/{signature}/suppress POST the picture deleted
omnimap_metadata /omnimap/{signature} GET, HEAD the picture's metadata

SnapshotController:

  • tile: the tile's bytes from the store (404 "No such map tile." when none or the signature is invalid). When the route's path ends in .webp and the store keeps another format, converted to WebP at quality 75 and fitted within public_size - with GD when it was built with WebP, else Imagick, else served as kept. Written also under public_directory at the route's path. Public, max-age of max_age, must-revalidate, an ETag (MD5 of the bytes), 304 when the request's matches.
  • metadata: the picture's metadata as JSON, {"status": "BAD"} when none.
  • export: for whoever Access grants control (403 "Only whoever may change the map." otherwise, and when no Access is bound), with a valid _token of intent omnimap-export when Tokens is bound (403 "Invalid token."); image (a data: URL; 400 "No picture was sent." when empty), tilesize (else cache.tilesize); the picture saved, the public tiles of that signature deleted, its metadata answered. An invalid picture is a 400 with omnimap's message.
  • suppress: the same guard with intent omnimap-suppress; the picture deleted with its public tiles; {"status": "OK"}.

The public tiles are deleted only inside public_directory and in a folder whose path holds the signature.

Proposer une modification docs/omnimap.md

Storages and static export

Storages: omnidrive

Where omnisense/omnidrive is installed (suggested), Omnidrive\Drives is the application's storages as omnidrive's drives: an Omnidrive\Flysystem\OperatorDrives over the services tagged flysystem.storage, by their storage attribute - the storages of league/flysystem-bundle's configuration (or omnibase's local ones without the bundle). Nothing is configured a second time: config/packages/flysystem.yaml names them, the code reads and writes them through Drive.

# config/packages/flysystem.yaml (league/flysystem-bundle)
flysystem:
    storages:
        local.uploads:
            adapter: local
            options: { directory: '%kernel.project_dir%/public/uploads' }
use Omnidrive\Drives;

final class Avatars
{
    public function __construct(private readonly Drives $drives) {}

    public function store(int $id, string $bytes): void
    {
        $this->drives->get('local.uploads')->write('avatars/'.$id.'.png', $bytes);
    }
}
Service Class
Omnidrive\Flysystem\OperatorDrives a tagged_locator('flysystem.storage', 'storage')
Omnidrive\Drives (public alias) the application's, by the storages' names (names(), get())

The drive is a FlysystemDrive over the very operator: a file written through the drive is in the host's storage (DrivesTest). What a drive does (addresses, visibility, errors) is omnidrive's manual. The map cache's cache.drive names one of these drives (Maps).

Static export: omnibridge/static

Where omnibridge/static is installed, config/services.php registers the console command omnibridge:static:build (Omnibridge\Symfony\Static\BuildCommand, tag console.command): omnibridge/static's own command line (Console\Cli), handed what this host offers the site's file.

bin/console omnibridge:static:build static.php --out=var/static [--verify] [--strict]
Argument or option Default Effect
site static.php the file that declares the site (it returns an Omnibridge\Static\Site, or a closure that takes the host's offer); relative to the project directory unless absolute
--out var/static the folder to export to (emptied first if it holds an earlier export)
--render-origin the router's default URI the origin the pages are rendered at: the request context's scheme and host, its port unless 80 or 443
--verify off render every page twice; a page that differs is an error
--strict off a warning fails the build too
--quiet-progress off errors and counts only

What the command hands Cli::build() as the host's offer:

Key Value
Cli::HOST_NAME symfony
Cli::HOST_RENDERER a RequestRenderer over $kernel->handle($request): each page a visitor's GET at the render origin, through the kernel's own request handling (firewall, listeners, controllers)
Cli::HOST_ROUTES the router's RouteCollection
Cli::HOST_CONTAINER the kernel's container
Cli::HOST_PUBLIC_DIR <project>/public

The exit code is Cli::build()'s (its report, its checks and its codes are omnibridge/static's manual); an InvalidArgumentException it throws (a site file that is not one) is printed as an error and the command exits with Command::INVALID (2).

Run on lesringostars (2026-10-24, the working copy loaded by hand, neither package installed there yet): the same report as the plain command line, but for the dev 404 page (Symfony's exception page, its trace).

Proposer une modification docs/drives-static.md

Errors

At compile time

Error When
LogicException omniauth.yaml turns "<key>" on, which needs <package>: composer require <package>. totp without omniauth/otp, passkey without omniauth/webauthn, oauth without omniauth's Omniauth\OAuth\
InvalidConfigurationException omnimap.cache needs a drive ... or a storage ... omnimap.cache with neither drive nor storage
InvalidConfigurationException (Symfony's) omnimap.provider missing or empty, a value out of range (email_code.digits 4 to 9, totp.digits 6 to 10, cache.scale 1 or 2, a lifetime under 60...), cache.format not one of the four
none an adapter whose framework service is missing: removed by ContractsPass, the container compiles without that contract

At run time

Exception Thrown by When
Omnibridge\Exception\MissingContractException Omnibridge\Runtime a contract asked that this container does not bind (no SecurityBundle: Access, Sessions, Tokens; Locales, Files, Routes unless the application or omnibase binds them)
Symfony's RouteNotFoundException RouterUrls, and every place that generates a configured route a route name the router does not know (login_route, second_factor.form_route, oauth.sign_up_route...)
Omnibridge\Symfony\Auth\Exception\SignInRefused OmniauthAuthenticator every refused sign-in: a CustomUserMessageAuthenticationException with factor, failure (omniauth's Failure or null), reason (csrf, identity-unknown, omniauth's), identity (an unknown provider identity); the message is the page's (messages). Security hands it to onAuthenticationFailure(), never to the application
LogicException OmniauthAuthenticator::authenticate() omniauth's account is not a Symfony UserInterface
LogicException Contexts::of() no request to sign in from
LogicException CeremonyController::secondFactor() no TwigBundle ("The second factor's page is a Twig template..."); a second factor asked with none offered
NotFoundHttpException CeremonyController::oauthStart() no identity provider served, or an unknown provider (404)
InvalidArgumentException OAuthProviders::create() a provider's service is not an omniauth Provider
InvalidArgumentException PendingToken an empty firewall name; an attribute asked that the token does not hold
NotFoundHttpException SnapshotController::tile() no such tile (404)
BadRequestHttpException SnapshotController::export(), suppress() no picture sent, an invalid picture or signature (400, omnimap's message)
AccessDeniedHttpException SnapshotController::export(), suppress() the visitor lacks control, or no Access is bound; an invalid token (403)

Proposer une modification docs/errors.md

Testing

Testing an application

Under a KernelTestCase or a WebTestCase, booting the kernel fills the registry (OmnibridgeBundle::boot() or omnibase's BaseBundle), and a new kernel replaces the previous one's services: nothing to reset between tests. A test that sets a contract by hand (Runtime::set()) takes it back after (Runtime::set($contract, null) or Runtime::reset()), or the next tests see it.

The sign-in is tested as a visitor does it, with Symfony's browser: post the form, follow the redirection to the second factor's page, post the code. The package's AuthTest does exactly that, with an in-memory accounts store, omniauth's RecordingSender (the e-mails kept) and FakeProvider (an identity provider simulated): copy its kernel (tests/Fixtures/AuthKernel.php) for an application's own.

The package's suite

docker compose run --rm test   # composer update, then PHPUnit
docker compose run --rm bare   # without the suggested families

compose.yml's two services use the image omnibridge-symfony:php8.4 (docker/Dockerfile: PHP 8.4-cli, Composer 2, intl, zip, and GD with JPEG and WebP for omnimap's pictures), the package mounted at /package (bare: read-only at /source), a named volume composer-cache. bare (docker/bare.sh) copies the package, removes omnisense/omniauth and its methods, omnisense/omnimap and its providers, omnisense/omnidrive and league/flysystem-memory, checks that none of their classes loads, and runs the group without-omniauth. With the checkout's vendor/ installed, the suite runs without network:

docker run --rm --network none -v "$PWD":/package omnibridge-symfony:php8.4 vendor/bin/phpunit --do-not-cache-result

The kernels (tests/Fixtures): TestKernel (FrameworkBundle, SecurityBundle with an in-memory user, TwigBundle, OmnibridgeBundle; without SecurityBundle on demand; a Flysystem memory storage tagged memory.uploads), AuthKernel (one firewall signing in through omniauth alone, every method on, the accounts in memory, TOTP secrets sealed with a test key), MapKernel (omnimap on OpenStreetMap, its tiles answered by the test, the cache on a local storage).

Test class What it checks
ContractsTest the bundle seeds the registry with each adapter (one object through Runtime and the container); shutdown takes it back; Urls are the router's addresses; Translations the translator's messages; Environment the kernel's; Access the authorization checker's and Sessions sign the visitor out; the remember-me domain asked at sign-out; Tokens the CSRF manager's; without SecurityBundle the container compiles without Access, Sessions and Tokens
TwigTest the pages' functions are the contracts; they answer as the framework's services; without security the contracts leave the functions out
FileLinksTest the format maps the container to the host; the former syntax read as Symfony's; nothing to map; the decoration links to the host; an error page links to the host; export() gives the renderers built outside the container the mapping
DrivesTest the tagged storages are the drives, by their names; a file written through the drive is in the storage
AuthTest a password signs in; a wrong password, an unknown address and a forged token are refused; tries throttled; remember-me is Security's own; TOTP secrets sealed at rest with a key; an account with TOTP is asked its code and only its page opens; the second factor's page is no one's without a first factor; an e-mailed code sent hashed and served once; a backup code stands for the TOTP once; a trusted device skips the second factor until revoked; a sign-in link serves once and a failed one is no link; a passkey signs in without the second factor; an identity provider signs in by its subject and asks the second factor; the return address follows the host of each request; an unserved provider is no page
MapTest the services; a visitor gets the picture the server made and its tiles (WebP, written in the public folder); an administrator captures and deletes with the page's tokens, a visitor refused
WithoutOmniauthTest (group without-omniauth) without omniauth installed, nothing of Auth\ is wired

Proposer une modification docs/testing.md

Verified, and not

What the tests prove

Run on 2026-10-27 in the package's image omnibridge-symfony:php8.4 (PHP 8.4, Symfony 8.1.9), the checkout's own vendor/, no network: 38 tests, 246 assertions, green. What each class checks is in Testing. The bare run (without the suggested families) needs Composer's network and was not run that day; the commit f643d67 states it green.

The examples of this manual run in the same image on 2026-10-27, their output pasted: the test kernel booted with and without SecurityBundle (Installation), the FileLinks decoration and formats (Contracts), and the two configuration trees, dumped with config:dump-reference (omniauth, omnimap). For the dump, symfony/yaml (not installed in this package's vendor) was loaded from omnibridge/laravel's vendor, the same version (8.1.9).

Verified elsewhere, as the commits and the README state

  • The contracts and omniauth's sign-in on lesringostars (Symfony), with its parity suite against the Laravel twin.
  • The map tiles on lesringostars: WebP converted with Imagick where GD has no WebP (commit 6cdce16), at quality 75 (3fa1968).
  • omnibridge:static:build on lesringostars (2026-10-24, the working copy loaded by hand): the same report as the plain command line but for the dev 404 page (commit 67129db).
  • omniauth's provider factories through a service locator, after a compiled container (debug off) dropped an inlined tagged iterator (lesringostars' prod_demo_test kernel, Symfony 8.1.5).

What is not verified

  • Symfony 6 and 7: composer.json admits them, the suite runs on Symfony 8.1 only.
  • A real identity provider: Google is simulated (FakeProvider) and configured by GoogleFactory in the tests; no sign-in against Google's servers in this package.
  • A real passkey authenticator: the tests use omniauth/webauthn's in-memory passkeys.
  • omnimap providers other than OpenStreetMap through this wiring: Google is registered, not exercised; MapLibre is registered and resolved as a renderer (MapTest::testTheServices), its page not drawn.
  • FileLinks::formatter() as a decoration in an application: tested by FileLinksTest and run here, registered by no configuration of this package or omnibase.

Proposer une modification docs/verified.md