Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
"authors": [
{
"name": "Jan Böhmer",
"email": "mail@jan-boehmer.de"
"email": "mail@jan-boehmer.de",
"role": "Maintainer"
},
{
"name": "Github Contributors",
"homepage": "https://github.com/jbtronics/settings-bundle/graphs/contributors"
}
],
"suggest": {
"symfony/twig-bridge": "Allows to access settings in twig templates",
"doctrine/doctrine-bundle": "To use the doctrine ORM storage"
"doctrine/doctrine-bundle": "To use the doctrine ORM storage",
"symfony/yaml": "To use the YAML metadata driver for settings configuration"
},
"require": {
"php": "^8.1",
Expand Down Expand Up @@ -51,7 +57,8 @@
"phpstan/phpstan-symfony": "^1.3",
"doctrine/doctrine-bundle": "^2.11",
"doctrine/orm": "^3.0",
"doctrine/doctrine-fixtures-bundle": "^3.5"
"doctrine/doctrine-fixtures-bundle": "^3.5",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"config": {
"allow-plugins": {
Expand Down
30 changes: 29 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
use Jbtronics\SettingsBundle\Manager\SettingsRegistryInterface;
use Jbtronics\SettingsBundle\Manager\SettingsResetter;
use Jbtronics\SettingsBundle\Manager\SettingsResetterInterface;
use Jbtronics\SettingsBundle\Metadata\Driver\AttributeDriver;
use Jbtronics\SettingsBundle\Metadata\Driver\ChainDriver;
use Jbtronics\SettingsBundle\Metadata\Driver\MetadataDriverInterface;
use Jbtronics\SettingsBundle\Metadata\Driver\YamlDriver;
use Jbtronics\SettingsBundle\Metadata\MetadataManager;
use Jbtronics\SettingsBundle\Metadata\MetadataManagerInterface;
use Jbtronics\SettingsBundle\Migrations\SettingsMigrationInterface;
Expand Down Expand Up @@ -60,11 +64,34 @@
$services->instanceof(\Jbtronics\SettingsBundle\Migrations\SettingsMigration::class)
->call('setParameterTypeRegistry', [service('jbtronics.settings.parameter_type_registry')]);

/**********************************************************************************
* Metadata Drivers
**********************************************************************************/
$services->set('jbtronics.settings.metadata_driver.attribute', AttributeDriver::class)
->args([
'$searchPathes' => '%jbtronics.settings.search_paths%',
])
;

$services->set('jbtronics.settings.metadata_driver.yaml', YamlDriver::class)
->args([
'$yamlMappingPaths' => '%jbtronics.settings.yaml_mapping_paths%',
]);

$services->set('jbtronics.settings.metadata_driver', ChainDriver::class)
->args([
'$drivers' => [
service('jbtronics.settings.metadata_driver.attribute'),
service('jbtronics.settings.metadata_driver.yaml'),
],
]);
$services->alias(MetadataDriverInterface::class, 'jbtronics.settings.metadata_driver');

$services->set('jbtronics.settings.settings_registry', SettingsRegistry::class)
->args([
'$directories' => '%jbtronics.settings.search_paths%',
'$cache' => service('jbtronics.settings.cache.metadata_service'),
'$debug_mode' => '%kernel.debug%',
'$metadataDriver' => service('jbtronics.settings.metadata_driver'),
]);
$services->alias(SettingsRegistryInterface::class, 'jbtronics.settings.settings_registry');

Expand All @@ -74,6 +101,7 @@
'$debug_mode' => '%kernel.debug%',
'$settingsRegistry' => service('jbtronics.settings.settings_registry'),
'$parameterTypeGuesser' => service('jbtronics.settings.parameter_type_guesser'),
'$metadataDriver' => service('jbtronics.settings.metadata_driver'),
'$defaultStorageAdapter' => '%jbtronics.settings.default_storage_adapter%',
'$defaultCacheable' => '%jbtronics.settings.cache.default_cacheable%',
]);
Expand Down
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ jbtronics_settings:
search_paths:
- '%kernel.project_dir%/src/Settings'

# Directories containing YAML mapping files for settings classes.
# This allows defining settings metadata in YAML instead of PHP attributes.
# See the YAML Configuration documentation for more information.
yaml_mapping_paths: []
# Example:
# - '%kernel.project_dir%/config/settings'

# The class name of the service, which is used on all storage adapters if
# non is set explicitly. Can be null, if the storage adapter is configured # explicitly everywhere
default_storage_adapter: ~
Expand All @@ -38,6 +45,13 @@ jbtronics_settings:

# The directory where the proxy classes should be stored
proxy_dir: '%kernel.cache_dir%/jbtronics_settings/proxies'

# ADVANCED: The metadata drivers to retrieve the settings metadata, when the container is compiled.
# You do not need to configure this, unless you implement a custom metadata driver, whose settings should be dependency injectable.
# By default, the YamlDriver is registered, when yaml mapping paths are configured
metadata_compiler_providers: ~
# Example:
# - 'Jbtronics\SettingsBundle\Metadata\Driver\YamlDriver'

# The configuration for caching of settings
cache:
Expand Down
170 changes: 170 additions & 0 deletions docs/usage/yaml_configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: YAML Configuration
layout: default
parent: Usage
nav_order: 2
---

# YAML Configuration

{: .important }
> It is recommended to use PHP attributes for settings configuration. This YAML configuration option is currently more considered experimentally, and might change in the future or be removed completely in future versions.

By default, settings classes are configured using PHP attributes (`#[Settings]`, `#[SettingsParameter]`, etc.). As an alternative, you can define settings metadata in YAML files. This allows you to keep your settings classes as plain PHP in the application layer, while the infrastructure-level configuration (storage adapters, parameter types, labels, etc.) lives in YAML files — following the same pattern as Doctrine ORM's YAML mapping.

## Requirements

Install the `symfony/yaml` package:

```bash
composer require symfony/yaml
```

## Setup

Configure the directories where your YAML mapping files are located in your bundle configuration:

```yaml
# config/packages/jbtronics_settings.yaml

jbtronics_settings:
yaml_mapping_paths:
- '%kernel.project_dir%/config/settings'
```

## Defining settings in YAML

### Step 1: Create a plain PHP settings class

Your settings class is a regular PHP class with typed properties. No attributes needed.

```php
<?php
// src/Settings/AppSettings.php

namespace App\Settings;

class AppSettings
{
public string $siteName = 'My Application';

public ?int $itemsPerPage = 25;

public bool $maintenanceMode = false;
}
```

### Step 2: Create a YAML mapping file

Create a YAML file in your configured mapping directory. The file name should follow the convention of replacing namespace backslashes with dots:

`App\Settings\AppSettings` → `App.Settings.AppSettings.yaml`

```yaml
# config/settings/App.Settings.AppSettings.yaml

App\Settings\AppSettings:
name: app
storageAdapter: Jbtronics\SettingsBundle\Storage\JSONFileStorageAdapter
label: "Application Settings"
description: "General application configuration"

parameters:
siteName:
type: Jbtronics\SettingsBundle\ParameterTypes\StringType
label: "Site Name"
description: "The name of the application"

itemsPerPage:
type: Jbtronics\SettingsBundle\ParameterTypes\IntType
label: "Items per Page"
nullable: true

maintenanceMode:
type: Jbtronics\SettingsBundle\ParameterTypes\BoolType
label: "Maintenance Mode"
groups:
- admin
```

## YAML reference

### Class-level options

The top-level key is the fully qualified class name. All options mirror the `#[Settings]` attribute:

| Option | Type | Default | Description |
|---|---|---|---|
| `name` | string | auto-generated | Short name for the settings class |
| `storageAdapter` | string | global default | FQCN of the storage adapter service |
| `storageAdapterOptions` | array | `[]` | Options passed to the storage adapter |
| `groups` | string[] | `null` | Default groups for parameters |
| `version` | int | `null` | Version number for migrations |
| `migrationService` | string | `null` | FQCN of the migration service |
| `dependencyInjectable` | bool | `true` | Whether the class can be injected via DI |
| `label` | string | `null` | User-friendly label |
| `description` | string | `null` | User-friendly description |
| `cacheable` | bool | `null` | Override caching behavior |

### Parameter options (under `parameters:`)

Each key under `parameters` is a property name. Options mirror the `#[SettingsParameter]` attribute:

| Option | Type | Default | Description |
|---|---|---|---|
| `type` | string | auto-guessed | FQCN of the parameter type |
| `name` | string | property name | Internal name for the parameter |
| `label` | string | `null` | User-friendly label |
| `description` | string | `null` | User-friendly description |
| `options` | array | `[]` | Extra options for the parameter type |
| `formType` | string | `null` | FQCN of the Symfony form type |
| `formOptions` | array | `[]` | Options passed to the form type |
| `nullable` | bool | auto-detected | Whether the value can be null |
| `groups` | string[] | `null` | Groups this parameter belongs to |
| `envVar` | string | `null` | Environment variable name |
| `envVarMode` | string | `INITIAL` | One of: `INITIAL`, `OVERWRITE`, `OVERWRITE_PERSIST` |
| `envVarMapper` | string | `null` | FQCN of a ParameterType service for env var mapping |
| `cloneable` | bool | `true` | Whether property is cloned when settings are cloned |

### Embedded settings options (under `embeddedSettings:`)

Each key under `embeddedSettings` is a property name. Options mirror the `#[EmbeddedSettings]` attribute:

| Option | Type | Default | Description |
|---|---|---|---|
| `target` | string | auto-detected | FQCN of the embedded settings class |
| `groups` | string[] | `null` | Groups this embedded class belongs to |
| `label` | string | `null` | User-friendly label |
| `description` | string | `null` | User-friendly description |
| `formOptions` | array | `null` | Options passed to the embedded form |

## Embedded settings example

```yaml
# config/settings/App.Settings.DashboardSettings.yaml

App\Settings\DashboardSettings:
storageAdapter: Jbtronics\SettingsBundle\Storage\JSONFileStorageAdapter

parameters:
title:
type: Jbtronics\SettingsBundle\ParameterTypes\StringType
label: "Dashboard Title"

embeddedSettings:
widgetSettings:
target: App\Settings\WidgetSettings
label: "Widget Configuration"
groups:
- admin
```

## Mixing attributes and YAML

You can use both attributes and YAML in the same project. Each settings class should use one or the other — if a class has `#[Settings]` attributes, those take precedence over any YAML configuration for that class.

## Limitations

- **Callable `envVarMapper`**: YAML only supports class-string references (FQCN of a `ParameterTypeInterface` service). PHP closure mappers are not available in YAML — use attributes if you need callable mappers.
- **`TranslatableInterface` labels**: YAML labels and descriptions are plain strings. Use translation keys as strings for i18n support, which is the standard Symfony approach.
- **Type guessing**: Works the same as with attributes — if `type` is omitted, the bundle guesses from the PHP property type declaration.
11 changes: 11 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ public function getConfigTreeBuilder(): TreeBuilder

->booleanNode('save_after_migration')->defaultTrue()->end()

->arrayNode('yaml_mapping_paths')
->defaultValue([])
->scalarPrototype()->end()
->end()

->arrayNode('metadata_compiler_providers')
->treatNullLike([])
->defaultValue([])
->scalarPrototype()->end()
->end()

->end();

$this->addFileStorageConfiguration($rootNode);
Expand Down
29 changes: 29 additions & 0 deletions src/DependencyInjection/JbtronicsSettingsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@

namespace Jbtronics\SettingsBundle\DependencyInjection;

use Jbtronics\SettingsBundle\Metadata\Driver\CompileTimeMetadataDriverInterface;
use Jbtronics\SettingsBundle\Metadata\Driver\MetadataDriverInterface;
use Jbtronics\SettingsBundle\Metadata\Driver\YamlDriver;
use Jbtronics\SettingsBundle\Migrations\SettingsMigrationInterface;
use Jbtronics\SettingsBundle\ParameterTypes\ParameterTypeInterface;
use Jbtronics\SettingsBundle\Storage\StorageAdapterInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

Expand All @@ -40,6 +45,8 @@ final class JbtronicsSettingsExtension extends Extension
public const TAG_STORAGE_ADAPTER = 'jbtronics.settings.storage_adapter';
public const TAG_MIGRATION = 'jbtronics.settings.migration';



public const TAG_INJECTABLE_SETTINGS = 'jbtronics.settings.injectable_settings';

public const RESSOURCE_TAG_SETTINGS = 'jbtronics.settings';
Expand All @@ -66,6 +73,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('jbtronics.settings.proxy_dir', $config['proxy_dir']);
$container->setParameter('jbtronics.settings.proxy_namespace', $config['proxy_namespace']);
$container->setParameter('jbtronics.settings.search_paths', $config['search_paths']);
$container->setParameter('jbtronics.settings.yaml_mapping_paths', $config['yaml_mapping_paths']);
$container->setParameter('jbtronics.settings.default_storage_adapter', $config['default_storage_adapter']);
$container->setParameter('jbtronics.settings.save_after_migration', $config['save_after_migration']);

Expand All @@ -81,5 +89,26 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setAlias('jbtronics.settings.cache.metadata_service', $config['cache']['metadata_service']);
$container->setParameter('jbtronics.settings.cache.ttl', $config['cache']['ttl']);
$container->setParameter('jbtronics.settings.cache.invalidate_on_env_change', $config['cache']['invalidate_on_env_change']);


//When metadata compiler providers are configured, use that value
if (count($config['metadata_compiler_providers'] ?? []) > 0) {
//Set empty array, when first value is an empty string
if (count($config['metadata_compiler_providers']) === 1 && $config['metadata_compiler_providers'][0] === '') {
$config['metadata_compiler_providers'] = [];
}

$container->setParameter('jbtronics.settings.metadata_compiler_providers', $config['metadata_compiler_providers']);
} else {
$providers = [];

//Add YamlDriver as default, if yaml_mapping_paths are configured, otherwise don't add it, as it would be useless and just consume resources
if (count($container->getParameter('jbtronics.settings.yaml_mapping_paths')) > 0) {
$providers[] = YamlDriver::class;
}

$container->setParameter('jbtronics.settings.metadata_compiler_providers', $providers);
}

}
}
Loading
Loading