Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
ecourty/token-bundle
claude-code
cursor
symfony
security
bundle
doctrine
token
revocation
Install
php artisan boost:add-skill ecourty/token-bundle

Save this content to: AGENTS.md

---
package: ecourty/token-bundle
source_path: AGENTS.md
repo: https://github.com/EdouardCourty/token-bundle
---

# AGENTS.md - Coding Guidelines for AI Agents

## ๐ŸŽฏ Core Concept

**Token Bundle** (`ecourty/token-bundle`) is a Symfony bundle for managing secure, typed, and revocable tokens attached to any entity.

### Problem Solved

In most Symfony projects, developers repeatedly implement ad-hoc token systems for password resets, email verification, share links, and resource access. There is no generic, reusable solution that handles expiration, revocation, payload, and multi-use logic cleanly.

### Solution

A Doctrine-backed token system where any entity can become a **subject** of a token by implementing `TokenSubjectInterface`. Tokens are typed, carry an arbitrary JSON payload, support single-use or max-use limits, and can be revoked individually or in bulk.

---

## ๐Ÿ—๏ธ Architecture

### Overview

The bundle stores tokens in a single Doctrine-managed table. A `TokenManager` service handles all creation, consumption, and revocation logic, dispatching Symfony events on each action.

### Main Components

- **`Token` entity** โ€” Doctrine entity stored in `tokens` table with the following fields:
  - `id` (integer, auto-increment), `type` (string), `token` (unique string)
  - `subject_type` (FQCN), `subject_id` (string)
  - `payload` (json, nullable), `single_use` (bool)
  - `max_uses` (nullable int), `use_count` (int, default 0)
  - `expires_at` (datetime, **required** โ€” no permanent tokens), `consumed_at` (nullable datetime)
  - `revoked_at` (nullable datetime), `created_at` (datetime)
  - Composite index on `(subject_type, subject_id, type)` for efficient lookups

- **`TokenSubjectInterface`** โ€” Implemented by any entity that can be a token subject. Single method: `getTokenSubjectId(): string`. The subject FQCN + ID are stored to identify the entity.

- **`TokenManager`** โ€” Main service. Methods:
  - `create(string $type, TokenSubjectInterface $subject, string $expiresIn, bool $singleUse, ?int $maxUses, ?array $payload): Token`
  - `get(string $tokenString, string $type): Token`
  - `consume(string|Token $tokenOrString, ?string $type = null): Token`
  - `revoke(string $tokenString): void`
  - `revokeAll(TokenSubjectInterface $subject, ?string $type): int`
  - `findValid(TokenSubjectInterface $subject, string $type): ?Token`
  - `resolveSubject(Token $token): ?TokenSubjectInterface`

- **Events** (dispatched via Symfony EventDispatcher):
  - `TokenCreatedEvent`
  - `TokenConsumedEvent`
  - `TokenRevokedEvent`
  - `TokenAccessDeniedEvent` (when a `#[RequiresToken]` check fails)

- **`#[RequiresToken]` attribute** โ€” PHP attribute for controller methods/classes that validates a token before execution:
  - Reads token from request parameter (query/route/body) or a custom `TokenResolverInterface`
  - Stores validated `Token` entity in `$request->attributes->set('_token', $token)`
  - Throws `TokenAccessDeniedException` on failure
  - `TokenAccessDeniedListener` dispatches `TokenAccessDeniedEvent`, allowing response override via event listener

- **`TokenResolverInterface`** โ€” contract for custom token extraction from `Request` (e.g. from headers, cookies). Implementations are auto-tagged via `registerForAutoconfiguration`.

- **Console Command** โ€” `php bin/console token:purge` โ€” deletes expired and consumed tokens. Options: `--dry-run`, `--type=<type>`, `--before=<date>` (date cutoff for expired tokens, e.g. `-30 days`).

- **Bundle configuration** (`config/packages/token.yaml`):
  ```yaml
  token:
    token_length: 64  # min: 16
  ```

---

## ๐Ÿš€ Typical Use Cases

- **Password reset** โ€” single-use token, 1-hour TTL, sent by email
- **Email verification** โ€” single-use token, 24-hour TTL
- **Share link** โ€” multi-use token attached to a `Document` entity, no TTL or fixed expiry
- **Resource access** โ€” token with `max_uses: 10` giving limited access to a private resource
- **Temporary API access** โ€” token with payload carrying permissions

---

## ๐Ÿ’ก Design Patterns Used

- **Interface segregation** โ€” `TokenSubjectInterface` decouples any Doctrine entity from the bundle without inheritance
- **Event-driven** โ€” all side effects (logging, emails, alerts) are handled via Symfony events, not inline
- **Decorator-friendly** โ€” `TokenManager` can be decorated for custom behavior

