glitchr / omniguard
Omniguard: one contract for guarding a form - is this token valid (a captcha: ALTCHA, Turnstile, reCAPTCHA), is this content spam (Akismet), are this address, this e-mail, this name known for abuse (StopForumSpam, disposable e-mail domains) - whoever answers; and its Symfony bridge: a form field, a
Requires
- php: >=8.2
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- psr/cache: ^2.0|^3.0
- symfony/cache: ^6.4|^7.0|^8.0
- symfony/config: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/form: ^6.4|^7.0|^8.0
- symfony/http-client: ^6.4|^7.0|^8.0
- symfony/http-foundation: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
- symfony/routing: ^6.4|^7.0|^8.0
- symfony/translation: ^6.4|^7.0|^8.0
- symfony/validator: ^6.4|^7.0|^8.0
- symfony/yaml: ^6.4|^7.0|^8.0
- twig/twig: ^3.0
Suggests
- omniguard/akismet: Akismet: is this content spam - comment-check, submit-spam, submit-ham
- omniguard/altcha: ALTCHA: a proof of work served and checked by the site itself - no third party, no cookie, no consent; the default
- omniguard/disposable: Disposable e-mail domains: a free list shipped in the package, no call
- omniguard/recaptcha: Google reCAPTCHA: v2 checkbox and invisible, v3 score, Enterprise assessments
- omniguard/stopforumspam: StopForumSpam: are this IP address, this e-mail, this name reported for abuse
- omniguard/turnstile: Cloudflare Turnstile: a token checked by Cloudflare's siteverify
- psr/cache: A spent token remembered in a PSR-6 pool (Omniguard\Replay\CacheReplayStore) rather than in this process's memory
- symfony/config: In a Symfony application: the configuration of Omniguard\Bridge\Symfony\OmniguardBundle
- symfony/dependency-injection: In a Symfony application: the services of Omniguard\Bridge\Symfony\OmniguardBundle
- symfony/form: In a Symfony application: Omniguard\Bridge\Symfony\Form\ChallengeType
- symfony/http-kernel: In a Symfony application: Omniguard\Bridge\Symfony\OmniguardBundle itself
- symfony/routing: In a Symfony application: the route that serves an ALTCHA challenge
- symfony/translation: In a Symfony application: the widgets' texts in the visitor's language (domain omniguard)
- symfony/validator: In a Symfony application: the PassesChallenge constraint
- twig/twig: With Twig: Omniguard\Bridge\Twig\OmniguardExtension, the widget printed by omniguard_widget()
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-09 03:24:25 UTC
README
One contract for guarding a form, whoever answers. Three questions, and a gateway answers the one that concerns it:
- is this token valid? - a captcha: ALTCHA, Cloudflare Turnstile, Google reCAPTCHA;
- is this content spam? - a classifier: Akismet;
- are this address, this e-mail, this name known for abuse? - a list: StopForumSpam, the disposable e-mail domains.
$widget = $registry->challenge('forms')->widget('contact'); // what the page shows: script, element, field echo $widget->html(); // inside the <form> $verdict = $registry->challenge('forms')->verify(Attempt::fromPost($_POST, $widget, $ip)); $verdict->passed; // false: $verdict->reasons - missing, invalid, duplicate... $registry->classifier('comments')->classify($submission)->isSpam(); $registry->reputation('emails')->lookup(new Identity($ip, $email))->known;
This package holds the contract (ChallengeInterface, ClassifierInterface,
ReputationInterface, GatewayFactory, Registry), the models (Widget, Attempt, Verdict,
Submission, Classification, Identity, Reputation, Capabilities), the store of spent
tokens (ReplayStoreInterface), Testing\FixedGateway for an application's tests, and a bridge
for Symfony. It needs no framework: it requires PHP and symfony/http-client-contracts (the
interfaces, no client). Each gateway is a package of its own:
| Package | It answers | Third party | Cookies |
|---|---|---|---|
omniguard/altcha |
is this token valid: a proof of work the site issues and checks itself | none (the widget's script: yours, or jsDelivr's) | none |
omniguard/turnstile |
is this token valid: Cloudflare's widget, Cloudflare's siteverify | Cloudflare | none |
omniguard/recaptcha |
is this token valid: v2 checkbox and invisible, v3 score, Enterprise | _GRECAPTCHA |
|
omniguard/akismet |
is this content spam: ham, spam, flagrant; reports back | Akismet (Automattic) | - |
omniguard/stopforumspam |
is this identity known: the public database, a threshold of the site's | StopForumSpam | - |
omniguard/disposable |
is this e-mail's domain disposable: a free list shipped in the package | none | - |
What a gateway is, and is not
A gateway answers. It does not decide what a refusal means for the form, nor what to do when its
provider does not answer: the application does. A refusal is a Verdict, a Classification, a
Reputation - never an exception; a provider that did not answer is an
UnreachableException, a key it refused an InvalidKeyException, and neither is ever taken
for "fine".
It is told strings - a token, an address, a text, a name - and never an account, an entity or a request.
What a gateway cannot do is a NotSupportedException, and its Capabilities say so beforehand:
a captcha classifies nothing, a list without a key takes no report.
Documentation
- Installation and a first form
- Models
- The three questions: the contracts, the gateways, writing one
- Symfony: the bundle, the form field, the constraint, the route, Twig
- Privacy: who sees what, cookies, consent
- The Docker harness: the console, bare PHP
Plain PHP
composer require glitchr/omniguard omniguard/altcha omniguard/disposable
use Omniguard\Altcha\AltchaGatewayFactory; use Omniguard\Disposable\DisposableGatewayFactory; use Omniguard\Registry; $registry = new Registry([new AltchaGatewayFactory($spentTokens), new DisposableGatewayFactory()], [ 'forms' => ['factory' => 'altcha', 'options' => ['hmac_key' => getenv('ALTCHA_HMAC_KEY')]], 'emails' => ['factory' => 'disposable'], ]);
No bundle, no container: a factory per gateway package, the registry built by hand. docs/installation.md opens on a whole script that runs as it is.
Symfony
Omniguard\Bridge\Symfony\OmniguardBundle does that wiring in a Symfony application, and adds a
form field, a constraint, the route of the ALTCHA challenge and a Twig function
(docs/symfony.md); none of their components is required by this package.
omniguard: gateways: forms: { factory: altcha, options: { hmac_key: '%env(ALTCHA_HMAC_KEY)%' } } emails: { factory: disposable }
$builder->add('captcha', ChallengeType::class, ['action' => 'contact']);
Docker: every gateway, bare PHP
cd docker && cp .env.dist .env docker compose run --rm omniguard gateways docker compose run --rm omniguard bare # plain PHP: no bundle, no container, and what PHP loaded docker compose run --rm omniguard bare --live # the same against the providers, with their testing keys docker compose run --rm omniguard test
License: MIT since 2026-10-09; earlier versions remain published under LGPL-3.0-or-later.