PHPackages                             tito10047/iconcaptcha-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. tito10047/iconcaptcha-bundle

ActiveSymfony-bundle

tito10047/iconcaptcha-bundle
============================

A self-hosted, customizable, easy-to-implement and user-friendly captcha for PHP.

0.1.0(today)010↑2600%MITPHPPHP &gt;=8.2CI passing

Since Aug 25Pushed todayCompare

[ Source](https://github.com/tito10047/icon-captcha-bundle)[ Packagist](https://packagist.org/packages/tito10047/iconcaptcha-bundle)[ RSS](/packages/tito10047-iconcaptcha-bundle/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (19)Versions (2)Used By (0)

IconcaptchaBundle
=================

[](#iconcaptchabundle)

Symfony integration for [IconCaptcha](https://github.com/fabianwennink/IconCaptcha-PHP) — a self-hosted, privacy-friendly captcha where the visitor picks the least-common icon out of five to eight. No third-party service, no tracking, no API key.

The underlying library talks to `$_SESSION` directly and ends its AJAX endpoint with `exit()`. This bundle replaces both with Symfony equivalents and adds a form type, a constraint, a Twig extension and asset wiring.

Requirements
------------

[](#requirements)

PHP&gt;= 8.2, with the `gd` extension (or `imagick`)Symfony6.4, 7.4 or 8.1Session`framework.session` must be enabled — challenges and the CSRF token live thereInstallation
------------

[](#installation)

```
composer require tito10047/iconcaptcha-bundle
```

Symfony Flex registers the bundle in `config/bundles.php` automatically. Without Flex, add it by hand:

```
// config/bundles.php
return [
    // ...
    Tito10047\IconcaptchaBundle\IconcaptchaBundle::class => ['all' => true],
];
```

### Register the endpoint

[](#register-the-endpoint)

The widget loads its challenge over AJAX, so the bundle's route has to be imported. There is no published Flex recipe yet, so this step is manual:

```
# config/routes/icon_captcha.yaml
icon_captcha:
    resource: '@IconcaptchaBundle/config/routes.php'
    type: php
```

This registers a single route, `iconcaptcha_request`, at `POST /iconcaptcha/request`. It only answers to requests carrying `X-Requested-With: XMLHttpRequest` and a valid token; anything else gets a 400.

Usage with a form
-----------------

[](#usage-with-a-form)

```
use Tito10047\IconcaptchaBundle\Type\IconCaptchaType;

$builder->add('captcha', IconCaptchaType::class);
```

That is the whole integration. The field is unmapped, carries the `IconCaptcha`constraint, and renders the widget, the CSRF token and the client init script from the bundle's form theme — so `{{ form(form) }}` or `{{ form_row(form.captcha) }}` is enough.

### Field options

[](#field-options)

OptionTypeDefaultMeaning`theme``'light'` | `'dark'`the bundle's `theme` settingWidget colour scheme`include_assets``bool``false`Emit ``/`` tags for the client assets next to this widget`client_options``array``[]`Extra [client options](https://www.fabianwennink.nl/projects/IconCaptcha/documentation) such as `showCredits` or `locale`### Turning the captcha off

[](#turning-the-captcha-off)

A functional test suite cannot solve a challenge, so the protected form has to be submittable without one:

```
# config/packages/icon_captcha.yaml
when@test:
    icon_captcha:
        enable: false
```

With `enable: false` the field renders nothing at all and the constraint always passes. Both halves matter: rendering nothing while still validating would leave the form permanently unsubmittable. Nothing touches the session either, so a disabled captcha also works on a stateless route.

`include_assets` is off by default because most applications load the client once in their layout or their AssetMapper entrypoint; turning it on for several forms on one page would emit the script repeatedly.

`client_options` cannot override `general.endpoint`. It points at this bundle's route, and sending challenges anywhere else would put them somewhere the validator never reads back from.

Usage without a form
--------------------

[](#usage-without-a-form)

The Twig extension renders the same pieces by hand:

```
{{ iconcaptcha_style() }}
{{ iconcaptcha_script() }}

    {{ iconcaptcha_widget('dark') }}
    {{ iconcaptcha_token() }}
    Submit

```

You then have to initialise the client yourself and validate the submission:

```
use Tito10047\IconcaptchaBundle\Validator\IconCaptcha;

$violations = $validator->validate(null, new IconCaptcha());
```

The constraint reads the widget's hidden fields straight off the current request, which is why the value passed to `validate()` is irrelevant.

Usage with UX Live Components
-----------------------------

[](#usage-with-ux-live-components)

Works out of the box, provided the widget renders through the Stimulus controller — which it does automatically as soon as `symfony/stimulus-bundle` is installed. Enable the controller in your application:

```
// assets/controllers.json
{
    "controllers": {
        "@tito10047/iconcaptcha-bundle": {
            "iconcaptcha": { "enabled": true, "fetch": "eager" }
        }
    }
}
```

That is the whole integration; `$builder->add('captcha', IconCaptchaType::class)` is unchanged. Three things happen behind it, and they are worth knowing about because each one works around an assumption a Live Component breaks:

- **The widget carries `data-live-ignore`.** A component re-renders on every model change, which without this would replace the widget mid-challenge and make the visitor start over on each keystroke elsewhere in the form. The field's own input deliberately does *not*carry it — that is how the solved payload travels into the model.
- **The solved challenge is copied into the field's value.** A live request sends the component's model, never the DOM, so the widget's own hidden inputs (`ic-rq`, `ic-wid`, `ic-cid`) would otherwise never reach the server. The controller packs them into the field and dispatches a `change` event, which is the only thing the model binding watches.
- **A solved challenge stays valid for `validation.completion_expiration`.** See below.

### Reusing a solved challenge

[](#reusing-a-solved-challenge)

`ComponentWithFormTrait` submits and validates the form on *every* re-render, not only on the action that saves it, while the library destroys a challenge the moment it accepts one. Left alone, the first model change after solving would spend the captcha and the real submit would come back "unsolved".

So a successful validation is remembered and accepted again for the rest of the completion window. The record lives in the visitor's own session, so this is not a way to skip the captcha — a fresh client still has to solve one. It does mean that within one session a single solve covers repeated submissions for `completion_expiration` seconds. Applications that do not use Live Components can have the stricter behaviour back:

```
icon_captcha:
    validation:
        reuse_solved: false
```

Client assets
-------------

[](#client-assets)

The JS and CSS ship inside `fabianwennink/iconcaptcha`. The bundle never copies them, so they cannot drift from the installed library version.

### With AssetMapper (recommended)

[](#with-assetmapper-recommended)

The bundle registers the library's client directory under the `@fabianwennink/iconcaptcha`namespace, matching its npm package name. Both files are declared in the bundle's `assets/package.json`, which Flex reads on install, so `importmap.php` gets them by itself:

```
// importmap.php — written by Flex
return [
    // ...
    '@fabianwennink/iconcaptcha/js/iconcaptcha.min.js' => [
        'path' => './vendor/fabianwennink/iconcaptcha/assets/client/js/iconcaptcha.min.js',
    ],
    '@fabianwennink/iconcaptcha/css/iconcaptcha.min.css' => [
        'path' => './vendor/fabianwennink/iconcaptcha/assets/client/css/iconcaptcha.min.css',
        'type' => 'css',
    ],
];
```

Add them by hand if they are missing — Flex turned off, or `symfony/asset-mapper` installed after the bundle. The path may then be the logical path the registered namespace provides, which reads better and survives a moved vendor directory:

```
'@fabianwennink/iconcaptcha/js/iconcaptcha.min.js' => [
    'path' => '@fabianwennink/iconcaptcha/js/iconcaptcha.min.js',
],
'@fabianwennink/iconcaptcha/css/iconcaptcha.min.css' => [
    'path' => '@fabianwennink/iconcaptcha/css/iconcaptcha.min.css',
    'type' => 'css',
],
```

`type` is what makes AssetMapper treat the entry as a stylesheet. `importmap:require` infers it from the `.css` extension; a hand-written entry does not — it defaults to `js` and the stylesheet silently never loads.

Import the stylesheet from your entrypoint:

```
// assets/app.js
import '@fabianwennink/iconcaptcha/css/iconcaptcha.min.css';
```

AssetMapper turns that import into a `` because of the `type: css`entry.

**Do not import the JavaScript from your entrypoint.** It is not an ES module: it declares `var IconCaptcha = ...` at the top level and relies on a classic `` to hoist that onto `window`. Imported as a module the declaration stays module-scoped and the global never appears. The Stimulus controller therefore resolves the specifier through the importmap and loads the file as a classic script itself — which is why the importmap entry above is required even though nothing appears to import it.

### Without AssetMapper

[](#without-assetmapper)

```
php bin/console iconcaptcha:install-assets
```

This copies the files into `public/bundles/iconcaptcha/`, which is where `iconcaptcha_script()` and `iconcaptcha_style()` point. Re-run it after every `composer update` of the library. Symfony's own `assets:install` cannot do this: it only publishes a bundle's own `public/` directory, and these files are not in one.

Styling the widget
------------------

[](#styling-the-widget)

The library ships one fixed look in two variants, picked with the `theme` setting or the field's `theme` option. There are no CSS custom properties, so restyling means redeclaring the library's own selectors:

- **Load your stylesheet after the library's** — your rules match its selectors exactly, so specificity ties and load order decides. With AssetMapper, import your file after `@fabianwennink/iconcaptcha/css/iconcaptcha.min.css` in the entrypoint.
- **Keep the theme class in colour rules.** Every colour is scoped to `.iconcaptcha-theme-light` / `.iconcaptcha-theme-dark`; matching it keeps the tie.

The markup is `.iconcaptcha-widget` with a `.iconcaptcha-modal` inside; the client rewrites the body on every state change and signals the current state on the widget itself: `iconcaptcha-init` (resting), no class (the challenge), `iconcaptcha-success`, `iconcaptcha-error`. `.iconcaptcha-row`, wrapping the widget and the violation, is this bundle's — it gets no class from your form theme, so row spacing has to be restated, and the widget is not an input so it never gets an `is-invalid`.

Two things not to touch:

- **The size of `__body-icons` and `__body-selection`**, capped at 320px. A click is sent as an x coordinate plus the element's width, which the server divides by the icon count. Widen either and clicks land on the wrong icon.
- **The name of the `captcha-breathing` keyframes.** They set `border-color` on every step, beating any colour on the element. To recolour the resting circle, redeclare the whole animation under the same name — a later definition replaces the library's.

```
/* assets/styles/iconcaptcha.css, imported after the library's stylesheet */

.iconcaptcha-widget {
    width: 100%;
    max-width: 100%;
}

.iconcaptcha-widget.iconcaptcha-theme-light {
    background: var(--bs-body-bg);
    border: var(--bs-border-width) solid var(--bs-border-color);
}

/* Every accented pixel: resting circle, loading spinner, selection dot. */
.iconcaptcha-widget.iconcaptcha-init .iconcaptcha-modal__body-circle,
.iconcaptcha-widget .iconcaptcha-modal__body .captcha-loader {
    border-color: #e55b25;
}

.iconcaptcha-widget .iconcaptcha-modal__body-selection > i {
    background: #e55b25;
}
```

The icons and their separators are pixels in a server-rendered PNG, not DOM, so CSS cannot reach them: `challenge.border: false` drops the separators and `theme` picks the icon set. The typeface and the credit line come from the client, not the stylesheet:

```
$builder->add('captcha', IconCaptchaType::class, [
    'client_options' => ['general' => ['fontFamily' => 'Poppins', 'showCredits' => true]],
]);
```

`fontFamily` is written onto the widget as an inline style; leaving it unset is usually better, because the library's stylesheet already sets `font-family: inherit` and the widget then picks up the surrounding form's font on its own.

Configuration reference
-----------------------

[](#configuration-reference)

Every key is optional; the values below are the defaults.

```
# config/packages/icon_captcha.yaml
icon_captcha:
    enable: true                    # false renders nothing and validates everything
    theme: light                    # light | dark

    # null auto-detects: true when symfony/stimulus-bundle is installed. The Stimulus
    # branch is what makes the widget work inside a UX Live Component.
    use_stimulus: ~

    challenge:
        available_icons: 250        # size of the icon pool
        icon_amount:
            min: 5
            max: 8
        rotate: true                # randomly rotate icons
        border: true                # draw separators between icons
        generator: gd               # gd | imagick

    validation:
        inactivity_expiration: 120  # seconds a challenge may sit untouched
        completion_expiration: 300  # seconds a solved captcha stays valid
        reuse_solved: true          # accept the same solve again within that window
        attempts:
            enabled: true
            amount: 5               # wrong picks before a timeout
            timeout: 60             # seconds of timeout

    cors:
        enabled: false
        origins: []

    hooks:                          # class names, see the library documentation
        init: null                  # InitHookInterface
        generation: null            # GenerationHookInterface
        selection: null             # SelectionHookInterface
```

There is deliberately no `storage` key. Challenges always go through Symfony's session, so the library's own storage drivers are unreachable — configuring one would only produce a `TypeError` on the first request.

Translations
------------

[](#translations)

The constraint message lives in the `validators` domain under the `invalid_iconcaptcha`key. English and Slovak are bundled; override the key in your own catalogue to change the wording.

The widget's own strings — "Verify that you are human.", the instruction above the icons, the error and timeout messages — are rendered by the client, not by Twig, so they are handed over as the `locale` client option, translated into the application's locale. They live in the `iconcaptcha` domain:

Key`iconcaptcha.initialization.loading`shown while the challenge is being fetched`iconcaptcha.initialization.verify`the widget's resting state`iconcaptcha.header`the instruction above the icons; may contain HTML`iconcaptcha.correct`shown once solved`iconcaptcha.incorrect.title` / `.subtitle`wrong icon picked`iconcaptcha.timeout.title` / `.subtitle`too many wrong picksEnglish and Slovak ship with the bundle. Override a key in your own `iconcaptcha`catalogue, or per form with `client_options`:

```
$builder->add('captcha', IconCaptchaType::class, [
    'client_options' => ['locale' => ['correct' => 'Thanks!']],
]);
```

Development
-----------

[](#development)

```
composer test        # phpunit
composer phpstan     # static analysis, level 6
composer check-cs    # php-cs-fixer, dry run
composer fix-cs      # php-cs-fixer, apply
./tests_e2e/run.sh   # end-to-end run against a throwaway skeleton project
```

License
-------

[](#license)

MIT. The wrapped [IconCaptcha](https://github.com/fabianwennink/IconCaptcha-PHP) library is MIT as well.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/11459248?v=4)[Jozef Môstka](/maintainers/tito10047)[@tito10047](https://github.com/tito10047)

---

Top Contributors

[![tito10047](https://avatars.githubusercontent.com/u/11459248?v=4)](https://github.com/tito10047 "tito10047 (21 commits)")

---

Tags

symfonybundlecaptchaformSymfony Bundleiconcaptcha

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tito10047-iconcaptcha-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/tito10047-iconcaptcha-bundle/health.svg)](https://phpackages.com/packages/tito10047-iconcaptcha-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M433](/packages/easycorp-easyadmin-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M780](/packages/sylius-sylius)[chameleon-system/chameleon-base

The Chameleon System core.

1029.4k6](/packages/chameleon-system-chameleon-base)[kimai/kimai

Kimai - Time Tracking

4.8k9.4k1](/packages/kimai-kimai)[contao/core-bundle

Contao Open Source CMS

1301.7M3.1k](/packages/contao-core-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M684](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