---

## Project breakdown

```
src/
  Entity/
    Token.php                  # Doctrine entity (table: tokens)
  Attribute/
    RequiresToken.php          # PHP attribute for controller-level token validation
  Contract/
    TokenSubjectInterface.php  # Interface for subject entities
    TokenResolverInterface.php # Interface for custom token resolution from requests
  Repository/
    TokenRepository.php        # Queries, atomic increment, bulk revoke, purge
  Service/
    TokenManager.php           # Core service (create/get/consume/revoke/revokeAll/findValid/resolveSubject)
  Exception/
    AbstractTokenException.php # Base exception (extends RuntimeException)
    TokenNotFoundException.php
    TokenExpiredException.php
    TokenAlreadyConsumedException.php
    TokenRevokedException.php
    TokenMaxUsesReachedException.php
    TokenAccessDeniedException.php # Thrown when #[RequiresToken] check fails
  Event/
    AbstractTokenEvent.php     # Base event (carries Token)
    TokenCreatedEvent.php
    TokenConsumedEvent.php
    TokenRevokedEvent.php
    TokenAccessDeniedEvent.php # Dispatched when a #[RequiresToken] check fails
  EventListener/
    RequiresTokenListener.php  # kernel.controller_arguments โ€” validates token, stores in request attributes
    TokenAccessDeniedListener.php # kernel.exception โ€” dispatches TokenAccessDeniedEvent, allows response override
  Command/
    PurgeTokensCommand.php     # token:purge (--dry-run, --type)
  DependencyInjection/
    Configuration.php          # token_length config node
    TokenExtension.php         # PrependExtensionInterface for Doctrine mapping + autoconfigure TokenResolverInterface
  Resources/
    config/
      services.php
  TokenBundle.php
```

```
tests/
  App/
    Entity/TestUser.php        # Fixture entity implementing TokenSubjectInterface
    Controller/TokenTestController.php # Test controller with #[RequiresToken] routes
    Resolver/HeaderTokenResolver.php   # Test TokenResolverInterface implementation
    TestKernel.php             # Minimal test kernel (Framework + Doctrine + TokenBundle, no hacks)
    bin/console
    config/
      services.php             # DI defaults only (no compiler pass, no public overrides)
      packages/
        framework.php
        doctrine.php           # SQLite in-memory + native lazy objects
      routes.php
  Unit/
    Attribute/RequiresTokenTest.php
    Entity/TokenTest.php
    Event/TokenAccessDeniedEventTest.php
  Integration/
    IntegrationTestCase.php    # Base class: boot kernel, wire TokenRepository directly, create schema, teardown
    Repository/TokenRepositoryTest.php
    Service/TokenManagerTest.php
  Functional/
    Command/PurgeTokensCommandTest.php
    EventListener/RequiresTokenListenerTest.php
```

**IMPORTANT**: This section should evolve with the project. When a new feature is created, updated or removed, this section should too.

## ๐Ÿงช Testing

This bundle should be covered by unit, integration and functional tests.
The tests are located in the `tests/{Unit|Integration|Functional}` folder.
Unit tests can use mocks or stubs if needed.

### Testing private bundle services

Symfony's compiler inlines private services with a single consumer (e.g. `TokenManager` is only consumed by `PurgeTokensCommand`). Once inlined, these services are inaccessible via the test container โ€” even with `framework.test: true`. Do NOT add a compiler pass to the test kernel to work around this.

Instead, **instantiate bundle services directly in integration tests**:
- `EntityManagerInterface` and `ManagerRegistry` are public Doctrine services โ€” get them from the container.
- `TokenRepository` is constructed with `ManagerRegistry` directly.
- `TokenManager` is instantiated with its dependencies, using a dedicated `EventDispatcher` instance so tests can track dispatched events.

This mirrors exactly how a real application wires these services via DI.

---

## Remarks & Guidelines

### General

- NEVER commit or push the git repository.
- When unsure about something, you MUST ask the user for clarification. Same goes it the user request is unclear.
- When facing a problem that has an easy "hacky" solution, and a more robust but more difficult to implement one, always choose the robust one:
  - Easy hacky fixes become technical debt, and can lead to issues down the road
  - Robust solutions means the project will remain serious and well-built.
- ALWAYS write tests for the important components. Better safe than sorry!
- Do NOT write ANY type documentation unless explicitly asked.
- Once a feature is complete, update the @README.md and @AGENTS.md accordingly.
- The @README.md file should consist of a project overview for end-users, not a technical explanation of the project. It should include:
  - Table of contents
  - Quick start / Installation
  - Core features
  - Configuration reference
  - Usage
  - Development / Contribution guidelines

### Symfony Bundles

