Weave 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/platform-parameter-bundle
claude-code
cursor
symfony-bundle
doctrine-orm
psr-6-cache
dependency-injection
uuid
enums
validation

Platform Parameter Bundle is a Symfony bundle for managing global platform parameters stored in the database with type-safe access and caching system.

Install
php artisan boost:add-skill ecourty/platform-parameter-bundle

Save this content to: AGENTS.md

# AGENTS.md - Platform Parameter Bundle

## ๐ŸŽฏ Core Concept

**Platform Parameter Bundle** is a Symfony bundle for managing **global platform parameters** stored in the database with type-safe access and caching system.

### Problem Solved

- Avoid hard-coded values in code
- Enable dynamic configuration without redeployment
- Centralize business parameters (e.g., limits, feature flags, external URLs)
- Provide a type-safe API to prevent type errors

### Solution

A simple and robust system: **Doctrine Entity + Service Provider + PSR-6 Cache**

---

## ๐Ÿ—๏ธ Architecture

### Overview

The architecture follows a layered approach:

**Application Layer** (Controllers, Services, Commands) interacts with the **PlatformParameterProvider** service through dependency injection. This provider implements the `PlatformParameterProviderInterface` and acts as the single entry point for parameter access.

The provider coordinates three underlying components:
- **PSR-6 Cache**: First line of defense for fast parameter retrieval
- **Doctrine ORM**: Database persistence layer for parameter storage
- **Validator Logic**: Type conversion and validation mechanisms

All parameters are stored in the `platform_parameter` database table and accessed through the ORM.

### Main Components

#### 1. **AbstractPlatformParameter** (Entity)
- **MappedSuperclass** allowing extension
- Fields: `id`, `key`, `value`, `type`, `label`, `description`, timestamps
- Auto-generated UUID
- Lifecycle callbacks for `updatedAt`

```php
// src/Entity/AbstractPlatformParameter.php
#[ORM\MappedSuperclass]
abstract class AbstractPlatformParameter
{
    private Uuid $id;
    private string $key;      // Unique identifier
    private string $value;    // Stored as TEXT
    private ParameterType $type;
    private string $label;
    private ?string $description;
    private \DateTimeImmutable $createdAt;
    private \DateTimeImmutable $updatedAt;
}
```

**Design Choice**: `MappedSuperclass` allows projects to extend the entity to add custom fields (category, icon, sortOrder, etc.)

#### 2. **PlatformParameter** (Concrete Entity)
- Inherits from `AbstractPlatformParameter`
- Standard Doctrine entity used by default
- Table `platform_parameter`

```php
#[ORM\Entity]
#[ORM\Table(name: 'platform_parameter')]
class PlatformParameter extends AbstractPlatformParameter
{
    // Ready to use, no custom fields
}
```

#### 3. **ParameterType** (Enum)
7 supported types with built-in validation:

```php
enum ParameterType: string
{
    case STRING   = 'string';    // Simple text
    case INTEGER  = 'integer';   // Integer number
    case BOOLEAN  = 'boolean';   // true/false/1/0/yes/no
    case JSON     = 'json';      // JSON object/array
    case LIST     = 'list';      // Multi-line or delimiter-separated
    case FLOAT    = 'float';     // Decimal number
    case DATETIME = 'datetime';  // Date/time with multiple formats
}
```

#### 4. **PlatformParameterProviderInterface** (Contract)
Service interface exposing the public API:

```php
interface PlatformParameterProviderInterface
{
    public function getString(string $key, ?string $default = null): string;
    public function getInt(string $key, ?int $default = null): int;
    public function getBool(string $key, ?bool $default = null): bool;
    public function getJson(string $key, ?array $default = null): array;
    public function getList(string $key, ?array $default = null, string $separator = "\n"): array;
    public function getFloat(string $key, ?float $default = null): float;
    public function getDateTime(string $key, ?\DateTimeImmutable $default = null, ?string $format = null): \DateTimeImmutable;
    public function has(string $key): bool;
    public function clearCache(?string $key = null): void;
}
```

**Design Pattern**: Interface injection for testability and decoupling

#### 5. **PlatformParameterProvider** (Service - READ)
Provider implementation with business logic:

**Responsibilities**:
- Fetch from cache or DB
- Type validation and conversion
- Error handling (exceptions or defaults)
- Smart cache invalidation

**Execution Flow**:

When a parameter is requested, the provider follows this sequence:
1. **Cache Check**: Look for the parameter in cache using the format `{prefix}.{key}`
2. **Cache HIT**: If found in cache, return the cached value immediately
3. **Cache MISS**: If not in cache, query the database using Doctrine EntityManager
4. **Database Result**: If found in DB, store it in cache and return the value
5. **Not Found**: If the parameter doesn't exist, either return the provided default value or throw a `ParameterNotFoundException`

#### 6. **PlatformParameterWriter** (Service - WRITE)
Writer service for updating parameter values from code:

