PHPackages                             fedale/access-control-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. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. fedale/access-control-bundle

ActiveSymfony-bundle[Authentication &amp; Authorization](/categories/authentication)

fedale/access-control-bundle
============================

Symfony bundle for dynamic access control and authorization management

v1.0.0(1mo ago)037MITPHPPHP ^8.2

Since Jun 11Pushed 2w agoCompare

[ Source](https://github.com/fedale/access-control-bundle)[ Packagist](https://packagist.org/packages/fedale/access-control-bundle)[ RSS](/packages/fedale-access-control-bundle/feed)WikiDiscussions main Synced 2w ago

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

Fedale AccessControl Bundle
===========================

[](#fedale-accesscontrol-bundle)

[![PHP](https://camo.githubusercontent.com/d91d3d1139cf0d8faaa80eeeeac7d3c59c9319e56960ef81c948e4160be4c4c1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d3737376262342e737667)](https://www.php.net/)[![Symfony](https://camo.githubusercontent.com/84e88a55bb89cf99aa82f5d038eceba4860a83a90f41f88a05f14a3b62dc5d27/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73796d666f6e792d253545362e34253230253743253743253230253545372e302d3030303030302e737667)](https://symfony.com/)

Rule-based HTTP access control for Symfony. The bundle inspects every incoming request and allows or denies it according to a set of rules matched on **path, host, HTTP method, client IP and user roles**. Rules can come from any source — Doctrine (built-in), a YAML/array, a remote API, MongoDB… — because the lookup sits behind a single interface. An optional PSR-6 cache layer keeps rule loading cheap.

---

Features
--------

[](#features)

- Allow/deny rules matched on **path** (regex), **host** (regex), **HTTP methods**, **client IP / CIDR** and **roles**.
- **First-match-wins** evaluation, ordered by an explicit `sort` field.
- **Source-agnostic**: rules are read through `RuleProviderInterface`. Doctrine is the default; any service can replace it.
- **Super-admin bypass** and configurable **anonymous-access** gating.
- **Default policy** (`allow`/`deny`) applied when no rule matches.
- Optional **PSR-6 caching** of the rule set, configurable from YAML.
- Built on Symfony's native `ChainRequestMatcher` — no deprecated APIs.

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

[](#requirements)

- PHP **8.2+**
- Symfony **6.4** or **7.x** (`http-kernel`, `http-foundation`, `security-bundle`, `security-core`)
- `psr/cache` ^3.0
- **Doctrine ORM** is only required when you use the default `doctrine` rule provider (`doctrine/orm`, `doctrine/doctrine-bundle`). It is a `require-dev`/`suggest` dependency, not a hard one.

Installation
------------

[](#installation)

```
composer require fedale/access-control-bundle
```

> **Not on Packagist yet.** Until the package is published, point Composer at the repository directly in your application's `composer.json`:
>
> ```
> {
>     "repositories": [
>         { "type": "vcs", "url": "https://github.com/Fedale/FedaleAccessControlBundle" }
>     ]
> }
> ```
>
>
>
> then run `composer require fedale/access-control-bundle:dev-main`.

If you use **Symfony Flex**, the bundle is registered automatically. Otherwise, add it to `config/bundles.php`:

```
// config/bundles.php
return [
    // ...
    Fedale\AccessControlBundle\FedaleAccessControlBundle::class => ['all' => true],
];
```

Quick start
-----------

[](#quick-start)

1. Create the configuration file:

```
# config/packages/fedale_access_control.yaml
fedale_access_control:
    enabled: true
    super_admin_role: ROLE_SUPER_ADMIN
    default_policy: deny      # block anything no rule matched
    provider: doctrine        # read rules from the database
```

2. Create the database table and add a rule (see [Doctrine provider](#doctrine-default-provider)). For example, a rule that blocks `/admin` for everyone except `ROLE_ADMIN`:

namepathrolesallowsortadmin-area`^/admin``["ROLE_ADMIN"]``1``0`3. That's it. A request to `/admin` from an anonymous user now receives:

```
HTTP/1.1 403 Forbidden

Access denied

```

while a user with `ROLE_ADMIN` (or `ROLE_SUPER_ADMIN`) passes through.

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

[](#configuration-reference)

All options with their defaults:

```
# config/packages/fedale_access_control.yaml
fedale_access_control:

    # Master switch. When false the listener allows every request.
    enabled: true

    # When false, requests from unauthenticated users that match no rule are denied,
    # regardless of `default_policy`. See "Super admin & anonymous access".
    anonymous_access: false

    # Role that bypasses all rules. Set to an empty string to disable the bypass.
    super_admin_role: ROLE_SUPER_ADMIN

    # Policy applied when no rule matches the request: allow | deny
    default_policy: deny

    cache:
        # Enable/disable the PSR-6 cache decorator around the rule provider.
        enabled: true
        # Id of the PSR-6 cache pool used to store the rules.
        pool: cache.app
        # Time-to-live in seconds; null = no expiration (invalidate the pool manually).
        ttl: null

    # 'doctrine' (built-in) or the service id of a custom RuleProviderInterface
    # implementation (e.g. a YAML, API or Mongo source).
    provider: doctrine
```

OptionTypeDefaultDescription`enabled`bool`true`Turn the whole access control on/off.`anonymous_access`bool`false`Allow unauthenticated users through on an unmatched request.`super_admin_role`string`ROLE_SUPER_ADMIN`Role that bypasses every rule (empty string disables it).`default_policy``allow|deny``deny`Decision when no rule matches.`cache.enabled`bool`true`Wrap the provider with a caching decorator.`cache.pool`string`cache.app`PSR-6 pool service id.`cache.ttl`int|null`null`Cache lifetime in seconds; `null` means no expiration.`provider`string`doctrine``doctrine` or a custom provider service id.How it works
------------

[](#how-it-works)

The bundle registers a listener on the `kernel.request` event (priority `-10`). For every main request it calls `AccessDecisionManager::decide()`, which evaluates:

1. **Bundle disabled?** (`enabled: false`) → **allow**.
2. **Super admin?** (current user is granted `super_admin_role`) → **allow**.
3. **Find the first matching rule** (rules are evaluated in `sort` order, first match wins):
    - rule found and `allow = false` → **deny**;
    - rule found, `allow = true`, no roles required → **allow**;
    - rule found, `allow = true`, roles required → **allow** if the user is granted **any** of them, otherwise **deny**.
4. **No rule matched** (fallback):
    - user is **not authenticated** and `anonymous_access = false` → **deny**;
    - otherwise apply `default_policy` (`allow` → allow, `deny` → deny).

When the decision is **deny**, the listener dispatches an `AccessDeniedEvent` and then short-circuits the request with a `403 Forbidden` response. If no listener provides its own response, the body is **content-negotiated**: clients that send `Accept: application/json` get `{"error":"Access denied"}`, everyone else gets the plain text `Access denied`.

> The bundle stays **outside** Symfony's firewall: a denied request always yields a `403`, it is never redirected to the login page. Authentication-driven redirects remain the firewall's job — this layer only answers "may this request proceed?".

### Reacting to a denial (`AccessDeniedEvent`)

[](#reacting-to-a-denial-accessdeniedevent)

To log denials, emit metrics, or return a custom response (a branded error page, a redirect, a different JSON shape), listen for `AccessDeniedEvent`. Setting a response on the event overrides the default one:

```
use Fedale\AccessControlBundle\Event\AccessDeniedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;

#[AsEventListener]
final class AccessDeniedSubscriber
{
    public function __invoke(AccessDeniedEvent $event): void
    {
        // $event->getRequest() is available for inspection/logging.
        $event->setResponse(new Response('Custom forbidden page', Response::HTTP_FORBIDDEN));
    }
}
```

Defining rules
--------------

[](#defining-rules)

A rule is described by the `AccessRule` DTO. Whatever the source, it produces objects with these fields:

FieldTypeMeaning`id`intIdentifier (from the source).`name`stringHuman-readable label.`reason`?stringOptional explanation (e.g. why access is blocked); `null` when unset.`path`string**Regular expression** matched against the request path (e.g. `^/admin`).`host`?stringOptional **regular expression** matched against the host.`roles`string\[\]Required roles; empty means "no role required".`methods`string\[\]Allowed HTTP methods (e.g. `["GET", "POST"]`); empty means any.`ips`string\[\]Allowed IPs or CIDR ranges (e.g. `["10.0.0.0/8"]`); empty means any.`allow`bool`true` = allow (subject to roles), `false` = deny.`sort`intEvaluation order (ascending). Lower runs first.`active`boolInactive rules are ignored.Matching is delegated to Symfony's `ChainRequestMatcher`: `path` and `host` are treated as regular expressions, while `methods` and `ips` only constrain the request when non-empty. Invalid `path`/`host` regular expressions are rejected as soon as the rule is built, with an `InvalidArgumentException` naming the offending rule.

> **Client IP behind a proxy.** The `ips` field matches against `Request::getClientIp()`. If your app sits behind a reverse proxy or load balancer, configure Symfony's [trusted proxies](https://symfony.com/doc/current/deployment/proxies.html)(`framework.trusted_proxies` / `trusted_headers`) — otherwise `getClientIp()` returns the proxy's address and your IP rules will match the wrong client.

Rule sources (providers)
------------------------

[](#rule-sources-providers)

The decision logic never talks to a database directly — it depends only on `RuleProviderInterface`:

```
namespace Fedale\AccessControlBundle\Contract;

use Fedale\AccessControlBundle\Dto\AccessRule;

interface RuleProviderInterface
{
    /** @return iterable */
    public function getRules(): iterable;
}
```

The `provider` option selects which implementation is wired.

### Doctrine (default provider)

[](#doctrine-default-provider)

With `provider: doctrine` the rules are read from the `access_control` table, mapped by the `AccessControlEntity`. Only **active** rules are returned, ordered by `sort`.

The bundle **registers its own Doctrine ORM mapping automatically** (via `prependExtension`) when `provider: doctrine` and DoctrineBundle is installed — you do **not** need to add a `doctrine.orm.mappings` entry pointing into `vendor/` in your application. The mapping is skipped automatically if you switch to a custom provider.

Create the schema with a migration (recommended):

```
php bin/console make:migration
php bin/console doctrine:migrations:migrate
```

or, in development:

```
php bin/console doctrine:schema:update --force
```

Add rules like any other entity:

```
use Fedale\AccessControlBundle\Bridge\Doctrine\Entity\AccessControlEntity;

$rule = (new AccessControlEntity())
    ->setName('admin-area')
    ->setReason('Admins only')
    ->setPath('^/admin')
    ->setRoles(['ROLE_ADMIN'])
    ->setMethods([])          // any method
    ->setIps([])              // any IP
    ->setAllow(true)
    ->setSort(0)
    ->setActive(true);

$entityManager->persist($rule);
$entityManager->flush();
```

The setters normalise input for you (trim strings, de-duplicate roles/IPs, upper-case methods).

### Custom provider (YAML, API, Mongo, …)

[](#custom-provider-yaml-api-mongo-)

To read rules from anywhere else, implement `RuleProviderInterface` and point `provider` at your service id. Here is a provider that returns rules defined in plain PHP:

```
// src/Security/StaticRuleProvider.php
namespace App\Security;

use Fedale\AccessControlBundle\Contract\RuleProviderInterface;
use Fedale\AccessControlBundle\Dto\AccessRule;

final class StaticRuleProvider implements RuleProviderInterface
{
    public function getRules(): iterable
    {
        yield new AccessRule(
            id: 1,
            name: 'admin-area',
            reason: 'Admins only',
            path: '^/admin',
            host: null,
            roles: ['ROLE_ADMIN'],
            methods: [],
            ips: [],
            allow: true,
            sort: 0,
            active: true,
        );

        yield new AccessRule(
            id: 2,
            name: 'block-internal',
            reason: 'Internal API is private',
            path: '^/internal',
            host: null,
            roles: [],
            methods: [],
            ips: [],
            allow: false,
            sort: 10,
            active: true,
        );
    }
}
```

If autowiring/autoconfiguration is on (the Symfony default), the service id equals the class name. Select it:

```
# config/packages/fedale_access_control.yaml
fedale_access_control:
    provider: 'App\Security\StaticRuleProvider'
```

With a custom provider you do **not** need Doctrine at all.

> The selected provider must resolve to a real service; otherwise the `RuleProviderInterface` alias is missing and the container fails to boot.

Caching
-------

[](#caching)

When `cache.enabled` is `true`, the selected provider is transparently wrapped in a `CachedRuleProvider` that stores the materialised rule list in a **PSR-6** pool. This works for any source (Doctrine, custom, …) because it decorates the interface, not the implementation.

```
fedale_access_control:
    cache:
        enabled: true
        pool: cache.app   # any PSR-6 pool service
        ttl: 3600         # refresh hourly; null = never expires
```

**Invalidation.** With `ttl: null` the cache never expires, so after changing your rules you must clear it. Either clear the configured pool, or delete the single cache key used by the bundle:

```
use Fedale\AccessControlBundle\Cache\CachedRuleProvider;

$pool->deleteItem(CachedRuleProvider::CACHE_KEY);
```

To turn caching off entirely, set `cache.enabled: false`.

Super admin &amp; anonymous access
----------------------------------

[](#super-admin--anonymous-access)

- **Super admin** — any user granted `super_admin_role` (default `ROLE_SUPER_ADMIN`) bypasses every rule. Set `super_admin_role: ''` to remove the bypass.
- **Anonymous access** — only affects requests that match **no** rule. With `anonymous_access: false`, unauthenticated users are denied on an unmatched request even if `default_policy: allow`. Authenticated users instead fall through to `default_policy`.

Example — site that is open by default, but never to anonymous users on unknown routes:

```
fedale_access_control:
    anonymous_access: false
    default_policy: allow
```

Explicit `allow` rules (e.g. for `^/login`) are always evaluated first, so public routes keep working.

Architecture / extension points
-------------------------------

[](#architecture--extension-points)

Everything is wired through small contracts under `src/Contract/`, so you can override any piece:

InterfaceResponsibility`RuleProviderInterface`Where rules come from (Doctrine, YAML, API, …).`RuleRequestMatcherInterface`Whether a single rule matches a request.`AccessMatcherInterface`Pick the first matching rule for a request.`AccessDecisionManagerInterface`Turn a request into an allow/deny decision.On every denial the listener dispatches `AccessDeniedEvent`, so you can hook in logging, metrics or a custom response without replacing any of the above (see [Reacting to a denial](#reacting-to-a-denial-accessdeniedevent)).

Default implementations live in `src/Matcher/`, `src/Security/` and `src/Cache/`. To replace one, register your own service aliased to the relevant interface.

Console
-------

[](#console)

List the effective rules as the bundle sees them (read-only, works with any provider):

```
php bin/console fedale:access-control:list
```

```
 sort  id  name        path       host  methods  ips         roles       policy  active
 0     1   admin-area  ^/admin    *     *        *           ROLE_ADMIN  allow   yes
 10    2   block-int   ^/internal *     *        10.0.0.0/8  -           deny    yes

```

With the Doctrine provider only **active** rules are listed; with caching enabled you see the materialised (cached) list. Enabling/disabling rules is plain entity manipulation — see [Doctrine provider](#doctrine-default-provider).

> A Symfony Profiler **data collector** (showing which rule matched and the resulting decision) is a natural next extension but is not bundled yet, to keep the dependency surface small.

Testing
-------

[](#testing)

```
composer install
vendor/bin/phpunit
```

The suite covers unit (matchers, decision manager, cache), DI wiring, a functional test booting a real `HttpKernel`, and ORM mapping validation.

License &amp; contributing
--------------------------

[](#license--contributing)

Released under the [MIT License](LICENSE). Issues and pull requests are welcome.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance94

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 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

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/aad4207f0dcf5e8fcac3ef897660e0f550a85e9ccb097868fe8419cdd8b5cfe5?d=identicon)[fedale](/maintainers/fedale)

---

Top Contributors

[![fedale](https://avatars.githubusercontent.com/u/127323?v=4)](https://github.com/fedale "fedale (9 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/fedale-access-control-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/fedale-access-control-bundle/health.svg)](https://phpackages.com/packages/fedale-access-control-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M405](/packages/easycorp-easyadmin-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M608](/packages/shopware-core)[symfony/security-bundle

Provides a tight integration of the Security component into the Symfony full-stack framework

2.5k185.6M2.4k](/packages/symfony-security-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M215](/packages/sulu-sulu)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)[contao-community-alliance/dc-general

Universal data container for Contao

1680.8k92](/packages/contao-community-alliance-dc-general)

PHPackages © 2026

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