- Symfony bundles are meant to be re-used and integrated in other Symfony projects. When developing features, keep this in mind.  
- Architecture, naming, design, extensibility and easiness to install and use should be key priorities to consider when developing this project.

## ๐Ÿ“š References

- **Source code**: `/src`
- **Tests**: `/tests`
- **README**: User documentation
- **Symfony Docs**: https://symfony.com/doc/current/bundles.html

package: ecourty/token-bundle source_path: AGENTS.md repo: https://github.com/EdouardCourty/token-bundle

AGENTS.md - Coding Guidelines for AI Agents

๐ŸŽฏ Core Concept

Token Bundle (ecourty/token-bundle) is a Symfony bundle for managing secure, typed, and revocable tokens attached to any entity.

Problem Solved

In most Symfony projects, developers repeatedly implement ad-hoc token systems for password resets, email verification, share links, and resource access. There is no generic, reusable solution that handles expiration, revocation, payload, and multi-use logic cleanly.

Solution

A Doctrine-backed token system where any entity can become a subject of a token by implementing TokenSubjectInterface. Tokens are typed, carry an arbitrary JSON payload, support single-use or max-use limits, and can be revoked individually or in bulk.


๐Ÿ—๏ธ Architecture

Overview

The bundle stores tokens in a single Doctrine-managed table. A TokenManager service handles all creation, consumption, and revocation logic, dispatching Symfony events on each action.

Main Components

  • Token entity โ€” Doctrine entity stored in tokens table with the following fields:

    • id (integer, auto-increment), type (string), token (unique string)
    • subject_type (FQCN), subject_id (string)
    • payload (json, nullable), single_use (bool)
    • max_uses (nullable int), use_count (int, default 0)
    • expires_at (datetime, required โ€” no permanent tokens), consumed_at (nullable datetime)
    • revoked_at (nullable datetime), created_at (datetime)
    • Composite index on (subject_type, subject_id, type) for efficient lookups
  • TokenSubjectInterface โ€” Implemented by any entity that can be a token subject. Single method: getTokenSubjectId(): string. The subject FQCN + ID are stored to identify the entity.

  • TokenManager โ€” Main service. Methods:

    • create(string $type, TokenSubjectInterface $subject, string $expiresIn, bool $singleUse, ?int $maxUses, ?array $payload): Token
    • get(string $tokenString, string $type): Token
    • consume(string|Token $tokenOrString, ?string $type = null): Token
    • revoke(string $tokenString): void
    • revokeAll(TokenSubjectInterface $subject, ?string $type): int
    • findValid(TokenSubjectInterface $subject, string $type): ?Token
    • resolveSubject(Token $token): ?TokenSubjectInterface
  • Events (dispatched via Symfony EventDispatcher):

    • TokenCreatedEvent
    • TokenConsumedEvent
    • TokenRevokedEvent
    • TokenAccessDeniedEvent (when a #[RequiresToken] check fails)
  • #[RequiresToken] attribute โ€” PHP attribute for controller methods/classes that validates a token before execution:

    • Reads token from request parameter (query/route/body) or a custom TokenResolverInterface
    • Stores validated Token entity in $request->attributes->set('_token', $token)
    • Throws TokenAccessDeniedException on failure
    • TokenAccessDeniedListener dispatches TokenAccessDeniedEvent, allowing response override via event listener
  • TokenResolverInterface โ€” contract for custom token extraction from Request (e.g. from headers, cookies). Implementations are auto-tagged via registerForAutoconfiguration.

  • Console Command โ€” php bin/console token:purge โ€” deletes expired and consumed tokens. Options: --dry-run, --type=<type>, --before=<date> (date cutoff for expired tokens, e.g. -30 days).

  • Bundle configuration (config/packages/token.yaml):

    token:
      token_length: 64  # min: 16
    

๐Ÿš€ Typical Use Cases

  • Password reset โ€” single-use token, 1-hour TTL, sent by email
  • Email verification โ€” single-use token, 24-hour TTL
  • Share link โ€” multi-use token attached to a Document entity, no TTL or fixed expiry
  • Resource access โ€” token with max_uses: 10 giving limited access to a private resource
  • Temporary API access โ€” token with payload carrying permissions

๐Ÿ’ก Design Patterns Used

  • Interface segregation โ€” TokenSubjectInterface decouples any Doctrine entity from the bundle without inheritance
  • Event-driven โ€” all side effects (logging, emails, alerts) are handled via Symfony events, not inline
  • Decorator-friendly โ€” TokenManager can be decorated for custom behavior

Project breakdown

src/
  Entity/
    Token.php                  # Doctrine entity (table: tokens)
  Attribute/
    RequiresToken.php          # PHP attribute for controller-level token validation
  Contract/
    TokenSubjectInterface.php  # Interface for subject entities
    TokenResolverInterface.php # Interface for custom token resolution from requests
  Repository/
    TokenRepository.php        # Queries, atomic increment, bulk revoke, purge
  Service/
    TokenManager.php           # Core service (create/get/consume/revoke/revokeAll/findValid/resolveSubject)
  Exception/
    AbstractTokenException.php # Base exception (extends RuntimeException)
    TokenNotFoundException.php
    TokenExpiredException.php
    TokenAlreadyConsumedException.php
    TokenRevokedException.php
    TokenMaxUsesReachedException.php
    TokenAccessDeniedException.php # Thrown when #[RequiresToken] check fails
  Event/
    AbstractTokenEvent.php     # Base event (carries Token)
    TokenCreatedEvent.php
    TokenConsumedEvent.php
    TokenRevokedEvent.php
    TokenAccessDeniedEvent.php # Dispatched when a #[RequiresToken] check fails
  EventListener/
    RequiresTokenListener.php  # kernel.controller_arguments โ€” validates token, stores in request attributes
    TokenAccessDeniedListener.php # kernel.exception โ€” dispatches TokenAccessDeniedEvent, allows response override
  Command/
    PurgeTokensCommand.php     # token:purge (--dry-run, --type)
  DependencyInjection/
    Configuration.php          # token_length config node
    TokenExtension.php         # PrependExtensionInterface for Doctrine mapping + autoconfigure TokenResolverInterface
  Resources/
    config/
      services.php
  TokenBundle.php
tests/
  App/
    Entity/TestUser.php        # Fixture entity implementing TokenSubjectInterface
    Controller/TokenTestController.php # Test controller with #[RequiresToken] routes
    Resolver/HeaderTokenResolver.php   # Test TokenResolverInterface implementation
    TestKernel.php             # Minimal test kernel (Framework + Doctrine + TokenBundle, no hacks)
    bin/console
    config/
      services.php             # DI defaults only (no compiler pass, no public overrides)
      packages/
        framework.php
        doctrine.php           # SQLite in-memory + native lazy objects
      routes.php
  Unit/
    Attribute/RequiresTokenTest.php
    Entity/TokenTest.php
    Event/TokenAccessDeniedEventTest.php
  Integration/
    IntegrationTestCase.php    # Base class: boot kernel, wire TokenRepository directly, create schema, teardown
    Repository/TokenRepositoryTest.php
    Service/TokenManagerTest.php
  Functional/
    Command/PurgeTokensCommandTest.php
    EventListener/RequiresTokenListenerTest.php

IMPORTANT: This section should evolve with the project. When a new feature is created, updated or removed, this section should too.

๐Ÿงช Testing

This bundle should be covered by unit, integration and functional tests. The tests are located in the tests/{Unit|Integration|Functional} folder. Unit tests can use mocks or stubs if needed.

Testing private bundle services

Symfony's compiler inlines private services with a single consumer (e.g. TokenManager is only consumed by PurgeTokensCommand). Once inlined, these services are inaccessible via the test container โ€” even with framework.test: true. Do NOT add a compiler pass to the test kernel to work around this.

Instead, instantiate bundle services directly in integration tests:

  • EntityManagerInterface and ManagerRegistry are public Doctrine services โ€” get them from the container.
  • TokenRepository is constructed with ManagerRegistry directly.
  • TokenManager is instantiated with its dependencies, using a dedicated EventDispatcher instance so tests can track dispatched events.

This mirrors exactly how a real application wires these services via DI.


Remarks & Guidelines

General

  • NEVER commit or push the git repository.
  • When unsure about something, you MUST ask the user for clarification. Same goes it the user request is unclear.
  • When facing a problem that has an easy "hacky" solution, and a more robust but more difficult to implement one, always choose the robust one:
    • Easy hacky fixes become technical debt, and can lead to issues down the road
    • Robust solutions means the project will remain serious and well-built.
  • ALWAYS write tests for the important components. Better safe than sorry!
  • Do NOT write ANY type documentation unless explicitly asked.
  • Once a feature is complete, update the @README.md and @AGENTS.md accordingly.
  • The @README.md file should consist of a project overview for end-users, not a technical explanation of the project. It should include:
    • Table of contents
    • Quick start / Installation
    • Core features
    • Configuration reference
    • Usage
    • Development / Contribution guidelines

Symfony Bundles

  • Symfony bundles are meant to be re-used and integrated in other Symfony projects. When developing features, keep this in mind.
  • Architecture, naming, design, extensibility and easiness to install and use should be key priorities to consider when developing this project.

๐Ÿ“š References

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
besmartand-pro/php-quality-config
sentix/ai-chatbot
codifyo/ts-generator-bundle
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor