PHPackages                             nexara/api-platform-voter - 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. [Security](/categories/security)
4. /
5. nexara/api-platform-voter

ActiveSymfony-bundle[Security](/categories/security)

nexara/api-platform-voter
=========================

Symfony bundle that enforces voter-based authorization for API Platform 3.

0.2.1(3mo ago)06MITPHPPHP ^8.1CI failing

Since Jan 24Pushed 3mo agoCompare

[ Source](https://github.com/nexara-group/api-platform-voter)[ Packagist](https://packagist.org/packages/nexara/api-platform-voter)[ Docs](https://github.com/nexara-group/api-platform-voter)[ RSS](/packages/nexara-api-platform-voter/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (13)Versions (13)Used By (0)

Nexara API Platform Voter
=========================

[](#nexara-api-platform-voter)

[![Packagist Version](https://camo.githubusercontent.com/9b19a8929a0e1b9efdcea29040de1311347c2b29314aa76661b187601681697b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e65786172612f6170692d706c6174666f726d2d766f7465722e737667)](https://packagist.org/packages/nexara/api-platform-voter)[![Packagist Downloads](https://camo.githubusercontent.com/8a7c67aae756bdc5a45b63249dd4f16d1d37d4e2c054120a125924f3d5bd68f5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e65786172612f6170692d706c6174666f726d2d766f7465722e737667)](https://packagist.org/packages/nexara/api-platform-voter)[![License](https://camo.githubusercontent.com/642ef12c03a008dbd54e877a5e109775e64f0475455c87377507233f6f6607b0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6e65786172612f6170692d706c6174666f726d2d766f7465722e737667)](https://github.com/nexara-group/api-platform-voter/blob/main/LICENSE)

A Symfony bundle that enforces consistent voter-based authorization for API Platform 3 resources.

Features
--------

[](#features)

### Core Features

[](#core-features)

- ✅ **Opt-in security** per resource via `#[Secured]` attribute
- ✅ **Automatic CRUD mapping** to voter attributes (`{prefix}:list`, `{prefix}:create`, etc.)
- ✅ **Custom operation support** with explicit voter methods
- ✅ **UPDATE operations** receive both new and previous objects for comparison
- ✅ **Flexible configuration** with customizable prefixes and targeted voters
- ✅ **Type-safe** with PHP 8.1+ and strict types
- ✅ **Well-tested** with comprehensive test coverage

### Advanced Features (v0.3+)

[](#advanced-features-v03)

- 🧪 **Testing utilities** with role hierarchy support (`VoterTestTrait`, `SecurityBuilder`)
- ⚙️ **Flexible operation mapping** with configurable naming conventions
- 🔒 **Automatic custom provider security** with opt-in/opt-out configuration
- 🐛 **Debug tools** with voter chain visualization
- 📊 **Validation commands** for voter implementations
- 🔄 **Migration helpers** from native API Platform security
- 🌐 **GraphQL support** with field-level authorization
- 🏢 **Multi-tenancy** with automatic tenant context injection
- ⚡ **Performance optimizations** with lazy loading and caching
- 🛠️ **Maker command** with pre-defined templates

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

[](#requirements)

- PHP 8.1 or higher
- Symfony 6.4 or 7.0+
- API Platform 3.0+

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

[](#installation)

```
composer require nexara/api-platform-voter
```

The bundle will be automatically registered in `config/bundles.php`.

Quick Start
-----------

[](#quick-start)

### 1. Mark Your Resource as Protected

[](#1-mark-your-resource-as-protected)

Add the `#[Secured]` attribute to your API Platform resource:

```
use ApiPlatform\Metadata\ApiResource;
use Nexara\ApiPlatformVoter\Attribute\Secured;

#[ApiResource]
#[Secured(prefix: 'article', voter: ArticleVoter::class)]
class Article
{
    // Your entity properties...
}
```

### 2. Create a Voter

[](#2-create-a-voter)

Use the maker command to generate a voter:

```
php bin/console make:api-resource-voter
```

Or create one manually with **3 configuration modes** (v0.3+):

#### Mode 1: Auto-Configuration (Recommended)

[](#mode-1-auto-configuration-recommended)

```
namespace App\Security\Voter;

use App\Entity\Article;
use Nexara\ApiPlatformVoter\Voter\CrudVoter;
use Symfony\Bundle\SecurityBundle\Security;

final class ArticleVoter extends CrudVoter
{
    public function __construct(private readonly Security $security)
    {
        $this->autoConfigure(); // ✨ Zero config!
    }

    protected function canCreate(): bool
    {
        return $this->security->isGranted('ROLE_USER');
    }

    protected function canUpdate(mixed $object, mixed $previousObject): bool
    {
        return $object->getAuthor() === $this->security->getUser();
    }

    protected function canDelete(mixed $object): bool
    {
        return $this->security->isGranted('ROLE_ADMIN');
    }
}
```

#### Mode 2: Fluent Builder (Modern)

[](#mode-2-fluent-builder-modern)

```
final class ArticleVoter extends CrudVoter
{
    public function __construct(private readonly Security $security)
    {
        $this->configure()
            ->prefix('article')
            ->resource(Article::class)
            ->autoDiscoverOperations(); // Auto-finds can* methods
    }

    protected function canUpdate(mixed $object, mixed $previousObject): bool
    {
        return $object->getAuthor() === $this->security->getUser();
    }
}
```

#### Mode 3: Manual (Backward Compatible)

[](#mode-3-manual-backward-compatible)

```
final class ArticleVoter extends CrudVoter
{
    public function __construct(private readonly Security $security)
    {
        $this->setPrefix('article');
        $this->setResourceClasses(Article::class);
    }

    protected function canUpdate(mixed $object, mixed $previousObject): bool
    {
        return $object->getAuthor() === $this->security->getUser();
    }
}
```

### 3. That's It!

[](#3-thats-it)

Your API Platform resource is now protected by the voter. All CRUD operations will be automatically checked.

Operation Mapping
-----------------

[](#operation-mapping)

The bundle automatically maps API Platform operations to voter attributes:

OperationHTTP MethodVoter AttributeVoter MethodSubjectCollection GET`GET /articles``article:list``canList()``null`Collection POST`POST /articles``article:create``canCreate()`New objectItem GET`GET /articles/{id}``article:read``canRead($object)`ObjectItem PUT/PATCH`PUT /articles/{id}``article:update``canUpdate($new, $previous)``[$new, $previous]`Item DELETE`DELETE /articles/{id}``article:delete``canDelete($object)`ObjectCustom operation`POST /articles/{id}/publish``article:publish``canPublish($object, $previous)`Object or `[$new, $previous]`Custom Operations
-----------------

[](#custom-operations)

For custom operations, implement a method following the naming convention `can{OperationName}`:

```
#[ApiResource(
    operations: [
        new Post(
            uriTemplate: '/articles/{id}/publish',
            name: 'publish',
            // ... other config
        ),
    ]
)]
#[Secured(voter: ArticleVoter::class)]
class Article
{
    // ...
}
```

```
final class ArticleVoter extends CrudVoter
{
    // ... other methods

    protected function canPublish(mixed $object, mixed $previousObject): bool
    {
        // Custom logic for publish operation
        return $this->security->isGranted('ROLE_MODERATOR')
            && $object->getStatus() === 'draft';
    }
}
```

Voter Configuration Modes (v0.3+)
---------------------------------

[](#voter-configuration-modes-v03)

The unified `CrudVoter` supports 3 configuration modes:

### 1. Auto-Configuration (Zero Config)

[](#1-auto-configuration-zero-config)

```
final class ArticleVoter extends CrudVoter
{
    public function __construct(private readonly Security $security)
    {
        $this->autoConfigure(); // Reads from #[Secured] + VoterRegistry
    }
}
```

### 2. Fluent Builder (Modern API)

[](#2-fluent-builder-modern-api)

```
final class ArticleVoter extends CrudVoter
{
    public function __construct(private readonly Security $security)
    {
        $this->configure()
            ->prefix('article')
            ->resource(Article::class)
            ->operations('publish', 'archive')
            ->autoDiscoverOperations(); // Auto-finds can* methods
    }
}
```

### 3. Manual Configuration (Backward Compatible)

[](#3-manual-configuration-backward-compatible)

```
final class ArticleVoter extends CrudVoter
{
    public function __construct(private readonly Security $security)
    {
        $this->setPrefix('article');
        $this->setResourceClasses(Article::class);
    }
}
```

> **Migration from v0.2.x:** See [VOTER\_MIGRATION\_GUIDE.md](VOTER_MIGRATION_GUIDE.md)

Configuration
-------------

[](#configuration)

Create `config/packages/nexara_api_platform_voter.yaml`:

```
nexara_api_platform_voter:
    # Enable/disable the bundle (default: true)
    enabled: true

    # Enforce authorization for collection list operations (default: true)
    enforce_collection_list: true

    # Custom providers security (v0.3+)
    custom_providers:
        auto_secure: true  # Automatically secure all custom providers
        secure: []         # Explicitly secure specific providers
        skip: []           # Skip specific providers

    # Operation mapping configuration (v0.3+)
    operation_mapping:
        custom_operation_patterns:
            - '!^_api_'  # Exclude _api_* operations
        naming_convention: 'preserve'  # snake_case, camelCase, kebab-case, preserve
        normalize_names: false
        detect_by_uri: true  # Detect custom ops by URI pattern

    # Debug mode (v0.3+)
    debug: false
    debug_output: 'detailed'  # simple, detailed, json

    # Audit logging (v0.3+)
    audit:
        enabled: false
        level: 'all'  # all, denied_only, granted_only
        include_context: true
```

Attribute Options
-----------------

[](#attribute-options)

### `#[Secured]` Parameters

[](#secured-parameters)

- **`prefix`** (optional): Custom prefix for voter attributes. Defaults to lowercase resource class name.
- **`voter`** (optional): Specific voter class to use. When set, only this voter can grant access.

```
#[Secured(
    prefix: 'blog_post',
    voter: BlogPostVoter::class
)]
class Article { }
```

Advanced Usage
--------------

[](#advanced-usage)

### Accessing the User

[](#accessing-the-user)

Inject Symfony's `Security` service to access the current user:

```
public function __construct(
    private readonly Security $security,
) {
    $this->setPrefix('article');
    $this->setResourceClasses(Article::class);
}

protected function canUpdate(mixed $object, mixed $previousObject): bool
{
    $user = $this->security->getUser();
    return $user && $object->getAuthor() === $user;
}
```

### Comparing Previous and New Objects

[](#comparing-previous-and-new-objects)

For UPDATE operations, you receive both the new and previous state:

```
protected function canUpdate(mixed $object, mixed $previousObject): bool
{
    // Prevent changing the author
    if ($object->getAuthor() !== $previousObject->getAuthor()) {
        return $this->security->isGranted('ROLE_ADMIN');
    }

    return $object->getAuthor() === $this->security->getUser();
}
```

### Multiple Resource Classes

[](#multiple-resource-classes)

A single voter can handle multiple resource classes:

```
public function __construct()
{
    $this->setPrefix('content');
    $this->setResourceClasses(Article::class, BlogPost::class, Page::class);
}
```

### GraphQL Support

[](#graphql-support)

For GraphQL APIs, use `GraphQLCrudVoter` with field-level authorization:

```
use Nexara\ApiPlatformVoter\GraphQL\GraphQLCrudVoter;

final class ArticleVoter extends GraphQLCrudVoter
{
    protected function canAccessField(string $fieldName, mixed $object): bool
    {
        return match ($fieldName) {
            'email' => $this->security->isGranted('ROLE_ADMIN'),
            'internalNotes' => $object->getAuthor() === $this->security->getUser(),
            default => true,
        };
    }

    protected function canModifyField(string $fieldName, mixed $object, mixed $newValue): bool
    {
        return match ($fieldName) {
            'author' => $this->security->isGranted('ROLE_ADMIN'),
            'publishedAt' => $this->security->isGranted('ROLE_MODERATOR'),
            default => true,
        };
    }
}
```

### Multi-Tenancy

[](#multi-tenancy)

For multi-tenant applications, use `TenantAwareVoterTrait`:

```
use Nexara\ApiPlatformVoter\Voter\CrudVoter;
use Nexara\ApiPlatformVoter\MultiTenancy\TenantAwareVoterTrait;

final class ArticleVoter extends CrudVoter
{
    use TenantAwareVoterTrait;

    protected function canUpdate(mixed $object, mixed $previousObject): bool
    {
        // TenantContext is automatically injected
        if (!$this->belongsToCurrentTenant($object)) {
            return false;
        }

        return $object->getAuthor() === $this->security->getUser();
    }
}
```

### Debug &amp; Troubleshooting

[](#debug--troubleshooting)

Visualize voter decision chains:

```
use Nexara\ApiPlatformVoter\Debug\VoterChainVisualizer;

$visualizer = new VoterChainVisualizer($debugger);

// Text visualization
echo $visualizer->visualize('article:update');

// Tree visualization
echo $visualizer->visualizeAsTree('article:update');

// Summary
echo $visualizer->summarize('article:update');
```

Enable debug mode in configuration:

```
nexara_api_platform_voter:
    debug: true
    debug_output: 'detailed'
```

Testing
-------

[](#testing)

### Testing Your Voters

[](#testing-your-voters)

The bundle provides powerful testing utilities with full role hierarchy support:

#### Using VoterTestTrait

[](#using-votertesttrait)

```
use Nexara\ApiPlatformVoter\Testing\VoterTestTrait;
use PHPUnit\Framework\TestCase;

class ArticleVoterTest extends TestCase
{
    use VoterTestTrait;

    public function testModeratorCanPublish(): void
    {
        $user = $this->createUser(['ROLE_MODERATOR']);

        // Creates Security with proper role hierarchy
        $security = $this->createSecurityWithRoleHierarchy([
            'ROLE_ADMIN' => ['ROLE_MODERATOR', 'ROLE_USER'],
            'ROLE_MODERATOR' => ['ROLE_USER'],
        ], $user);

        $voter = new ArticleVoter($security);

        // Now $security->isGranted('ROLE_USER') returns true for MODERATOR
        $article = new Article();
        $this->assertTrue($voter->canPublish($article, null));
    }
}
```

#### Using SecurityBuilder

[](#using-securitybuilder)

```
use Nexara\ApiPlatformVoter\Testing\SecurityBuilder;

$security = SecurityBuilder::create()
    ->withRoleHierarchy([
        'ROLE_ADMIN' => ['ROLE_MODERATOR', 'ROLE_USER'],
        'ROLE_MODERATOR' => ['ROLE_USER'],
    ])
    ->withUser($user)
    ->build();

$voter = new ArticleVoter($security);
```

#### Using VoterTestCase

[](#using-votertestcase)

```
use Nexara\ApiPlatformVoter\Testing\VoterTestCase;

class ArticleVoterTest extends VoterTestCase
{
    protected function createVoter(): VoterInterface
    {
        return new ArticleVoter($this->createMock(Security::class));
    }

    public function testGrantsAccess(): void
    {
        $this->mockUser(['ROLE_USER']);
        $this->assertVoterGrants('article:create', new Article());
    }

    public function testDeniesAccess(): void
    {
        $this->mockAnonymousUser();
        $this->assertVoterDenies('article:delete', new Article());
    }
}
```

### Running Tests

[](#running-tests)

The bundle includes a comprehensive test suite:

```
# Run tests
composer test

# Run all quality checks
composer qa
```

Console Commands
----------------

[](#console-commands)

### Validate Voter Implementations

[](#validate-voter-implementations)

```
# Validate all voters
php bin/console voter:validate

# Validate specific voter
php bin/console voter:validate --voter=App\\Voter\\ArticleVoter

# Show detailed output
php bin/console voter:validate --detailed
```

Validates:

- ✅ CRUD method implementations
- ✅ Custom operation methods
- ✅ VoterRegistry registration
- ✅ `#[Secured]` attribute on resources
- ✅ Test coverage
- ✅ Method signatures

### Analyze Migration from Native Security

[](#analyze-migration-from-native-security)

```
php bin/console voter:analyze-migration
```

Provides:

- 📊 Analysis of resources with native security expressions
- 📋 Step-by-step migration plan
- ⏱️ Estimated migration time
- 🎯 Complexity assessment

Quality Assurance
-----------------

[](#quality-assurance)

This bundle maintains high code quality standards:

- **PHPStan** (level 8) for static analysis
- **ECS** for code style (PSR-12, Clean Code)
- **Rector** for automated refactoring
- **PHPUnit** for testing

```
composer phpstan      # Static analysis
composer ecs          # Check code style
composer ecs-fix      # Fix code style
composer rector       # Check refactoring opportunities
composer test         # Run tests
composer qa           # Run all checks
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.

Security
--------

[](#security)

If you discover a security vulnerability, please review our [Security Policy](SECURITY.md).

License
-------

[](#license)

This bundle is released under the [MIT License](LICENSE).

Credits
-------

[](#credits)

Developed and maintained by [Nexara s.r.o.](https://github.com/nexara-group)

Support
-------

[](#support)

- 📖 [Documentation](https://github.com/nexara-group/api-platform-voter)
- 🐛 [Issue Tracker](https://github.com/nexara-group/api-platform-voter/issues)
- 💬 [Discussions](https://github.com/nexara-group/api-platform-voter/discussions)

###  Health Score

34

—

LowBetter than 77% of packages

Maintenance80

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 84.4% 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

Every ~1 days

Total

12

Last Release

103d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/89c07d94f5adbb869cc78cb6845f0bccfeeee08468072baee91e49bd92785d32?d=identicon)[nexara-group](/maintainers/nexara-group)

---

Top Contributors

[![palofiser](https://avatars.githubusercontent.com/u/143612683?v=4)](https://github.com/palofiser "palofiser (27 commits)")[![nexara-group](https://avatars.githubusercontent.com/u/256993081?v=4)](https://github.com/nexara-group "nexara-group (5 commits)")

---

Tags

symfonybundlesecurityvoterapi-platform

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StyleECS

Type Coverage Yes

### Embed Badge

![Health badge](/badges/nexara-api-platform-voter/health.svg)

```
[![Health](https://phpackages.com/badges/nexara-api-platform-voter/health.svg)](https://phpackages.com/packages/nexara-api-platform-voter)
```

###  Alternatives

[sulu/sulu

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

1.3k1.3M152](/packages/sulu-sulu)[rezzza/security-bundle

Signed requests check

1753.6k](/packages/rezzza-security-bundle)[usu/scrypt-password-encoder-bundle

Scrypt password encoder for Symfony2

191.3k](/packages/usu-scrypt-password-encoder-bundle)

PHPackages © 2026

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