**Responsibilities**:
- Update existing parameter values with type safety
- Delete parameters by key
- Type validation (must match parameter's declared type)
- Value conversion (PHP type โ†’ string for database)
- Exception handling (not found or type mismatch)

**Available Methods**:
```php
interface PlatformParameterWriterInterface
{
    public function setString(string $key, string $value): void;
    public function setInt(string $key, int $value): void;
    public function setBool(string $key, bool $value): void;
    public function setJson(string $key, array $value): void;
    public function setList(string $key, array $value, string $separator = "\n"): void;
    public function setFloat(string $key, float $value): void;
    public function setDateTime(string $key, \DateTimeImmutable $value, ?string $format = null): void;
    public function delete(string $key): void;
}
```

**Behavior**:
- All `set*()` methods throw `ParameterNotFoundException` if the parameter doesn't exist
- All `set*()` methods throw `ParameterTypeMismatchException` if the parameter type doesn't match the method type
- `delete()` method throws `ParameterNotFoundException` if the parameter doesn't exist
- Cache clearing is automatic via Doctrine listener
- Events (`PlatformParameterUpdatedEvent`, `PlatformParameterDeletedEvent`) are dispatched automatically
- **No auto-creation**: Writer only updates existing parameters (creation must be done via Doctrine entities)

**Design Principle**: CQRS-style separation: Provider = READ, Writer = WRITE

#### 7. **Symfony Configuration**
```yaml
# config/packages/platform_parameter.yaml
platform_parameter:
    entity_class: 'Ecourty\PlatformParameterBundle\Entity\PlatformParameter'
    cache_ttl: 3600
    cache_key_prefix: 'platform_parameter'
    clear_cache_on_parameter_update: true  # Auto-clear cache via Doctrine listener
    cache_adapter: null  # Optional custom cache service
```

---

## ๐Ÿ”„ Data Flow

### Reading a Parameter

When reading a parameter, for example `$provider->getInt('max_uploads', 10)`, the system follows these steps:

1. **Key Generation**: The provider generates a cache key by prefixing the parameter key (e.g., `'platform_parameter.max_uploads'`)
2. **Cache Lookup**: The PSR-6 cache is queried with this key
3. **Cache Hit Path**: If the item exists in cache, the cached `AbstractPlatformParameter` object is returned directly
4. **Cache Miss Path**: If not in cache, Doctrine's EntityManager performs a `findOneBy(['key' => 'max_uploads'])` query
5. **Database Hit**: If found in the database, the parameter is cached for future requests and the value is returned
6. **Database Miss**: If not found in the database, the default value (10) is returned, or a `ParameterNotFoundException` is thrown if no default was provided

### Writing/Updating Parameters

#### Method 1: PlatformParameterWriter (Recommended for Updates)

The bundle provides a **PlatformParameterWriter** service for type-safe parameter updates from code:

```php
// In a service or controller
public function __construct(
    private PlatformParameterWriterInterface $writer,
) {
}

public function updateParameter(): void
{
    // Update existing parameters with type-safe methods
    $this->writer->setInt('max_uploads', 100);
    $this->writer->setBool('maintenance_mode', true);
    $this->writer->setString('site_name', 'My Site');
    $this->writer->setJson('config', ['key' => 'value']);
    $this->writer->setList('emails', ['[email protected]', '[email protected]']);
    $this->writer->setFloat('rate', 3.14);
    $this->writer->setDateTime('last_sync', new \DateTimeImmutable());
    
    // Delete a parameter
    $this->writer->delete('old_parameter');
}
```

**Important**: The Writer can only **update existing parameters**. It throws `ParameterNotFoundException` if the parameter doesn't exist, and `ParameterTypeMismatchException` if you try to set a value with the wrong type (e.g., calling `setInt()` on a STRING parameter).

**Why no auto-creation?** Because if you extend `AbstractPlatformParameter` with custom fields (category, icon, etc.), the Writer doesn't know how to populate those fields. Parameter creation must be done explicitly via Doctrine entities.

#### Method 2: Direct Doctrine Entity Manipulation (For Creation)

To **create** a parameter, you must work directly with Doctrine entities:

```php
// In a service or controller
$parameter = new PlatformParameter();
$parameter->setKey('max_uploads');
$parameter->setValue('20');
$parameter->setType(ParameterType::INTEGER);
$parameter->setLabel('Maximum Uploads');
$parameter->setDescription('Maximum number of files per upload');

$em->persist($parameter);
$em->flush();
```

**By default**, cache is automatically cleared via a Doctrine listener when parameters are created, updated, or deleted. You can disable this behavior by setting `clear_cache_on_parameter_update: false` in configuration if you prefer manual cache management.

### Event System

The bundle dispatches **Symfony events** whenever parameters are created, updated, or deleted. This allows host projects to easily react to parameter changes without configuring Doctrine listeners.

**Available Events**:

1. **PlatformParameterCreatedEvent** - Dispatched after a parameter is created
2. **PlatformParameterUpdatedEvent** - Dispatched after a parameter is updated (includes old and new values)
3. **PlatformParameterDeletedEvent** - Dispatched after a parameter is deleted

**Example: React to parameter changes**

```php
// src/EventSubscriber/ParameterChangeSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Ecourty\PlatformParameterBundle\Event\PlatformParameterUpdatedEvent;
use Ecourty\PlatformParameterBundle\Event\PlatformParameterCreatedEvent;
use Ecourty\PlatformParameterBundle\Event\PlatformParameterDeletedEvent;

class ParameterChangeSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private SlackNotifier $slack,
        private LoggerInterface $logger,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            PlatformParameterCreatedEvent::class => 'onParameterCreated',
            PlatformParameterUpdatedEvent::class => 'onParameterUpdated',
            PlatformParameterDeletedEvent::class => 'onParameterDeleted',
        ];
    }

public function onParameterCreated(PlatformParameterCreatedEvent $event): void
    {
        $parameter = $event->getParameter();
        $this->logger->info('Parameter created', [
            'key' => $parameter->getKey(),
            'value' => $parameter->getValue(),
        ]);
    }

    public function onParameterUpdated(PlatformParameterUpdatedEvent $event): void
    {
        $parameter = $event->getParameter();
        
        // Send Slack notification for critical parameters
        if ($parameter->getKey() === 'maintenance_mode') {
            $this->slack->send(sprintf(
                'Maintenance mode changed from "%s" to "%s"',
                $event->getOldValue(),
                $event->getNewValue()
            ));
        }
    }

public function onParameterDeleted(PlatformParameterDeletedEvent $event): void
    {
        $parameter = $event->getParameter();
        $this->logger->warning('Parameter deleted', [
            'key' => $parameter->getKey(),
            'last_value' => $parameter->getValue(),
        ]);
    }
}
```

**Event Details**:

- **PlatformParameterCreatedEvent**: Contains the newly created parameter entity
- **PlatformParameterUpdatedEvent**: Contains the parameter entity, old value (string), and new value (string)
- **PlatformParameterDeletedEvent**: Contains the parameter entity (before deletion)

**Events are always dispatched**, regardless of the `clear_cache_on_parameter_update` configuration setting. They work with:
- Console commands (`platform-parameter:set`, `platform-parameter:delete`)
- Direct Doctrine operations (`$em->persist()`, `$em->flush()`, `$em->remove()`)
- EasyAdmin CRUD operations

**Cache Management**: By default, cache is automatically cleared when parameters change. You can disable this behavior by setting `clear_cache_on_parameter_update: false` in configuration, but events will still be dispatched, allowing you to implement custom cache invalidation logic via event subscribers.

---

## ๐Ÿ’ก Design Patterns Used

### 1. **Provider Pattern**
The `PlatformParameterProvider` acts as a uniform facade for retrieving parameters.

### 2. **Type Safety**
Each method returns a strict PHP 8.3 type, preventing type errors.

### 3. **Fail-Safe with Defaults**
```php
$value = $provider->getInt('unknown_key', 100); // Returns 100 if not found
$value = $provider->getInt('unknown_key');      // Throws ParameterNotFoundException
```

### 4. **Cache-Aside (Lazy Loading)**
The cache is checked first, the DB is only consulted in case of MISS.

### 5. **Tagged Cache Support**
If `TagAwareCacheInterface` is available, the bundle uses tags for efficient invalidation.

```php
// Clear all parameters
$provider->clearCache();

// With TagAwareCache: 1 operation
โ†’ invalidateTags(['platform_parameter'])

// Without TagAwareCache: N operations (fetch all + delete each)
โ†’ findAll() โ†’ deleteItem() for each key
```

### 6. **Template Method (Extension Point)**
`AbstractPlatformParameter` allows inheritance for extension without modifying the core.

---

## ๐Ÿš€ Typical Use Cases

### 1. Feature Flags
```php
if ($provider->getBool('feature_new_dashboard', false)) {
    return $this->render('dashboard_v2.html.twig');
}
```

### 2. Dynamic Limits
```php
$maxFileSize = $provider->getInt('max_upload_size_mb', 10) * 1024 * 1024;
if ($file->getSize() > $maxFileSize) {
    throw new UploadException('File too large');
}
```

### 3. External Configuration
```php
$apiConfig = $provider->getJson('stripe_config', [
    'public_key' => '',
    'secret_key' => '',
    'webhook_url' => '',
]);

$stripe = new StripeClient($apiConfig['secret_key']);
```

### 4. Dynamic Lists
```php
$allowedEmails = $provider->getList('beta_testers_emails', []);
if (!in_array($user->getEmail(), $allowedEmails)) {
    throw new AccessDeniedException();
}
```

### 5. Maintenance Mode
```php
// In an EventSubscriber
if ($provider->getBool('maintenance_mode', false)) {
    throw new ServiceUnavailableHttpException(3600, 'Under maintenance');
}
```

---

## ๐Ÿงฉ Extension: Custom Entity

### Why Extend?
To add custom metadata: category, sort order, icon, tags, etc.

### How?

**Step 1**: Create a custom entity
```php
namespace App\Entity;

#[ORM\Entity]
#[ORM\Table(name: 'platform_parameter')]
class CustomPlatformParameter extends AbstractPlatformParameter
{
    #[ORM\Column(type: 'string', length: 100, nullable: true)]
    private ?string $category = null;

#[ORM\Column(type: 'integer', nullable: true)]
    private ?int $sortOrder = null;

    // Getters/setters...
}
```

**Step 2**: Configure the bundle
```yaml
platform_parameter:
    entity_class: 'App\Entity\CustomPlatformParameter'
```

**Step 3**: Migration
```bash
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
```

โœ… **The provider continues to work as is** - autowiring automatically uses your custom entity.

---

## ๐ŸŽจ EasyAdmin Integration (Optional)

The bundle provides a **ready-to-use CRUD controller** if EasyAdmin is installed.

### Usage

```php
// src/Controller/Admin/PlatformParameterCrudController.php
namespace App\Controller\Admin;

use Ecourty\PlatformParameterBundle\Controller\PlatformParameterCrudController as BaseCrudController;

class PlatformParameterCrudController extends BaseCrudController
{
    // Ready to use!
}
```

### Included Features
- Complete CRUD with validation
- Colored badges for types
- Help text on each field
- Role-based permissions
- Truncated values in list view

---

## โšก Performance & Caching

### Cache Strategy

1. **Cache Key Format**: `{prefix}.{parameter_key}` (e.g., `platform_parameter.max_uploads`)
2. **Configurable TTL**: Default 3600s (1h)
3. **Per-parameter cache**: Each key is cached individually
4. **Granular invalidation**: Clear by key or globally

### Optimizations

#### Using TagAwareCache
```yaml
# config/packages/cache.yaml
framework:
    cache:
        pools:
            platform_parameter.cache:
                adapter: cache.adapter.redis.tag_aware
                default_lifetime: 3600
```

Benefits:
- Bulk invalidation in O(1) instead of O(n)
- Avoids fetching all parameters during `clearCache()`

#### Cache Warmup (Optional)
If you have many critical parameters:

```php
// Command or EventListener
foreach ($criticalKeys as $key) {
    try {
        $provider->getString($key);
    } catch (ParameterNotFoundException) {
        // Log missing parameter
    }
}
```

---

## ๐Ÿ›ก๏ธ Error Handling

### Exceptions

#### ParameterNotFoundException
Thrown when a parameter doesn't exist and no default is provided.

```php
try {
    $value = $provider->getString('missing_key');
} catch (ParameterNotFoundException $e) {
    // Log or handle
    $value = 'fallback';
}
```

#### InvalidArgumentException
Thrown during type conversion if the value is invalid.

```php
// DB value: "not_a_number"
$provider->getInt('bad_value'); // throws InvalidArgumentException
```

### Best Practices

1. **Always provide a default in production**
```php
// โŒ Risky
$limit = $provider->getInt('rate_limit');

// โœ… Safe
$limit = $provider->getInt('rate_limit', 100);
```

2. **Validate in dev, default in prod**
```php
if ($this->environment === 'dev' && !$provider->has('critical_param')) {
    throw new \RuntimeException('Missing critical parameter');
}
$value = $provider->getString('critical_param', 'default');
```

---

## ๐Ÿงช Testing

### Mocking the Provider

```php
use Ecourty\PlatformParameterBundle\Contract\PlatformParameterProviderInterface;

class MyServiceTest extends TestCase
{
    public function testWithMockProvider(): void
    {
        $provider = $this->createMock(PlatformParameterProviderInterface::class);
        $provider->method('getInt')
            ->willReturnMap([
                ['max_uploads', null, 20],
                ['timeout', null, 30],
            ]);

        $service = new MyService($provider);
        // Test service behavior
    }
}
```

### Functional Tests

```php
// Create test parameters
$parameter = new PlatformParameter();
$parameter->setKey('test_key');
$parameter->setValue('42');
$parameter->setType(ParameterType::INTEGER);

$this->entityManager->persist($parameter);
$this->entityManager->flush();

// Test provider
$result = $this->parameterProvider->getInt('test_key');
$this->assertSame(42, $result);
```

---

## ๐Ÿ“‹ Developer Checklist

### Adding a New Parameter

1. โœ… Create the entity in DB (manually or via EasyAdmin)
2. โœ… Define: key, value, type, label, description
3. โœ… Test access via provider
4. โœ… Document the parameter in the project (README, confluence, etc.)

### Modifying a Parameter

1. โœ… Update in DB via EasyAdmin or SQL
2. โœ… **Clear the cache**: `$provider->clearCache('key')` or via console
3. โœ… Verify that the new value is active

### Debugging an Issue

1. โ“ Does the parameter exist in DB? โ†’ `SELECT * FROM platform_parameter WHERE key = '...'`
2. โ“ Is the cache stale? โ†’ `$provider->clearCache('key')` or `php bin/console platform-parameter:cache:clear key`
3. โ“ Is the type correct? โ†’ Check `type` column and called method
4. โ“ Is there a silent exception? โ†’ Check logs

---

## ๐ŸŽ›๏ธ CLI Commands

The bundle provides 5 Symfony commands for managing parameters via the console.

### 1. List all parameters

```bash
php bin/console platform-parameter:list
```

**Features**:
- Displays a formatted table with columns: Key, Value, Type, Label
- Values longer than 50 characters are truncated with "..."
- Shows a warning if no parameters exist
- Output: "Found X parameter(s)."

**Use case**: Quick overview of all platform parameters

---

### 2. Get parameter details

```bash
php bin/console platform-parameter:get <key>
```

**Arguments**:
- `key` (required): The parameter key to display

**Features**:
- Shows all metadata: ID (UUID), Key, Value (full), Type, Label, Description, Created At, Updated At
- Returns error if parameter doesn't exist

**Examples**:
```bash
php bin/console platform-parameter:get max_uploads
php bin/console platform-parameter:get site_name
```

**Use case**: Inspect complete parameter details including timestamps

---

### 3. Update a parameter value

```bash
php bin/console platform-parameter:set <key> <value>
```

**Arguments**:
- `key` (required): The parameter key (must exist)
- `value` (required): The new parameter value

**Behavior**:
- Updates the value of an existing parameter
- Automatically detects parameter type and converts value accordingly
- Cache is automatically cleared
- Events are automatically dispatched
- Returns error if parameter doesn't exist

**Examples**:

```bash
# Update integer parameter
php bin/console platform-parameter:set max_uploads 50

# Update boolean parameter
php bin/console platform-parameter:set maintenance_mode true

# Update string parameter
php bin/console platform-parameter:set site_name "My New Site"

# Update JSON parameter
php bin/console platform-parameter:set config '{"key":"value"}'
```

**Use case**: 
- Quickly modify parameter values in scripts or deployments
- Update configuration without accessing database or EasyAdmin
- CI/CD pipeline parameter updates

**Note**: To create new parameters, use Doctrine entities directly (see "Writing/Updating Parameters" section).

---

### 4. Delete a parameter

```bash
php bin/console platform-parameter:delete <key> [--force]
```

**Arguments**:
- `key` (required): The parameter key to delete

**Options**:
- `--force` or `-f`: Skip confirmation prompt

**Behavior**:
- Displays parameter details (key, value, type, label) before deletion
- Asks for confirmation in interactive mode (unless `--force` is used)
- Removes the parameter from database
- Clears the cache for the deleted key
- Returns error if parameter doesn't exist

**Examples**:

```bash
# Delete with confirmation prompt
php bin/console platform-parameter:delete old_feature_flag

# Delete without confirmation (useful in scripts)
php bin/console platform-parameter:delete old_feature_flag --force
php bin/console platform-parameter:delete old_feature_flag -f
```

**Use case**: 
- Clean up deprecated parameters
- Remove test parameters after debugging
- Automated cleanup scripts

---

### 5. Clear parameter cache

```bash
php bin/console platform-parameter:cache:clear [key]
```

**Arguments**:
- `key` (optional): Specific parameter key to clear (leave empty to clear all)

**Behavior**:
- If `key` is provided: clears cache for that specific parameter
- If `key` is omitted: clears cache for all parameters
- Uses tag-based invalidation if `TagAwareCache` is available (O(1))
- Otherwise fetches all parameters and deletes individually (O(n))

**Examples**:

```bash
# Clear cache for specific parameter
php bin/console platform-parameter:cache:clear site_name

# Clear cache for all parameters
php bin/console platform-parameter:cache:clear
```

**Use case**:
- Manual cache invalidation if automatic cache clearing is disabled
- Force cache refresh after direct database modifications
- Debugging cache-related issues

**Note**: If `clear_cache_on_parameter_update` is enabled (default), cache is automatically cleared when using `set` or `delete` commands, so this command is rarely needed.

---

## Remarks

NEVER commit or push the git repository.

## ๐Ÿ“š References

- **Source code**: `/src`
- **Tests**: `/tests`
- **README**: User documentation
- **Symfony Docs**: https://symfony.com/doc/current/bundles.html
- **PSR-6 Cache**: https://www.php-fig.org/psr/psr-6/

AGENTS.md - Platform Parameter Bundle

๐ŸŽฏ Core Concept

Platform Parameter Bundle is a Symfony bundle for managing global platform parameters stored in the database with type-safe access and caching system.

Problem Solved

  • Avoid hard-coded values in code
  • Enable dynamic configuration without redeployment
  • Centralize business parameters (e.g., limits, feature flags, external URLs)
  • Provide a type-safe API to prevent type errors

Solution

A simple and robust system: Doctrine Entity + Service Provider + PSR-6 Cache


๐Ÿ—๏ธ Architecture

Overview

The architecture follows a layered approach:

Application Layer (Controllers, Services, Commands) interacts with the PlatformParameterProvider service through dependency injection. This provider implements the PlatformParameterProviderInterface and acts as the single entry point for parameter access.

The provider coordinates three underlying components:

  • PSR-6 Cache: First line of defense for fast parameter retrieval
  • Doctrine ORM: Database persistence layer for parameter storage
  • Validator Logic: Type conversion and validation mechanisms

All parameters are stored in the platform_parameter database table and accessed through the ORM.

Main Components

1. AbstractPlatformParameter (Entity)

  • MappedSuperclass allowing extension
  • Fields: id, key, value, type, label, description, timestamps
  • Auto-generated UUID
  • Lifecycle callbacks for updatedAt
// src/Entity/AbstractPlatformParameter.php
#[ORM\MappedSuperclass]
abstract class AbstractPlatformParameter
{
    private Uuid $id;
    private string $key;      // Unique identifier
    private string $value;    // Stored as TEXT
    private ParameterType $type;
    private string $label;
    private ?string $description;
    private \DateTimeImmutable $createdAt;
    private \DateTimeImmutable $updatedAt;
}

Design Choice: MappedSuperclass allows projects to extend the entity to add custom fields (category, icon, sortOrder, etc.)

2. PlatformParameter (Concrete Entity)

  • Inherits from AbstractPlatformParameter
  • Standard Doctrine entity used by default
  • Table platform_parameter
#[ORM\Entity]
#[ORM\Table(name: 'platform_parameter')]
class PlatformParameter extends AbstractPlatformParameter
{
    // Ready to use, no custom fields
}

3. ParameterType (Enum)

7 supported types with built-in validation:

enum ParameterType: string
{
    case STRING   = 'string';    // Simple text
    case INTEGER  = 'integer';   // Integer number
    case BOOLEAN  = 'boolean';   // true/false/1/0/yes/no
    case JSON     = 'json';      // JSON object/array
    case LIST     = 'list';      // Multi-line or delimiter-separated
    case FLOAT    = 'float';     // Decimal number
    case DATETIME = 'datetime';  // Date/time with multiple formats
}

4. PlatformParameterProviderInterface (Contract)

Service interface exposing the public API:

interface PlatformParameterProviderInterface
{
    public function getString(string $key, ?string $default = null): string;
    public function getInt(string $key, ?int $default = null): int;
    public function getBool(string $key, ?bool $default = null): bool;
    public function getJson(string $key, ?array $default = null): array;
    public function getList(string $key, ?array $default = null, string $separator = "\n"): array;
    public function getFloat(string $key, ?float $default = null): float;
    public function getDateTime(string $key, ?\DateTimeImmutable $default = null, ?string $format = null): \DateTimeImmutable;
    public function has(string $key): bool;
    public function clearCache(?string $key = null): void;
}

Design Pattern: Interface injection for testability and decoupling

5. PlatformParameterProvider (Service - READ)

Provider implementation with business logic:

Responsibilities:

  • Fetch from cache or DB
  • Type validation and conversion
  • Error handling (exceptions or defaults)
  • Smart cache invalidation

Execution Flow:

When a parameter is requested, the provider follows this sequence:

  1. Cache Check: Look for the parameter in cache using the format {prefix}.{key}
  2. Cache HIT: If found in cache, return the cached value immediately
  3. Cache MISS: If not in cache, query the database using Doctrine EntityManager
  4. Database Result: If found in DB, store it in cache and return the value
  5. Not Found: If the parameter doesn't exist, either return the provided default value or throw a ParameterNotFoundException

6. PlatformParameterWriter (Service - WRITE)

Writer service for updating parameter values from code:

Responsibilities:

  • Update existing parameter values with type safety
  • Delete parameters by key
  • Type validation (must match parameter's declared type)
  • Value conversion (PHP type โ†’ string for database)
  • Exception handling (not found or type mismatch)

Available Methods:

interface PlatformParameterWriterInterface
{
    public function setString(string $key, string $value): void;
    public function setInt(string $key, int $value): void;
    public function setBool(string $key, bool $value): void;
    public function setJson(string $key, array $value): void;
    public function setList(string $key, array $value, string $separator = "\n"): void;
    public function setFloat(string $key, float $value): void;
    public function setDateTime(string $key, \DateTimeImmutable $value, ?string $format = null): void;
    public function delete(string $key): void;
}

Behavior:

  • All set*() methods throw ParameterNotFoundException if the parameter doesn't exist
  • All set*() methods throw ParameterTypeMismatchException if the parameter type doesn't match the method type
  • delete() method throws ParameterNotFoundException if the parameter doesn't exist
  • Cache clearing is automatic via Doctrine listener
  • Events (PlatformParameterUpdatedEvent, PlatformParameterDeletedEvent) are dispatched automatically
  • No auto-creation: Writer only updates existing parameters (creation must be done via Doctrine entities)

Design Principle: CQRS-style separation: Provider = READ, Writer = WRITE

7. Symfony Configuration

# config/packages/platform_parameter.yaml
platform_parameter:
    entity_class: 'Ecourty\PlatformParameterBundle\Entity\PlatformParameter'
    cache_ttl: 3600
    cache_key_prefix: 'platform_parameter'
    clear_cache_on_parameter_update: true  # Auto-clear cache via Doctrine listener
    cache_adapter: null  # Optional custom cache service

๐Ÿ”„ Data Flow

Reading a Parameter

When reading a parameter, for example $provider->getInt('max_uploads', 10), the system follows these steps:

  1. Key Generation: The provider generates a cache key by prefixing the parameter key (e.g., 'platform_parameter.max_uploads')
  2. Cache Lookup: The PSR-6 cache is queried with this key
  3. Cache Hit Path: If the item exists in cache, the cached AbstractPlatformParameter object is returned directly
  4. Cache Miss Path: If not in cache, Doctrine's EntityManager performs a findOneBy(['key' => 'max_uploads']) query
  5. Database Hit: If found in the database, the parameter is cached for future requests and the value is returned
  6. Database Miss: If not found in the database, the default value (10) is returned, or a ParameterNotFoundException is thrown if no default was provided

Writing/Updating Parameters

Method 1: PlatformParameterWriter (Recommended for Updates)

The bundle provides a PlatformParameterWriter service for type-safe parameter updates from code:

// In a service or controller
public function __construct(
    private PlatformParameterWriterInterface $writer,
) {
}

public function updateParameter(): void
{
    // Update existing parameters with type-safe methods
    $this->writer->setInt('max_uploads', 100);
    $this->writer->setBool('maintenance_mode', true);
    $this->writer->setString('site_name', 'My Site');
    $this->writer->setJson('config', ['key' => 'value']);
    $this->writer->setList('emails', ['[email protected]', '[email protected]']);
    $this->writer->setFloat('rate', 3.14);
    $this->writer->setDateTime('last_sync', new \DateTimeImmutable());
    
    // Delete a parameter
    $this->writer->delete('old_parameter');
}

Important: The Writer can only update existing parameters. It throws ParameterNotFoundException if the parameter doesn't exist, and ParameterTypeMismatchException if you try to set a value with the wrong type (e.g., calling setInt() on a STRING parameter).

Why no auto-creation? Because if you extend AbstractPlatformParameter with custom fields (category, icon, etc.), the Writer doesn't know how to populate those fields. Parameter creation must be done explicitly via Doctrine entities.

Method 2: Direct Doctrine Entity Manipulation (For Creation)

To create a parameter, you must work directly with Doctrine entities:

// In a service or controller
$parameter = new PlatformParameter();
$parameter->setKey('max_uploads');
$parameter->setValue('20');
$parameter->setType(ParameterType::INTEGER);
$parameter->setLabel('Maximum Uploads');
$parameter->setDescription('Maximum number of files per upload');

$em->persist($parameter);
$em->flush();

By default, cache is automatically cleared via a Doctrine listener when parameters are created, updated, or deleted. You can disable this behavior by setting clear_cache_on_parameter_update: false in configuration if you prefer manual cache management.

Event System

The bundle dispatches Symfony events whenever parameters are created, updated, or deleted. This allows host projects to easily react to parameter changes without configuring Doctrine listeners.

Available Events:

  1. PlatformParameterCreatedEvent - Dispatched after a parameter is created
  2. PlatformParameterUpdatedEvent - Dispatched after a parameter is updated (includes old and new values)
  3. PlatformParameterDeletedEvent - Dispatched after a parameter is deleted

Example: React to parameter changes

// src/EventSubscriber/ParameterChangeSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Ecourty\PlatformParameterBundle\Event\PlatformParameterUpdatedEvent;
use Ecourty\PlatformParameterBundle\Event\PlatformParameterCreatedEvent;
use Ecourty\PlatformParameterBundle\Event\PlatformParameterDeletedEvent;

class ParameterChangeSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private SlackNotifier $slack,
        private LoggerInterface $logger,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            PlatformParameterCreatedEvent::class => 'onParameterCreated',
            PlatformParameterUpdatedEvent::class => 'onParameterUpdated',
            PlatformParameterDeletedEvent::class => 'onParameterDeleted',
        ];
    }

public function onParameterCreated(PlatformParameterCreatedEvent $event): void
    {
        $parameter = $event->getParameter();
        $this->logger->info('Parameter created', [
            'key' => $parameter->getKey(),
            'value' => $parameter->getValue(),
        ]);
    }

    public function onParameterUpdated(PlatformParameterUpdatedEvent $event): void
    {
        $parameter = $event->getParameter();
        
        // Send Slack notification for critical parameters
        if ($parameter->getKey() === 'maintenance_mode') {
            $this->slack->send(sprintf(
                'Maintenance mode changed from "%s" to "%s"',
                $event->getOldValue(),
                $event->getNewValue()
            ));
        }
    }

public function onParameterDeleted(PlatformParameterDeletedEvent $event): void
    {
        $parameter = $event->getParameter();
        $this->logger->warning('Parameter deleted', [
            'key' => $parameter->getKey(),
            'last_value' => $parameter->getValue(),
        ]);
    }
}

Event Details:

  • PlatformParameterCreatedEvent: Contains the newly created parameter entity
  • PlatformParameterUpdatedEvent: Contains the parameter entity, old value (string), and new value (string)
  • PlatformParameterDeletedEvent: Contains the parameter entity (before deletion)

Events are always dispatched, regardless of the clear_cache_on_parameter_update configuration setting. They work with:

  • Console commands (platform-parameter:set, platform-parameter:delete)
  • Direct Doctrine operations ($em->persist(), $em->flush(), $em->remove())
  • EasyAdmin CRUD operations

Cache Management: By default, cache is automatically cleared when parameters change. You can disable this behavior by setting clear_cache_on_parameter_update: false in configuration, but events will still be dispatched, allowing you to implement custom cache invalidation logic via event subscribers.


๐Ÿ’ก Design Patterns Used

1. Provider Pattern

The PlatformParameterProvider acts as a uniform facade for retrieving parameters.

2. Type Safety

Each method returns a strict PHP 8.3 type, preventing type errors.

3. Fail-Safe with Defaults

$value = $provider->getInt('unknown_key', 100); // Returns 100 if not found
$value = $provider->getInt('unknown_key');      // Throws ParameterNotFoundException

4. Cache-Aside (Lazy Loading)

The cache is checked first, the DB is only consulted in case of MISS.

5. Tagged Cache Support

If TagAwareCacheInterface is available, the bundle uses tags for efficient invalidation.

// Clear all parameters
$provider->clearCache();

// With TagAwareCache: 1 operation
โ†’ invalidateTags(['platform_parameter'])

// Without TagAwareCache: N operations (fetch all + delete each)
โ†’ findAll() โ†’ deleteItem() for each key

6. Template Method (Extension Point)

AbstractPlatformParameter allows inheritance for extension without modifying the core.


๐Ÿš€ Typical Use Cases

1. Feature Flags

if ($provider->getBool('feature_new_dashboard', false)) {
    return $this->render('dashboard_v2.html.twig');
}

2. Dynamic Limits

$maxFileSize = $provider->getInt('max_upload_size_mb', 10) * 1024 * 1024;
if ($file->getSize() > $maxFileSize) {
    throw new UploadException('File too large');
}

3. External Configuration

$apiConfig = $provider->getJson('stripe_config', [
    'public_key' => '',
    'secret_key' => '',
    'webhook_url' => '',
]);

$stripe = new StripeClient($apiConfig['secret_key']);

4. Dynamic Lists

$allowedEmails = $provider->getList('beta_testers_emails', []);
if (!in_array($user->getEmail(), $allowedEmails)) {
    throw new AccessDeniedException();
}

5. Maintenance Mode

// In an EventSubscriber
if ($provider->getBool('maintenance_mode', false)) {
    throw new ServiceUnavailableHttpException(3600, 'Under maintenance');
}

๐Ÿงฉ Extension: Custom Entity

Why Extend?

To add custom metadata: category, sort order, icon, tags, etc.

How?

Step 1: Create a custom entity

namespace App\Entity;

#[ORM\Entity]
#[ORM\Table(name: 'platform_parameter')]
class CustomPlatformParameter extends AbstractPlatformParameter
{
    #[ORM\Column(type: 'string', length: 100, nullable: true)]
    private ?string $category = null;

#[ORM\Column(type: 'integer', nullable: true)]
    private ?int $sortOrder = null;

    // Getters/setters...
}

Step 2: Configure the bundle

platform_parameter:
    entity_class: 'App\Entity\CustomPlatformParameter'

Step 3: Migration

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

โœ… The provider continues to work as is - autowiring automatically uses your custom entity.


๐ŸŽจ EasyAdmin Integration (Optional)

The bundle provides a ready-to-use CRUD controller if EasyAdmin is installed.

Usage

// src/Controller/Admin/PlatformParameterCrudController.php
namespace App\Controller\Admin;

use Ecourty\PlatformParameterBundle\Controller\PlatformParameterCrudController as BaseCrudController;

class PlatformParameterCrudController extends BaseCrudController
{
    // Ready to use!
}

Included Features

  • Complete CRUD with validation
  • Colored badges for types
  • Help text on each field
  • Role-based permissions
  • Truncated values in list view

โšก Performance & Caching

Cache Strategy

  1. Cache Key Format: {prefix}.{parameter_key} (e.g., platform_parameter.max_uploads)
  2. Configurable TTL: Default 3600s (1h)
  3. Per-parameter cache: Each key is cached individually
  4. Granular invalidation: Clear by key or globally

Optimizations

Using TagAwareCache

# config/packages/cache.yaml
framework:
    cache:
        pools:
            platform_parameter.cache:
                adapter: cache.adapter.redis.tag_aware
                default_lifetime: 3600

Benefits:

  • Bulk invalidation in O(1) instead of O(n)
  • Avoids fetching all parameters during clearCache()

Cache Warmup (Optional)

If you have many critical parameters:

// Command or EventListener
foreach ($criticalKeys as $key) {
    try {
        $provider->getString($key);
    } catch (ParameterNotFoundException) {
        // Log missing parameter
    }
}

๐Ÿ›ก๏ธ Error Handling

Exceptions

ParameterNotFoundException

Thrown when a parameter doesn't exist and no default is provided.

try {
    $value = $provider->getString('missing_key');
} catch (ParameterNotFoundException $e) {
    // Log or handle
    $value = 'fallback';
}

InvalidArgumentException

Thrown during type conversion if the value is invalid.

// DB value: "not_a_number"
$provider->getInt('bad_value'); // throws InvalidArgumentException

Best Practices

  1. Always provide a default in production
// โŒ Risky
$limit = $provider->getInt('rate_limit');

// โœ… Safe
$limit = $provider->getInt('rate_limit', 100);
  1. Validate in dev, default in prod
if ($this->environment === 'dev' && !$provider->has('critical_param')) {
    throw new \RuntimeException('Missing critical parameter');
}
$value = $provider->getString('critical_param', 'default');

๐Ÿงช Testing

Mocking the Provider

use Ecourty\PlatformParameterBundle\Contract\PlatformParameterProviderInterface;

class MyServiceTest extends TestCase
{
    public function testWithMockProvider(): void
    {
        $provider = $this->createMock(PlatformParameterProviderInterface::class);
        $provider->method('getInt')
            ->willReturnMap([
                ['max_uploads', null, 20],
                ['timeout', null, 30],
            ]);

        $service = new MyService($provider);
        // Test service behavior
    }
}

Functional Tests

// Create test parameters
$parameter = new PlatformParameter();
$parameter->setKey('test_key');
$parameter->setValue('42');
$parameter->setType(ParameterType::INTEGER);

$this->entityManager->persist($parameter);
$this->entityManager->flush();

// Test provider
$result = $this->parameterProvider->getInt('test_key');
$this->assertSame(42, $result);

๐Ÿ“‹ Developer Checklist

Adding a New Parameter

  1. โœ… Create the entity in DB (manually or via EasyAdmin)
  2. โœ… Define: key, value, type, label, description
  3. โœ… Test access via provider
  4. โœ… Document the parameter in the project (README, confluence, etc.)

Modifying a Parameter

  1. โœ… Update in DB via EasyAdmin or SQL
  2. โœ… Clear the cache: $provider->clearCache('key') or via console
  3. โœ… Verify that the new value is active

Debugging an Issue

  1. โ“ Does the parameter exist in DB? โ†’ SELECT * FROM platform_parameter WHERE key = '...'
  2. โ“ Is the cache stale? โ†’ $provider->clearCache('key') or php bin/console platform-parameter:cache:clear key
  3. โ“ Is the type correct? โ†’ Check type column and called method
  4. โ“ Is there a silent exception? โ†’ Check logs

๐ŸŽ›๏ธ CLI Commands

The bundle provides 5 Symfony commands for managing parameters via the console.

1. List all parameters

php bin/console platform-parameter:list

Features:

  • Displays a formatted table with columns: Key, Value, Type, Label
  • Values longer than 50 characters are truncated with "..."
  • Shows a warning if no parameters exist
  • Output: "Found X parameter(s)."

Use case: Quick overview of all platform parameters


2. Get parameter details

php bin/console platform-parameter:get <key>

Arguments:

  • key (required): The parameter key to display

Features:

  • Shows all metadata: ID (UUID), Key, Value (full), Type, Label, Description, Created At, Updated At
  • Returns error if parameter doesn't exist

Examples:

php bin/console platform-parameter:get max_uploads
php bin/console platform-parameter:get site_name

Use case: Inspect complete parameter details including timestamps


3. Update a parameter value

php bin/console platform-parameter:set <key> <value>

Arguments:

  • key (required): The parameter key (must exist)
  • value (required): The new parameter value

Behavior:

  • Updates the value of an existing parameter
  • Automatically detects parameter type and converts value accordingly
  • Cache is automatically cleared
  • Events are automatically dispatched
  • Returns error if parameter doesn't exist

Examples:

# Update integer parameter
php bin/console platform-parameter:set max_uploads 50

# Update boolean parameter
php bin/console platform-parameter:set maintenance_mode true

# Update string parameter
php bin/console platform-parameter:set site_name "My New Site"

# Update JSON parameter
php bin/console platform-parameter:set config '{"key":"value"}'

Use case:

  • Quickly modify parameter values in scripts or deployments
  • Update configuration without accessing database or EasyAdmin
  • CI/CD pipeline parameter updates

Note: To create new parameters, use Doctrine entities directly (see "Writing/Updating Parameters" section).


4. Delete a parameter

php bin/console platform-parameter:delete <key> [--force]

Arguments:

  • key (required): The parameter key to delete

Options:

  • --force or -f: Skip confirmation prompt

Behavior:

  • Displays parameter details (key, value, type, label) before deletion
  • Asks for confirmation in interactive mode (unless --force is used)
  • Removes the parameter from database
  • Clears the cache for the deleted key
  • Returns error if parameter doesn't exist

Examples:

# Delete with confirmation prompt
php bin/console platform-parameter:delete old_feature_flag

# Delete without confirmation (useful in scripts)
php bin/console platform-parameter:delete old_feature_flag --force
php bin/console platform-parameter:delete old_feature_flag -f

Use case:

  • Clean up deprecated parameters
  • Remove test parameters after debugging
  • Automated cleanup scripts

5. Clear parameter cache

php bin/console platform-parameter:cache:clear [key]

Arguments:

  • key (optional): Specific parameter key to clear (leave empty to clear all)

Behavior:

  • If key is provided: clears cache for that specific parameter
  • If key is omitted: clears cache for all parameters
  • Uses tag-based invalidation if TagAwareCache is available (O(1))
  • Otherwise fetches all parameters and deletes individually (O(n))

Examples:

# Clear cache for specific parameter
php bin/console platform-parameter:cache:clear site_name

# Clear cache for all parameters
php bin/console platform-parameter:cache:clear

Use case:

  • Manual cache invalidation if automatic cache clearing is disabled
  • Force cache refresh after direct database modifications
  • Debugging cache-related issues

Note: If clear_cache_on_parameter_update is enabled (default), cache is automatically cleared when using set or delete commands, so this command is rarely needed.


Remarks

NEVER commit or push the git repository.

๐Ÿ“š 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.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo