---
title: "Base for Applications Category"
description: "Base for Applications"
type: "docs"
category: "doc"
tags: []
authors: [Anonymous]
date: "2026-09-09"
last_update: "2026-09-09"
time_minutes: 1
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/core"
---

# Base for Applications



---

## Website Project

Base for Derafu&#039;s Websites

# Base for Derafu&#039;s Websites

This guide explains how to create a new website using the [Derafu Website Base](https://github.com/derafu/website), the [Docker](https://www.derafu.dev/docs/sysadmin/docker-php-caddy-server) and [PHP Deployer](https://www.derafu.dev/docs/sysadmin/deployer) projects for local development and deployment to the production server.

&gt; [!TIP] WIP: Work in Progress
&gt;
&gt; The Road to Success is Always Under Construction.

## Quick Start

Create a new website based on this base:

```shell
composer create-project derafu/website www.example.com --stability=dev
```

## Review what you need to do, install, or know

### Configure ZSH on local macOS machine

If you&#039;re on macOS and use ZSH as your shell, you can configure ZSH to accept comments in commands with:

```shell
echo &quot;setopt interactivecomments&quot; &gt;&gt; ~/.zshrc
source ~/.zshrc
```

### SSH Key on local machine

It&#039;s necessary to have an SSH key on your local machine. This key will be used to configure your Docker container.

With the following command, you&#039;ll get an existing key or create a new one in `$HOME/.ssh/id_rsa` and `$HOME/.ssh/id_rsa.pub`. If a new one is created, you&#039;ll need to enter your email to identify the key owner. If you prefer, you can enter your username and machine name (in case you have multiple SSH keys in different environments).

```shell
SSH_KEY=&quot;$HOME/.ssh/id_rsa&quot;
if [ -f &quot;$SSH_KEY.pub&quot; ]; then
    cat &quot;$SSH_KEY.pub&quot;
else
    echo -n &quot;Enter your email: &quot;; read COMMENT
    ssh-keygen -t rsa -b 4096 -N &quot;&quot; -C &quot;$COMMENT&quot; -f &quot;$SSH_KEY&quot;
    cat &quot;$SSH_KEY.pub&quot;
fi
```

With this command, the public key will be displayed on screen. If you need to see it again in the future, run:

```shell
cat $HOME/.ssh/id_rsa.pub
```

**Important**: The key in `$HOME/.ssh/id_rsa` is **private** and should never be shared.

### Add SSH key to GitHub

1. Go to [GitHub](https://github.com/settings/ssh/new).
2. In `Title`, enter the same email or comment you chose when creating the key.
3. In `Key`, paste the public key extracted with `cat $HOME/.ssh/id_rsa.pub`.
4. Click on `Add SSH key`.

### Docker container with PHP and Caddy

Prepare [Docker](https://www.derafu.dev/docs/sysadmin/docker-php-caddy-server) in `$DOCKER_DIR` on your local machine:

```shell
DEV_DIR=$HOME/dev
DOCKER_DIR=$DEV_DIR/docker-sites-php
mkdir -p $DEV_DIR
git clone https://github.com/derafu/docker-php-caddy-server.git $DOCKER_DIR
cat $HOME/.ssh/id_rsa.pub &gt; $DOCKER_DIR/config/ssh/authorized_keys
cd $DOCKER_DIR
```

Copy and edit the `.env` file and configure the environment variables as needed.

```shell
# It&#039;s recommended to at least configure the DEPLOYER_HOST variable.
cp .env-dist .env
```

&gt; [!WARNING]
&gt;
&gt;Don&#039;t proceed until you&#039;ve reviewed and configured the `.env` file.

Build the Docker container:

```shell
docker-compose up -d
```

With this configuration, the folder where sites to be developed will be installed will be in `$DOCKER_DIR/sites`. This folder will be shared between your local machine and the Docker container.

### Connect to Docker container

Configure the `dev` SSH alias on your local machine:

```shell
echo &quot;
Host dev
    HostName localhost
    User admin
    Port 2222
    IdentityFile $HOME/.ssh/id_rsa
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
&quot; &gt;&gt; $HOME/.ssh/config
```

Then you can enter the Docker container with:

```shell
ssh dev
```

**Note**: The alias name can be whatever you want; in this case, `dev` was used.

### SSH key in Docker container

To work with private repositories and deploy to the production server, it&#039;s necessary that the SSH key from your local machine be added to the Docker container.

On your local machine, run:

```shell
scp $HOME/.ssh/id_rsa* dev:.ssh/
```

### Basic Git configuration

Perform the following configurations inside the Docker container.

First, configure your GitHub email and name:

&gt; [!WARNING]
&gt;
&gt;Before pasting this command in the Docker container, edit it to use your GitHub email and name.

```shell
git config --global user.email &quot;you@example.com&quot;  # Your github email.
git config --global user.name &quot;Your Name&quot;         # Your name.
```

Configure case sensitivity and avoid mixing changes:

```shell
git config --global core.ignorecase false         # Case sensitive.
git config --global pull.rebase false             # Rebase instead of merge.
git config --global merge.ff false                # Fast forward.
```

Configure text editor:

```shell
git config --global core.editor nano              # Default editor nano or any other.
```

### Configure commit signing in Git

First, create the SSH key on your local machine:

```shell
SSH_KEY=&quot;$HOME/.ssh/id_ed25519&quot;
if [ -f &quot;$SSH_KEY.pub&quot; ]; then
    cat &quot;$SSH_KEY.pub&quot;
else
    echo -n &quot;Enter your email: &quot;; read COMMENT
    ssh-keygen -t ed25519 -N &quot;&quot; -C &quot;$COMMENT&quot; -f &quot;$SSH_KEY&quot;
    cat &quot;$SSH_KEY.pub&quot;
fi
```

Then add the SSH key to the Docker container:

```shell
scp $HOME/.ssh/id_ed25519* dev:.ssh/
```

Finally, inside the Docker container, configure Git to sign commits:

```shell
git config --global commit.gpgSign true               # Sign commits.
git config --global user.signingkey ~/.ssh/id_ed25519 # Your ssh key.
git config --global gpg.format ssh                    # Use ssh key.
git config --global tag.gpgSign true                  # Sign tags.
```

## Develop a website

### Create a new website

Enter the container and run:

```shell
site-create www.example.com
```

**Note**: Creating the website requires as a subsequent step that you configure its repository on GitHub and add the site to the `$DEPLOYER_DIR/sites.php` file using the `site-add` command.

### Clone a website

Enter the container and run:

```shell
site-clone www.example.com git@github.com:example/example.git
```

**Note**: When cloning an existing website, the site is automatically added to the `$DEPLOYER_DIR/sites.php` file.

### Visit the website in the browser

On your machine, configure `/etc/hosts` by adding the local development domain:

```shell
echo &quot;127.0.0.1         www.example.com.local&quot; | sudo tee -a /etc/hosts
```

Then you can access the website via the URL https://www.example.com.local:8443

## Deploy a website to production

All these instructions are executed in the Docker container.

### Add website to configuration file

If you created the site from scratch instead of cloning it, make sure the website is added to the `$DEPLOYER_DIR/sites.php` file. You can validate this by running:

```shell
site-add www.example.com git@github.com:example/example.git
```

**Note**: If the site requires special configuration, you&#039;ll need to manually edit the `$DEPLOYER_DIR/sites.php` file.

### Style tests, code quality, and unit tests

Run style tests, code quality, and unit tests in the Docker container with:

```shell
site www.example.com
site-check
```

### Push changes to GitHub

If everything is correct, push the changes to GitHub:

```shell
site www.example.com
site-send &quot;Website update.&quot;
```

**Note**: If the GitHub repository has a [configured webhook](https://www.derafu.dev/docs/sysadmin/github), the website will be deployed automatically when pushing changes and passing the style tests, code quality, and unit tests in the GitHub Actions workflow.

### Deployment

&gt; [!INFO]
&gt;
&gt;It&#039;s not necessary to deploy to the production server if the GitHub repository has a [configured webhook](https://www.derafu.dev/docs/sysadmin/github){.alert-link}.

If there are no errors, you can deploy to the production server with:

```shell
#DEPLOYER_HOST=hosting.example.com # Only if not configured in .env
site-deploy www.example.com
```

If an error occurs when deploying and you try to make a new deploy, it&#039;s very likely that the deploy is locked. If this happens, you can unlock and deploy again with:

```shell
#DEPLOYER_HOST=hosting.example.com # Only if not configured in .env
site-deploy-locked www.example.com
```

## Update components

### Update Docker

On your local machine, run:

```shell
DEV_DIR=$HOME/dev
DOCKER_DIR=$DEV_DIR/docker-sites-php
cd $DOCKER_DIR
git pull
docker-compose build --no-cache
docker-compose up -d
```

&gt; [!WARNING]
&gt;
&gt;You must add the SSH keys to the Docker container again and configure Git inside the container.

### Update PHP Deployer

Enter the container and run:

```shell
cd $DEPLOYER_DIR
git pull
composer update
```

### Update website

Enter the container and run:

```shell
site www.example.com
site-update
```

## Using development tools

### Composer

Install dependencies:

```shell
composer install
```

Add development dependency:

```shell
composer require --dev dependency-name
```

Add production dependency:

```shell
composer require dependency-name
```

Remove dependency:

```shell
composer remove dependency-name
```

Update dependencies:

```shell
composer update
```

**Note**: Normally only `composer install` is used to install dependencies.

### NPM

Install dependencies:

```shell
npm install
```

Add development dependency:

```shell
npm install --save-dev dependency-name
```

Add production dependency:

```shell
npm install --save dependency-name
```

Remove dependency:

```shell
npm uninstall dependency-name
```

Update dependencies:

```shell
npm update
```

**Note**: Normally only `npm install` is used to install dependencies.

### Git

View change status:

```shell
git status
```

Add changes:

```shell
git add .
```

Make commit:

```shell
git commit -m &quot;Commit message&quot;
```

Push changes to GitHub:

```shell
git push
```

Update local repository:

```shell
git pull
```

Undo changes:

```shell
git checkout -- .
```

**Note**: Instead of using dot `.`, you can specify the files you want to add, commit, or revert.

### PHP CS Fixer

Check code style:

```shell
composer phpcs
```

Fix code style:

```shell
composer phpcs-fix
```

### PHP Unit

Run unit tests:

```shell
composer tests
```

## Using terminal in Docker container

Enter a website directory:

```shell
cd $SITES_DIR/www.example.com
```

Exit directory:

```shell
cd ..
```

List files:

```shell
ls -la
```

View file content:

```shell
cat $DEPLOYER_DIR/sites.php
```

Edit a file:

```shell
nano $DEPLOYER_DIR/sites.php
```

Save and exit in `nano`:

```shell
Ctrl + X
```




---

## Foundation Project

Derafu Foundation

# Derafu Foundation




---

### Introduction

Base for Derafu&#039;s Projects

# Base for Derafu&#039;s Projects

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/foundation/main)
![CI Workflow](https://github.com/derafu/foundation/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/foundation)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/foundation)
![Total Downloads](https://poser.pugx.org/derafu/foundation/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/foundation/d/monthly)

## Overview

Derafu Foundation is a standardized base for creating consistent PHP projects. It provides a pre-configured environment with best practices for code organization, documentation, testing, and code quality assurance. This foundation is designed to streamline the creation of new projects while ensuring they follow consistent patterns and standards.

## Key Features

- **Standardized Structure**: Consistent file and directory organization across all projects.
- **Code Quality Tools**: Pre-configured PHP CS Fixer, PHPStan, and PHPUnit setups.
- **Asset Management**: Built-in Vite configuration for CSS, JavaScript, and image optimization.
- **Documentation**: Automatic website generation from README.md with GitHub Pages deployment.
- **CI/CD Integration**: GitHub Actions workflows for continuous integration and deployment.
- **Middleware-based HTTP Layer**: Ready-to-use PSR-compliant HTTP handling.
- **Routing System**: Flexible routing with support for static routes and filesystem-based routes.
- **Templating**: Twig and Markdown rendering support.

## Quick Start

### Installation

Create a new project based on this foundation:

```shell
composer create-project derafu/project project_dir --stability=dev
```

### Asset Compilation

If your project uses CSS, JavaScript, or images, add them to the `assets` directory and compile them:

```shell
npm install
npm run build
```

### Development Server

Start the built-in PHP development server:

```shell
php -S localhost:9000 public/index.php
```

Then visit [http://localhost:9000](http://localhost:9000) to see your project.

## Project Structure

```
project_dir/
├── .github/             # GitHub configurations and workflows.
├── app/                 # Application bootstrap files.
├── assets/              # Frontend assets (CSS, JS, images).
├── config/              # Configuration files.
│   ├── routes.yaml      # Route definitions.
│   └── services.yaml    # Service container configuration.
├── public/              # Web server root directory.
│   ├── index.php        # Application entry point.
│   └── static/          # Compiled assets (generated).
├── src/                 # Source code.
├── templates/           # Template files.
│   ├── components/      # Reusable template components.
│   ├── layouts/         # Layout templates.
│   └── pages/           # Page templates.
├── tests/               # Test files.
│   ├── fixtures/        # Test fixtures.
│   └── src/             # Source code tests.
├── vendor/              # Composer dependencies.
├── var/                 # Temporary files and caches.
├── .gitignore           # Git ignore configuration.
├── LICENSE              # MIT License.
├── package.json         # NPM configuration.
├── php-cs-fixer.php     # PHP CS Fixer configuration.
├── phpstan.neon         # PHPStan configuration.
├── phpunit.xml          # PHPUnit configuration.
├── README.md            # Project documentation.
└── vite.config.js       # Vite build configuration.
```

## Customization

After creating a project based on this foundation, you can:

1. Modify `README.md` with your project-specific details.
2. Update `package.json` with your project information.
3. Add your routes in `config/routes.yaml`.
4. Customize services in `config/services.yaml`.
5. Create controllers and other classes in the `src/` directory.
6. Add templates to the `templates/` directory.
7. Write tests in the `tests/` directory.




---

### Project Structure

Project Structure

# Project Structure

Derafu Foundation provides a standardized directory structure for your projects. This document explains the purpose of each directory and key files.

## Root Directories

### `.github/`

Contains GitHub-specific configurations:

- `workflows/ci.yml`: Continuous Integration workflow that runs tests and code quality checks.
- `workflows/cd.yml`: Continuous Deployment workflow for automatically deploying. By default, it is configured to deploy to GitHub Pages. Deployment is disabled by default, just remove the `false &amp;&amp;` in the file to enable it.

### `app/`

Application bootstrap files:

- `bootstrap.php`: The main bootstrap file that initializes the application runtime.

### `assets/`

Frontend assets organized by type:

- `css/`: CSS stylesheets.
- `js/`: JavaScript files.
- `img/`: Images and other media files.

### `config/`

Configuration files:

- `routes.yaml`: Route definitions for your application.
- `services.yaml`: Service container configuration (dependency injection).

### `public/`

Web server document root:

- `index.php`: Application entry point with Front Controller.
- `static/`: Compiled assets (generated by the build process).

**Note**: If your application doesn&#039;t need a Front Controller, by default, it&#039;s used only for documentation.

### `src/`

Application source code. Organize your PHP classes here according to their namespace. By convention, but not enforced out of Derafu ORG, use:

- `Abstract/`: Abstract classes, with `Abstract` prefix.
- `Contract/`: Interfaces for your application, with `Interface` suffix.
- `Controller/`: Controller classes, with `Controller` suffix.
- `Service/`: Service classes.

### `templates/`

Template files for rendering HTML:

- `components/`: Reusable template components (header, footer, etc.).
- `layouts/`: Layout templates that define page structure.
- `pages/`: Page-specific templates.
- `base.html.twig`: Base template that defines the HTML structure for all layouts.
- `error.html.twig`: Error page template.
- `html.html.twig`: HTML wrapper template, used when rendering a markdown o php template.

### `tests/`

Test files:

- `fixtures/`: Test fixtures and sample data.
- `src/`: Tests for your source code. You can organize your tests by tests suits, mirroring the structure of the `src/` directory, by features or any other criteria (but not enforced out of Derafu ORG).

### `var/`

Temporary files and caches:

- `cache/`: Cache files.
- `logs/`: Log files.
- `tmp/`: Temporary files.

## Key Files

### Configuration Files

- `.gitignore`: Specifies files that Git should ignore.
- `LICENSE`: MIT license file by default.
- `package.json`: NPM configuration for frontend assets.
- `php-cs-fixer.php`: PHP CS Fixer configuration.
- `phpstan.neon`: PHPStan configuration.
- `phpunit.xml`: PHPUnit configuration.
- `vite.config.js`: Vite build configuration.

### Application Files

- `public/index.php`: Main entry point that bootstraps the application.
- `app/bootstrap.php`: Initializes the runtime environment.

## File Copying Mechanism

The `Installer` class in `Installer.php` is responsible for copying foundation files to new projects during Composer installation. This class:

1. Defines a list of files to copy in the `FILES` constant.
2. Copies each file from the foundation package to the project directory.
3. Creates necessary directories if they don&#039;t exist.
4. Handles file overwrite rules (some files are configured to be overwritten in updates, others not).

Files marked with `true` in the `FILES` constant will be overwritten if they already exist:

```php
private const FILES = [
    // Files that will not be overwritten if they exist.
    &#039;.github/workflows/cd.yml&#039;,
    &#039;.github/workflows/ci.yml&#039;,
    // Files that will be overwritten even if they exist.
    &#039;app/bootstrap.php&#039; =&gt; true,
    // ...
];
```

**Note**: The idea behind overwriting files is to make it easier to update the foundation. But, you can disable it by removing the `Derafu\\Foundation\\Installer::copyFiles` from the `post-install-cmd` and `post-update-cmd` scripts in your `composer.json` file.

## Extending the Structure

When creating a new project based on this foundation, you should:

1. Keep the existing directory structure.
2. Add your own directories as needed.
3. Follow PSR-4 autoloading standards for your PHP classes.
4. Place tests in the corresponding structure within the `tests/` directory.

The structure is designed to be flexible while providing a consistent organization pattern across projects.




---

### Assets

Frontend Asset Management

# Frontend Asset Management

Derafu Foundation includes a pre-configured asset build system using [Vite](https://vitejs.dev). This document explains how to work with frontend assets in your project.

## Overview

The asset management system handles:

- CSS compilation.
- JavaScript bundling.
- Image optimization.
- Source maps generation.
- Asset versioning.

## Directory Structure

```
project_dir/
├── assets/
│   ├── css/
│   │   └── app.css           # Main CSS file.
│   ├── img/                  # Image files.
│   └── js/
│       ├── app.js            # Main JavaScript entry point.
│       └── images.js         # Image import handling.
├── public/
│   └── static/               # Output directory (generated).
│       ├── css/
│       ├── img/
│       └── js/
└── vite.config.js            # Vite configuration.
```

## Configuration

The Vite configuration is defined in `vite.config.js`. Key features include:

- Multiple entry points: `app.js`, `app.css`, and `images.js`.
- Output directory: `public/static/`.
- Custom file naming with `.min` suffix.
- Image optimization using `vite-plugin-sharp`.

### Entry Points

- `app.js`: Main JavaScript code.
- `app.css`: Main CSS styles.
- `images.js`: Used for processing and importing images.

### Image Optimization

Images are automatically optimized during the build process using the Sharp image processing library. The configuration includes:

- PNG compression: Quality 85%, maximum compression level.
- JPEG compression: Quality 85%, progressive encoding.
- Automatic resizing of large images to a maximum of 2000×2000 pixels.

## Usage

### Adding Assets

1. **CSS**: Add your CSS files to the `assets/css/` directory.
2. **JavaScript**: Add your JavaScript files to the `assets/js/` directory.
3. **Images**: Add your images to the `assets/img/` directory.

### Importing Assets

#### CSS

In your main `app.css` file:

```css
/* Import other CSS files */
@import &#039;./components/buttons.css&#039;;
```

#### JavaScript

In your JavaScript files:

```javascript
// Import other JavaScript files.
import &#039;./components/slider.js&#039;;

// Import CSS from JavaScript.
import &#039;../css/components/modal.css&#039;;

// Import images.
import logo from &#039;../img/logo.png&#039;;
```

#### Images

For processing multiple images at once, use the `images.js` file:

```javascript
// Import all images in a directory.
import.meta.glob(&#039;../img/**/*&#039;);
```

### Building Assets

To build assets for production:

```shell
npm run build
```

This will:

1. Compile and bundle all assets.
2. Optimize images.
3. Generate minified files with source maps.
4. Output everything to the `public/static/` directory.

### Using Built Assets

In your HTML/Twig templates, reference the built assets:

```html
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://www.derafu.dev/static/css/app.min.css&quot;&gt;
&lt;script src=&quot;https://www.derafu.dev/static/js/app.min.js&quot;&gt;&lt;/script&gt;
&lt;img src=&quot;https://www.derafu.dev/static/img/logo.png&quot;&gt;
```

## Best Practices

1. **Organize by component**: Group related CSS, JS, and images by feature or component.
2. **Optimize images before adding**: While the build process optimizes images, starting with optimized images is better.
3. **Use CSS imports**: Keep CSS modular by using imports.
4. **Lazy load images**: For image-heavy pages, consider implementing lazy loading.
5. **Watch file size**: Monitor the size of your built assets to ensure optimal loading times.

## Advanced Configuration

The Vite configuration can be extended to support:

- CSS preprocessors (Sass, Less, etc.).
- TypeScript.
- Additional plugins for specific needs.
- Custom output paths and formats.

To modify the configuration, edit the `vite.config.js` file as needed (remember to deactivate the `Derafu\\Foundation\\Installer::copyFiles` from the `post-install-cmd` and `post-update-cmd` scripts in your `composer.json` file).




---

### Code Quality

Code Quality Tools

# Code Quality Tools

Derafu Foundation comes with pre-configured code quality tools to ensure consistent code style and identify potential issues early in development. This document explains how to use these tools effectively.

## PHP CS Fixer

PHP CS Fixer is a tool that automatically fixes PHP coding standards issues in your code according to rules you define.

### Configuration

The configuration is defined in `php-cs-fixer.php` at the root of your project. Key features include:

- PSR-12 coding standards by default.
- Strict type declarations enabled.
- Automatic array syntax conversion to short syntax.
- Ordered imports.
- Modern PHP features optimization (e.g., arrow functions).
- PHPUnit strict assertions.

### Usage

Run PHP CS Fixer to check code style issues:

```shell
vendor/bin/php-cs-fixer fix --dry-run --diff
```

Fix code style issues automatically:

```shell
vendor/bin/php-cs-fixer fix
```

## PHPStan

PHPStan is a static analysis tool that finds bugs in your code without running it. It&#039;s focused on finding errors in code logic.

### Configuration

The configuration is defined in `phpstan.neon` at the root of your project. By default, it:

- Sets analysis level to 5 (out of 9)
- Analyzes code in the `src` and `tests` directories

### Levels Explained

- Level 1: Basic checks.
- Level 2: Possibly undefined variables.
- Level 3: Return types, phpdocs.
- Level 4: Type hints.
- Level 5: Basic dead code detection.
- Level 6: Detecting unreachable code.
- Level 7: Union types.
- Level 8: More precise analysis
- Level 9: Mixed type detection

### Usage

Run PHPStan analysis:

```shell
vendor/bin/phpstan analyse
```

## PHPUnit

PHPUnit is the standard testing framework for PHP applications.

### Configuration

The configuration is defined in `phpunit.xml` at the root of your project. Key features include:

- Test coverage reporting enabled.
- Strict mode enabled (fails on warnings, notices, etc.).
- Test results output to `var/tests-coverage.txt` and `var/tests-coverage.xml`.
- Test documentation output to `var/tests-testdox.txt`.

### Directory Structure

- `tests/src/`: Unit tests for your source code.
- `tests/fixtures/`: Test data and fixtures.

### Usage

Run the test suite:

```shell
vendor/bin/phpunit
```

Generate test coverage reports:

```shell
vendor/bin/phpunit --coverage-html var/coverage
```

## Integration with CI/CD

These tools are integrated into the CI workflow (`.github/workflows/ci.yml`), which automatically runs on each push to the repository. This ensures that code quality standards are maintained throughout development.

## Best Practices

1. **Run tools locally before committing**: This helps catch issues before they enter the codebase.
2. **Gradually increase PHPStan level**: Start with level 5 and work toward higher levels as your project matures.
3. **Aim for high test coverage**: Write tests for all critical code paths.
4. **Update rules as needed**: Customize tool configurations to match your project&#039;s specific requirements (remember to deactivate the `Derafu\\Foundation\\Installer::copyFiles` from the `post-install-cmd` and `post-update-cmd` scripts in your `composer.json` file).




---

### GitHub Actions

GitHub Actions

# GitHub Actions

Derafu Foundation includes pre-configured GitHub Actions workflows for continuous integration (CI) and continuous deployment (CD). This document explains how these workflows work and how to customize them.

## Overview

The `.github/workflows/` directory contains two main workflow files:

- `ci.yml`: Continuous Integration workflow.
- `cd.yml`: Continuous Deployment workflow.

## Continuous Integration (CI)

The CI workflow runs automatically on each push and pull request to validate code quality and ensure tests pass.

### What It Does

The CI workflow:

1. Sets up multiple PHP versions for testing.
2. Installs dependencies using Composer.
3. Validates Composer configuration.
4. Runs PHP CS Fixer to check code style.
5. Runs PHPStan for static analysis.
6. Runs PHPUnit tests.
7. Generates test coverage reports.

### Customization

To customize the CI workflow, edit the `.github/workflows/ci.yml` file. Common customizations include:

- Changing PHP versions to test against.
- Adding or removing validation steps.
- Modifying test coverage thresholds.
- Changing notification settings.

Example of adding a new PHP version to test against:

```yaml
# In .github/workflows/ci.yml
jobs:
  tests:
    strategy:
      matrix:
        php-version: [&#039;8.3&#039;, &#039;8.4&#039;]  # Add or remove versions as needed.
```

## Continuous Deployment (CD)

The CD workflow automatically builds and deploys your project documentation to GitHub Pages when changes are pushed to the main branch.

**Note**: Deployment is disabled by default, just remove the `false &amp;&amp;` in the file to enable it.

### What It Does

The CD workflow:

1. Checks out the repository.
2. Sets up Node.js.
3. Installs frontend dependencies.
4. Builds assets using Vite.
5. Sets up PHP.
6. Installs Composer dependencies.
7. Generates a static website from your project documentation.
8. Deploys the static website to GitHub Pages.

### GitHub Pages Setup

To use the CD workflow, you need to:

1. Enable GitHub Pages in your repository settings.
2. Set the source branch to `gh-pages` and the directory to `/` (root).
3. Ensure the workflow has permission to write to the repository.

### Customization

To customize the CD workflow, edit the `.github/workflows/cd.yml` file. Common customizations include:

- Changing the deployment branch.
- Adding custom build steps.
- Configuring environment variables.
- Adding additional deployment targets.

Example of changing the deployment branch:

```yaml
# In .github/workflows/cd.yml
- name: Deploy to GitHub Pages
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./public
    publish_branch: documentation  # Change from gh-pages to another branch name.
```

## Workflow Badges

You can add workflow status badges to your README.md to show the current status of your workflows:

```markdown
![GitHub last commit](https://img.shields.io/github/last-commit/derafu/project/main)
![CI Workflow](https://github.com/derafu/project/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/project)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/project)
![Total Downloads](https://poser.pugx.org/derafu/project/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/project/d/monthly)
```

Replace `derafu/project` with your actual GitHub username and repository name.

## Best Practices

1. **Keep workflows fast**: Optimize workflows to complete quickly.
2. **Use caching**: Cache dependencies to speed up builds.
3. **Secure secrets**: Use GitHub Secrets for sensitive information.
4. **Test workflow changes**: Test workflow changes on a feature branch before merging to main.
5. **Monitor workflow runs**: Regularly check workflow runs for issues.

## Troubleshooting

If a workflow fails:

1. Check the workflow run logs in the GitHub Actions tab.
2. Verify that all dependencies are correctly installed.
3. Ensure environment variables are correctly set.
4. Check if tests are failing locally as well.
5. Look for GitHub Actions service status issues.

If needed, you can run the workflow manually from the Actions tab by selecting the workflow and clicking &quot;Run workflow&quot;.




---

## HTTP Project

Derafu HTTP

# Derafu HTTP




---

### Introduction

Standard-Compliant HTTP Library with Extended Features

# Standard-Compliant HTTP Library with Extended Features

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/http/main)
![CI Workflow](https://github.com/derafu/http/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/http)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/http)
![Total Downloads](https://poser.pugx.org/derafu/http/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/http/d/monthly)

A PSR and RFC compliant HTTP library that provides elegant request/response handling, content negotiation and problem details for PHP applications.

## Why Derafu\Http?

### 🎯 **Simple, but not limited, HTTP Handling**

Most HTTP libraries either do too much or too little. Derafu\Http provides:

- **Extended Request/Response**: Smart content type negotiation and safe data access.
- **Content Negotiation**: Intelligent format detection and response transformation.
- **Problem Details**: RFC 7807 implementation for structured error handling.
- **PSR Compliance**: Built on PSR-7 and PSR-15 standards.
- **Middleware Architecture**: Flexible request/response processing pipeline.

### ✨ **Key Features**

- **Smart Request Handling**: Safe access to query, post, and JSON data.
- **Intelligent Responses**: Automatic content negotiation and format transformation.
- **Structured Errors**: Complete RFC 7807 Problem Details implementation.
- **Modular Design**: Core HTTP functionality with middleware support.
- **Type Safety**: Use of enums for HTTP status codes and content types.
- **Middleware Pipeline**: PSR-15 compliant middleware chain for request processing.

## Installation

```bash
composer require derafu/http
```

## Basic Usage

### public/index.php

```php
use Derafu\Http\Kernel;
use Derafu\Kernel\Environment;

require_once dirname(__DIR__) . &#039;/app/bootstrap.php&#039;;

return fn (array $context): Kernel =&gt; new Kernel(new Environment(
    $context[&#039;APP_ENV&#039;],
    (bool) $context[&#039;APP_DEBUG&#039;],
    $context
));
```

### Middleware Configuration

The library uses PSR-15 middlewares for request processing. Configure your middleware stack in the services file, for example `services.yaml`:

```yaml
# Core middlewares in required order.
Psr\Http\Server\RequestHandlerInterface:
    class: Derafu\Http\Service\RequestHandler
    public: true
    arguments:
        $middlewares:
            - &#039;@Derafu\Http\Middleware\RequestFactoryMiddleware&#039;
            - &#039;@Derafu\Http\Middleware\RouterMiddleware&#039;
            - &#039;@Derafu\Http\Middleware\DispatcherMiddleware&#039;
            - &#039;@Derafu\Http\Middleware\ResponseNormalizerMiddleware&#039;

# Register individual middlewares.
Derafu\Http\Middleware\RequestFactoryMiddleware: ~
Derafu\Http\Middleware\RouterMiddleware: ~
Derafu\Http\Middleware\DispatcherMiddleware: ~
Derafu\Http\Middleware\ResponseNormalizerMiddleware: ~
```

### Core Middlewares

The library includes four essential middlewares that must be configured in order:

1. **RequestFactoryMiddleware**: Converts PSR-7 requests to Derafu requests.
2. **RouterMiddleware**: Handles URL routing and route matching.
3. **DispatcherMiddleware**: Executes route handlers (controllers, closures or templates).
4. **ResponseNormalizerMiddleware**: Ensures PSR-7 compliant responses.

### Custom Middlewares

Create custom middlewares by implementing PSR-15&#039;s `MiddlewareInterface`:

```php
class CustomMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Process request.
        $response = $handler-&gt;handle($request);
        // Process response.
        return $response;
    }
}
```

### Working with Requests

```php
// Safe access to request data.
$id = $request-&gt;query(&#039;id&#039;, 0);
$data = $request-&gt;json();
$file = $request-&gt;file(&#039;document&#039;);

// Content negotiation.
$format = $request-&gt;getPreferredFormat();
```

### Creating Responses

```php
// Automatic content negotiation.
// Returns JSON for API requests in `/api` paths
return [&#039;status&#039; =&gt; &#039;ok&#039;];

// Explicit responses.
return (new Response())-&gt;asJson($data)-&gt;withHttpStatus(HttpStatus::CREATED);

// Redirect.
return (new Response())-&gt;redirect(&#039;https://www.example.com&#039;);
```

### Error Handling with Problem Details

Any exception will be treated as &quot;Problem Detail&quot; (RFC 7807). To have control over all fields, exceptions must implement `HttpExceptionInterface`.

```json
{
    &quot;type&quot;: &quot;about:blank&quot;,
    &quot;title&quot;: &quot;Not Found&quot;,
    &quot;status&quot;: 404,
    &quot;detail&quot;: &quot;No route found for \&quot;/api/mustFail\&quot;.&quot;,
    &quot;instance&quot;: &quot;/api/mustFail&quot;,
    &quot;extensions&quot;: {
        &quot;timestamp&quot;: &quot;2025-02-20T22:04:25+00:00&quot;,
        &quot;environment&quot;: &quot;dev&quot;,
        &quot;debug&quot;: true,
        &quot;context&quot;: [],
        &quot;throwable&quot;: null
    }
}
```

## Integration with Other Packages

Derafu\Http is designed to work with other Derafu packages:

### Required

- **derafu/kernel**: The core of the application, with dependency injection.
- **derafu/renderer**: For template rendering, with dependency on **derafu/twig**.
- **derafu/routing**: For URL routing, with autodiscovery of templates.
- **derafu/translation**: For I18n, with a simple translator that supports ICU.

### Optional

- **derafu/markdown**: For renderig templates in markdown format.

### Suggested

- **derafu/data-processor**: For data processing: format, cast, sanitize and validate.




---

### HTTP Flow

Flow - Detailed Explanation

# Flow - Detailed Explanation

The Derafu HTTP component implements a clean, modular approach to handling HTTP requests in PHP applications. The flow diagram illustrates how an HTTP request travels through the system, from the initial client request to the final response delivery.

{.w-75 .mx-auto}
![HTTP Flow](https://www.derafu.dev/img/diagrams/content/docs/core/http/derafu-http-flow.svg)

## Key Components

### Client

The external entity (browser, API consumer, etc.) that initiates the HTTP request and receives the response.

### Runtime
The application&#039;s entry point and execution environment. It serves as the orchestrator of the entire HTTP flow, responsible for:

- Bootstrapping the application.
- Creating the PSR-7 request object from the HTTP request.
- Initializing the kernel.
- Delegating request processing.
- Sending the response back to the client.

### Kernel

The core of the application, responsible for:

- Building and configuring the dependency injection container.
- Loading application configurations.
- Managing the application lifecycle.
- Coordinating the request handling process.

The Kernel implements a micro-kernel architecture that keeps the core small and efficient while pushing most functionality to handlers and middleware.

### Request Handler

Processes the HTTP request and produces a response. Handlers:

- Receive the request from the kernel.
- Apply application logic.
- Generate an appropriate response.
- May use services from the container to fulfill the request.

### Request

A PSR-7 compliant ServerRequest object that represents the HTTP request. It encapsulates:

- HTTP method.
- URI and query parameters.
- Headers.
- Body content.
- Server and environment variables.

### Response

A PSR-7 compliant Response object that represents the HTTP response. It includes:

- Status code.
- Headers.
- Response body.

### Container

The dependency injection container that:

- Manages service instantiation and configuration.
- Provides service dependencies throughout the application.
- Implements the PSR-11 container interface.
- Supports autowiring for simplified service definition.

## The HTTP Request/Response Flow

1. **Client Sends HTTP Request**
   The flow begins when a client makes an HTTP request to the application.

2. **Runtime Creates Request Object**
   The Runtime transforms the raw HTTP request into a PSR-7 compliant Request object, preparing it for processing.

3. **Runtime Initializes Kernel**
   The Kernel is initialized with the necessary configurations to process the current request.

4. **Kernel Builds Container**
   The Kernel builds and configures the dependency injection container, making all application services available.

5. **Runtime Delegates Request Processing**
   The Runtime passes the Request object to the Kernel for processing.

6. **Kernel Delegates to Handler**
   The Kernel identifies the appropriate Request Handler based on routing information and delegates the request processing.

7. **Handler Generates Response**
   The Handler applies business logic, interacts with the application services as needed, and generates a Response object.

8. **Response Returned to Runtime**
   The generated Response is returned through the call chain back to the Runtime.

9. **Runtime Sends Response to Client**
   The Runtime outputs the Response to the client, completing the HTTP cycle.




---

### Design Principles

Design Principles

# Design Principles

The Derafu HTTP component provides a lightweight, standards-compliant approach to HTTP request handling. By following established PHP interoperability standards (PSRs) and sound architectural principles, it enables the development of robust, maintainable web applications with minimal overhead.

The Derafu HTTP is designed with several key principles in mind:

## PSR Compliance

- Implements PSR-7 for HTTP messages.
- Follows PSR-11 for container interoperability.
- Supports PSR-15 for middleware.

## Separation of Concerns

Each component has a single, well-defined responsibility:

- Runtime manages the application lifecycle.
- Kernel coordinates processing.
- Handlers implement business logic.
- Container manages dependencies.

## Flexibility

The design allows for:

- Custom request handlers.
- Middleware integration.
- Extensible container configuration.
- Multiple runtime environments.

## Testability

The clear separation of components and dependency injection make unit testing straightforward:

- Mock the container for handler tests.
- Create test requests easily.
- Validate responses without invoking the full stack.

## Implementation Guidelines

When working with the Derafu HTTP component:

1. **Define Request Handlers** for different routes or endpoints.
2. **Configure the Container** with your application services.
3. **Set up your routes** to map URLs to handlers.
4. **Extend the base classes** as needed for custom functionality.

The architecture is designed to be minimal yet powerful, allowing developers to focus on the business logic rather than HTTP processing details.




---

## Routing Project

Derafu Routing

# Derafu Routing




---

### Introduction

Elegant PHP Router with Plugin Architecture

# Elegant PHP Router with Plugin Architecture

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/routing/main)
![CI Workflow](https://github.com/derafu/routing/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/routing)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/routing)
![Total Downloads](https://poser.pugx.org/derafu/routing/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/routing/d/monthly)

A lightweight, extensible PHP routing library that combines simplicity with power through its parser-based architecture.

## Features

- 🔌 Plugin architecture with swappable parsers.
- 🎯 Multiple routing strategies (static, dynamic, filesystem).
- 🧩 Easy to extend with custom parsers.
- 📁 Built-in filesystem routing for static sites.
- 🔄 Support for different content types (.md, .twig, etc.).
- 🛠️ Clean separation of concerns.
- 🪶 Lightweight with zero dependencies.
- ⚡ Fast pattern matching.
- 🧪 Comprehensive test coverage.
- 🔗 URL generation for named routes.

## Why Derafu\Routing?

Unlike traditional monolithic routers, Derafu\Routing uses a unique parser-based architecture that offers several advantages:

- **Modularity**: Each routing strategy is encapsulated in its own parser.
- **Flexibility**: Easy to add new routing patterns without modifying existing code.
- **Clarity**: Clear separation between route matching and request handling.
- **Extensibility**: Add custom parsers for specific routing needs.
- **Predictability**: Each parser has a single responsibility.
- **Performance**: Only load the parsers you need.

## Installation

Install via Composer:

```bash
composer require derafu/routing
```

## Basic Usage

```php
use Derafu\Routing\Router;
use Derafu\Routing\Dispatcher;
use Derafu\Routing\Parser\StaticParser;
use Derafu\Routing\Parser\FileSystemParser;

// Create and configure router.
$router = new Router();
$router-&gt;addParser(new StaticParser());
$router-&gt;addParser(new FileSystemParser([__DIR__ . &#039;/pages&#039;]));

// Add routes.
$router-&gt;addRoute(&#039;/&#039;, &#039;HomeController::index&#039;, name: &#039;home&#039;);
$router-&gt;addDirectory(__DIR__ . &#039;/pages&#039;);

// Create and configure dispatcher.
$dispatcher = new Dispatcher([
    &#039;md&#039; =&gt; fn ($file, $params) =&gt; renderMarkdown($file),
    &#039;twig&#039; =&gt; fn($file, $params) =&gt; renderTwig($file, $params),
]);

// Handle request.
try {
    $route = $router-&gt;match();
    echo $dispatcher-&gt;dispatch($route);
} catch (RouterException $e) {
    // Handle error.
}
```

## Available Parsers

### StaticParser

Handles exact route matches:

```php
$router-&gt;addRoute(&#039;/about&#039;, &#039;PagesController::about&#039;, name: &#039;about&#039;);
```

### DynamicParser

Supports parameters and patterns:

```php
$router-&gt;addRoute(&#039;/users/{id:\d+}&#039;, &#039;UserController::show&#039;, name: &#039;user.show&#039;);
$router-&gt;addRoute(&#039;/blog/{year}/{slug}&#039;, &#039;BlogController::post&#039;, name: &#039;blog.post&#039;);
```

### FileSystemParser

Maps URLs to files in directories:

```php
$router-&gt;addDirectory(__DIR__ . &#039;/pages&#039;);
// Examples:
// /about maps to /pages/about.md
// /contact maps to /pages/contact.html.twig
```

## URL Generation

Generate URLs for named routes:

```php
// Set request context (needed for absolute URLs).
$router-&gt;setContext(new RequestContext(
    baseUrl: &#039;/myapp&#039;,
    scheme: &#039;https&#039;,
    host: &#039;example.com&#039;
));

// Generate URLs.
$url = $router-&gt;generate(&#039;user.show&#039;, [&#039;id&#039; =&gt; 123]); // /myapp/users/123
$url = $router-&gt;generate(&#039;blog.post&#039;, [
    &#039;year&#039; =&gt; &#039;2024&#039;,
    &#039;slug&#039; =&gt; &#039;hello-world&#039;
]); // /myapp/blog/2024/hello-world

// Generate absolute URL.
$url = $router-&gt;generate(&#039;about&#039;, [], UrlReferenceType::ABSOLUTE_URL);
// https://example.com/myapp/about
```

## Creating Custom Parsers

Implement your own routing strategy by creating a parser:

```php
class CustomParser implements ParserInterface
{
    public function parse(string $uri, array $routes): ?RouteMatch
    {
        // Your custom routing logic.
    }

    public function supports(Route $route): bool
    {
        // Define what routes this parser can handle.
    }
}

$router-&gt;addParser(new CustomParser());
```

## File-based Routing Example

Perfect for static sites:

```
pages/
├── about.md
├── contact.html.twig
└── blog/
    ├── post-1.md
    └── post-2.md
```

URLs are automatically mapped to files:

- `/about` → `pages/about.md`
- `/contact` → `pages/contact.html.twig`
- `/blog/post-1` → `pages/blog/post-1.md`




---

### Guide

Complete Usage Guide

# Complete Usage Guide

Derafu Routing is a flexible PHP routing library that uses a parser-based architecture. Instead of having a monolithic router, it separates routing logic into specialized parsers, each handling different types of routes.

## Installation

```bash
composer require derafu/routing
```

## Basic Concepts

### Parser Architecture

The routing system is built around specialized parsers:

1. **StaticParser**: Handles exact route matches.
2. **DynamicParser**: Processes routes with parameters.
3. **FileSystemParser**: Maps URLs to physical files.

Each parser implements the `ParserInterface`:

```php
interface ParserInterface {
    public function parse(string $uri, array $routes): ?RouteMatchInterface;
    public function supports(RouteInterface $route): bool;
}
```

## Route Types

### Static Routes

The simplest form of routing, handled by `StaticParser`:

```php
$router = new Router([new StaticParser()]);
$router-&gt;addRoute(&#039;/about&#039;, &#039;PagesController::action&#039;, name: &#039;about&#039;);
$router-&gt;addRoute(&#039;/contact&#039;, &#039;ContactController::show&#039;, name: &#039;contact&#039;);
```

### Dynamic Routes

Handled by `DynamicParser`, supporting various parameter types:

```php
$router-&gt;addParser(new DynamicParser());

// Basic parameter.
$router-&gt;addRoute(&#039;/users/{id}&#039;, &#039;UserController::show&#039;, name: &#039;user.show&#039;);

// Validation with regular expressions.
$router-&gt;addRoute(&#039;/users/{id:\d+}&#039;, &#039;UserController::show&#039;, name: &#039;user.show&#039;);

// Optional parameters.
$router-&gt;addRoute(&#039;/blog/{year?}&#039;, &#039;BlogController::index&#039;, name: &#039;blog.index&#039;);

// Multiple parameters.
$router-&gt;addRoute(&#039;/blog/{year}/{month?}&#039;, &#039;BlogController::archive&#039;, name: &#039;blog.archive&#039;);

// Complex patterns.
$router-&gt;addRoute(&#039;/users/{username:[a-z0-9_-]+}&#039;, &#039;UserController::profile&#039;, name: &#039;user.profile&#039;);
```

### Filesystem Routes

The `FileSystemParser` maps URLs to actual files:

```php
$parser = new FileSystemParser(
    directories: [__DIR__ . &#039;/pages&#039;],
    extensions: [&#039;.html.twig&#039;, &#039;.md&#039;]
);
$router-&gt;addParser($parser);
```

Directory structure:
```
pages/
├── about.md           # Matches /about
├── contact.twig       # Matches /contact
└── blog/
    ├── post-1.md     # Matches /blog/post-1
    └── post-2.md     # Matches /blog/post-2
```

## Using the Router

### Basic Configuration

```php
use Derafu\Routing\Router;
use Derafu\Routing\Parser\StaticParser;
use Derafu\Routing\Parser\DynamicParser;

$router = new Router([
    new StaticParser(),
    new DynamicParser(),
]);
```

### Adding Routes

```php
// String handler (Controller@action).
$router-&gt;addRoute(&#039;/users&#039;, &#039;UserController::index&#039;, name: &#039;users.index&#039;);

// Closure handler.
$router-&gt;addRoute(&#039;/api/data&#039;, function($params) {
    return [&#039;data&#039; =&gt; &#039;value&#039;];
}, name: &#039;api.data&#039;);

// Array handler.
$router-&gt;addRoute(&#039;/blog&#039;, [
    &#039;controller&#039; =&gt; &#039;BlogController&#039;,
    &#039;action&#039; =&gt; &#039;list&#039;
], name: &#039;blog.index&#039;);

// Routes with name and parameters.
$router-&gt;addRoute(
    route: &#039;/users/{id}&#039;,
    handler: &#039;UserController::show&#039;,
    name: &#039;user.show&#039;,
    parameters: [&#039;active&#039; =&gt; true]
);
```

### Route Matching

```php
try {
    $match = $router-&gt;match(&#039;/users/123&#039;);
    // $match-&gt;getHandler(): Returns the route handler.
    // $match-&gt;getParameters(): Returns the route parameters.
    // $match-&gt;getName(): Returns the route name if defined.
} catch (RouteNotFoundException $e) {
    // Handle 404.
}
```

### URL Generation

The router allows generating URLs from named routes:

```php
// Set request context (needed for absolute URLs).
$router-&gt;setContext(new RequestContext(
    baseUrl: &#039;/myapp&#039;,
    scheme: &#039;https&#039;,
    host: &#039;example.com&#039;
));

// Generate relative URLs.
$url = $router-&gt;generate(&#039;user.show&#039;, [&#039;id&#039; =&gt; 123]); // /myapp/users/123
$url = $router-&gt;generate(&#039;blog.archive&#039;, [
    &#039;year&#039; =&gt; &#039;2024&#039;,
    &#039;month&#039; =&gt; &#039;03&#039;
]); // /myapp/blog/2024/03

// Generate URL without optional parameter.
$url = $router-&gt;generate(&#039;blog.archive&#039;, [
    &#039;year&#039; =&gt; &#039;2024&#039;
]); // /myapp/blog/2024

// Generate absolute URL.
$url = $router-&gt;generate(&#039;about&#039;, [], UrlReferenceType::ABSOLUTE_URL);
// https://example.com/myapp/about

// Generate network path URL.
$url = $router-&gt;generate(&#039;about&#039;, [], UrlReferenceType::NETWORK_PATH);
// //example.com/myapp/about
```

Available reference types are:

- `ABSOLUTE_PATH`: Absolute path from root (default).
- `ABSOLUTE_URL`: Complete URL with scheme and host.
- `NETWORK_PATH`: URL without scheme (useful for resources that work on both HTTP and HTTPS).

## The Dispatcher

The dispatcher handles the execution of matching routes:

```php
$dispatcher = new Dispatcher([
    &#039;md&#039; =&gt; function($file, $params) {
        // Render markdown file.
        return parseMarkdown(file_get_contents($file));
    },
    &#039;twig&#039; =&gt; function($file, $params) {
        // Render Twig template.
        return $twig-&gt;render($file, $params);
    }
]);

$result = $dispatcher-&gt;dispatch($match);
```

**Note**: This is a very basic *dispatcher*, you should implement your own.

## Advanced Usage

### Custom Parser Example

```php
class RegexParser implements ParserInterface
{
    public function parse(string $uri, array $routes): ?RouteMatchInterface
    {
        foreach ($routes as $route) {
            if (!$this-&gt;supports($route)) {
                continue;
            }

            // Custom regex matching logic
            if (preg_match($route-&gt;getPath(), $uri, $matches)) {
                return new RouteMatch(
                    $route-&gt;getHandler(),
                    $matches
                );
            }
        }
        return null;
    }

    public function supports(RouteInterface $route): bool
    {
        // Define what routes this parser can handle
        return str_starts_with($route-&gt;getPath(), &#039;#&#039;);
    }
}
```

## Best Practices

1. **Parser Order**: Add parsers in order of specificity.
   - StaticParser first (faster, more specific).
   - DynamicParser next.
   - FileSystemParser last (more flexible but slower).

2. **Route Organization**: Group related routes.
   ```php
   // User management
   $router-&gt;addRoute(&#039;/users&#039;, &#039;UserController::index&#039;, name: &#039;users.index&#039;);
   $router-&gt;addRoute(&#039;/users/{id}&#039;, &#039;UserController::show&#039;, name: &#039;users.show&#039;);

   // Blog system
   $router-&gt;addRoute(&#039;/blog&#039;, &#039;BlogController::index&#039;, name: &#039;blog.index&#039;);
   $router-&gt;addRoute(&#039;/blog/{slug}&#039;, &#039;BlogController::show&#039;, name: &#039;blog.show&#039;);
   ```

3. **Parameter Validation**: Use regex constraints for better security.
   ```php
   // Ensure ID is numeric.
   $router-&gt;addRoute(&#039;/users/{id:\d+}&#039;, &#039;UserController::show&#039;, name: &#039;users.show&#039;);

   // Validate username format.
   $router-&gt;addRoute(&#039;/users/{username:[a-z0-9_-]+}&#039;, &#039;UserController::profile&#039;, name: &#039;users.profile&#039;);
   ```

4. **Error Handling**: Always wrap matches in try-catch.
   ```php
   try {
       $match = $router-&gt;match($uri);
       $result = $dispatcher-&gt;dispatch($match);
   } catch (RouteNotFoundException $e) {
       // Handle 404.
   } catch (DispatcherException $e) {
       // Handle dispatcher errors.
   }
   ```

5. **URL Generation**: Always use route names instead of hardcoded URLs.
   ```php
   // Bad
   $url = &#039;/users/&#039; . $id;

   // Good
   $url = $router-&gt;generate(&#039;users.show&#039;, [&#039;id&#039; =&gt; $id]);
   ```

6. **Request Context**: Configure context if absolute URLs are needed.
   ```php
   $router-&gt;setContext(new RequestContext(
       baseUrl: &#039;/myapp&#039;,
       scheme: &#039;https&#039;,
       host: &#039;example.com&#039;,
       httpPort: 80,
       httpsPort: 443
   ));
   ```




---

## Kernel Project

Lightweight Kernel Implementation with DI Container

# Lightweight Kernel Implementation with DI Container

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/kernel/main)
![CI Workflow](https://github.com/derafu/kernel/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/kernel)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/kernel)
![Total Downloads](https://poser.pugx.org/derafu/kernel/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/kernel/d/monthly)

A lightweight kernel implementation with a dependency injection container, inspired by Symfony but with a minimal approach.

## Overview

Derafu Kernel provides a clean, flexible foundation for PHP applications with minimal dependencies. It offers:

- A lightweight kernel implementation with DI container.
- Environment-aware configuration management.
- Support for PHP and YAML configuration files.
- Container caching for improved performance.
- Built-in support for multiple environments (dev, prod, test, etc.).

## Installation

```bash
composer require derafu/kernel
```

## Basic Usage

### Create a Kernel

The simplest way to use the kernel is to create an instance with a specific environment:

```php
use Derafu\Kernel\MicroKernel;

// Create a kernel with &#039;dev&#039; environment and debug mode enabled.
$kernel = new MicroKernel(&#039;dev&#039;, true);

// Boot the kernel to initialize the container.
$kernel-&gt;boot();
```

### Custom Kernel Implementation

You can extend the `MicroKernel` class to customize its behavior:

```php
use Derafu\Kernel\MicroKernel;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

class AppKernel extends MicroKernel
{
    // Override the default configuration files.
    protected const CONFIG_FILES = [
        &#039;services.php&#039; =&gt; &#039;php&#039;,
        &#039;routes.php&#039; =&gt; &#039;routes&#039;,
        &#039;parameters.yaml&#039; =&gt; &#039;yaml&#039;,
    ];

    // Add additional container configuration.
    protected function configure(ContainerConfigurator $configurator): void
    {
        $services = $configurator-&gt;services();

        // Register application services.
        $services-&gt;set(&#039;app.service&#039;, MyService::class)
            -&gt;public()
            -&gt;args([&#039;param1&#039;, &#039;param2&#039;]);
    }
}
```

### Environment Configuration

The kernel uses an environment object to determine settings and directories:

```php
use Derafu\Kernel\Environment;

// Create an environment with custom settings.
$environment = new Environment(
    &#039;prod&#039;,                 // Environment name.
    false,                  // Debug mode.
    [&#039;custom&#039; =&gt; &#039;value&#039;]   // Context variables.
);

// Create a kernel with the custom environment.
$kernel = new MicroKernel($environment);
```

## Configuration

### Directory Structure

The kernel expects a `config` directory for configurations. Any other structure is free.

### Service Configuration

Define services in `config/services.php`:

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $configurator) {
    $services = $configurator-&gt;services();

    // Auto-configure and autowire services by default.
    $services-&gt;defaults()
        -&gt;autowire()
        -&gt;autoconfigure();

    // Register a single service.
    $services-&gt;set(&#039;app.service&#039;, AppService::class)
        -&gt;public();

    // Register multiple services from namespace.
    $services-&gt;load(&#039;App\\Service\\&#039;, &#039;../src/Service/*&#039;)
        -&gt;public();
};
```

### Route Configuration

Define routes in `config/routes.php` or `config/routes.yaml`:

```php
// routes.php
return [
    &#039;home&#039; =&gt; [
        &#039;path&#039; =&gt; &#039;/&#039;,
        &#039;controller&#039; =&gt; &#039;App\\Controller\\HomeController::index&#039;,
    ],
    &#039;blog_show&#039; =&gt; [
        &#039;path&#039; =&gt; &#039;/blog/{slug}&#039;,
        &#039;controller&#039; =&gt; &#039;App\\Controller\\BlogController::show&#039;,
        &#039;parameters&#039; =&gt; [
            &#039;requirements&#039; =&gt; [
                &#039;slug&#039; =&gt; &#039;[a-z0-9-]+&#039;,
            ],
        ],
    ],
];
```

Or in YAML:

```yaml
# routes.yaml
home:
    path: /
    controller: App\Controller\HomeController::index

blog_show:
    path: /blog/{slug}
    controller: App\Controller\BlogController::show
    parameters:
        requirements:
            slug: &#039;[a-z0-9-]+&#039;
```

## Environment Types

The kernel supports multiple environment types through constants in `EnvironmentInterface`:

- `LOCAL`: Local development environment.
- `DEVELOPMENT`: Development environment.
- `TEST`: Testing environment.
- `STAGING`: Staging environment.
- `QUALITY_ASSURANCE`: QA environment.
- `PREPRODUCTION`: Pre-production environment.
- `PRODUCTION`: Production environment.




---

## Backbone Project

Derafu Backbone

# Derafu Backbone




---

### Introduction

The Architectural Spine for PHP Libraries

# The Architectural Spine for PHP Libraries

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/backbone/main)
![CI Workflow](https://github.com/derafu/backbone/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/backbone)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/backbone)
![Total Downloads](https://poser.pugx.org/derafu/backbone/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/backbone/d/monthly)

Derafu Backbone is a lightweight architectural framework that provides a consistent structure for building modular, maintainable PHP libraries.

## Features

- **Hierarchical Organization**: Clear structure with Packages, Components, and Workers.
- **Separation of Concerns**: Jobs for atomic operations, Handlers for orchestration, Strategies for implementation variants.
- **Attribute-Based Discovery**: Use PHP 8 attributes instead of rigid namespace conventions.
- **Extensible Architecture**: Designed to grow with your application.

## Key Benefits

- **Consistent Structure**: Standardized approach to organizing domain logic.
- **Reduced Complexity**: Clear responsibilities for each architectural element.
- **Improved Testability**: Isolated components are easier to test.
- **Enhanced Collaboration**: Common vocabulary and patterns for development teams.
- **Flexible Implementation**: Adapt to different domains without changing the core architecture.

## Installation

```bash
composer require derafu/backbone
```

## Quick Example

```php
#[Package(name: &#039;billing&#039;)]
class BillingPackage extends AbstractPackage implements PackageInterface
{
    // Package implementation.
}

#[Component(name: &#039;document&#039;, package: &#039;billing&#039;)]
class DocumentComponent extends AbstractComponent implements ComponentInterface
{
    // Component implementation.
}

#[Worker(name: &#039;renderer&#039;, component: &#039;document&#039;, package: &#039;billing&#039;)]
class RendererWorker extends AbstractWorker implements WorkerInterface
{
    // Worker implementation.
}
```




---

### Architecture

Architecture

# Architecture

Derafu Backbone provides a structured framework for building modular and maintainable PHP libraries. It follows a hierarchical organization that separates concerns into distinct components, making your code more organized, testable, and extensible.

{.w-75 .mx-auto}
![Derafu Backbone Core Architecture](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-architecture.svg)

### Package

**Definition**: A high-level container representing a complete domain or subdomain within your application.

**Responsibility**: Groups functionally related components that work together to provide domain-specific capabilities.

**Characteristics**:

- Represents a complete business domain (e.g., Billing, Accounting, Human Resources).
- Contains multiple components.
- Provides domain-wide configuration and services.
- Acts as the primary entry point for domain functionality.

**Examples**: `BillingPackage`, `AccountingPackage`, `HumanResourcesPackage`

### Component

**Definition**: A functional area or module within a domain package.

**Responsibility**: Groups workers that handle specific aspects of the domain.

**Characteristics**:

- Represents a distinct functional area within a domain.
- Contains multiple workers focused on related tasks.
- Provides component-specific configuration.
- Manages cross-cutting concerns within its functional area.

**Examples**: `DocumentComponent`, `ExchangeComponent`, `TradingPartiesComponent`

### Worker

**Definition**: Coordinator of business operations related to a specific aspect of functionality.

**Responsibility**: Exposes public methods that can be called by clients and coordinates Jobs, Handlers, and Strategies to perform work.

**Characteristics**:

- Acts as a facade for the underlying implementation.
- Exposes domain operations through public methods.
- Coordinates the execution of jobs and handlers.
- May implement simple operations directly.
- Delegates complex processing to handlers.

**Examples**: `BuilderWorker`, `RendererWorker`

### Job

**Definition**: An atomic, self-contained unit of work that performs a single operation.

**Responsibility**: Executes a specific task with clear inputs and outputs.

**Characteristics**:

- Focused on doing one thing well.
- Encapsulates a single operation.
- Has clear, well-defined inputs and outputs.
- Does not orchestrate other jobs.
- Reusable across different contexts.
- Stateless and idempotent when possible.

**Examples**: `NormalizeBoletaAfectaJob`, `NormalizeFacturaAfectaJob`

### Handler

**Definition**: Orchestrator of complex processes involving multiple operations.

**Responsibility**: Coordinates multiple jobs or strategies to complete complex workflows.

**Characteristics**:

- Orchestrates multiple operations in sequence.
- Manages workflow and process state.
- Selects appropriate strategies based on context.
- Handles errors and transactional boundaries.
- Implements business process logic.
- Can use multiple jobs and strategies.

**Examples**: `EmailSenderHandler`, `SiiSenderHandler`

### Strategy

**Definition**: Specific implementation of an algorithm or approach to solving a problem.

**Responsibility**: Provides variant implementations for specific operations that can be interchanged.

**Characteristics**:

- Implements a specific algorithm or approach.
- Allows for swappable implementations.
- Used by handlers to provide different behaviors.
- Focuses on &quot;how&quot; something is done.
- Follows the Strategy design pattern.
- Encapsulates specific implementation details.

**Examples**: `JsonParserStrategy`, `XmlParserStrategy`, `YamlParserStrategy`

## Benefits of This Architecture

1. **Separation of Concerns**: Each component has a clear, single responsibility.
2. **Modularity**: Components can be developed, tested, and deployed independently.
3. **Extensibility**: New implementations can be added without modifying existing code.
4. **Testability**: Clear boundaries make testing easier and more focused.
5. **Maintainability**: Organized structure makes the codebase easier to understand and maintain.
6. **Flexibility**: Strategies allow for varying implementations without changing coordination logic.

## Design Pattern Comparisons

- **Jobs**: Similar to the Command Pattern.
- **Handlers**: Combination of Chain of Responsibility and Mediator patterns.
- **Strategies**: Direct implementation of the Strategy Pattern.
- **Workers**: Follow the Facade Pattern.
- **Overall Structure**: Influenced by Hexagonal/Ports and Adapters Architecture.

This architecture provides a clear separation of responsibilities, allowing for component reuse and making your system more adaptable to changing requirements.




---

### Relationships

Relationships and Interactions

# Relationships and Interactions

{.w-50 .mx-auto}
![Derafu Backbone Relationships](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-relationships.svg)

## Package → Component Relationship

- A Package contains multiple Components.
- Components within a Package are thematically related.
- Components access Package-level configuration and services.

## Component → Worker Relationship

- A Component contains multiple Workers.
- Workers are grouped logically within a Component.
- Workers share Component-level resources and configuration.

## Worker → Job/Handler/Strategy Relationship

- Workers expose public methods that can be called by clients.
- Workers delegate to Jobs for simple operations.
- Workers delegate to Handlers for complex workflows.
- Workers don&#039;t typically interact directly with Strategies (Handlers do).

## Handler → Job/Strategy Relationship

- Handlers coordinate the execution of multiple Jobs.
- Handlers select and use appropriate Strategies based on context.
- Handlers implement orchestration logic while Jobs and Strategies provide implementation.

## Strategy Relationships

- Strategies implement interchangeable algorithms.
- Handlers select appropriate Strategies based on conditions.
- Strategies focus on specific implementation details.




---

### Workflow

Typical Workflow

# Typical Workflow

{.w-75 .mx-auto}
![Derafu Backbone Workflow](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-workflow.svg)

1. A client interacts with a public method on a Worker.
2. The Worker can:
   - Execute simple logic internally.
   - Delegate to a Job for a specific task.
   - Delegate to a Handler for a complex process.

3. If a Handler is used:
   - The Handler coordinates the workflow.
   - May execute multiple Jobs in sequence.
   - May select different Strategies based on context.
   - Manages errors and transactions.

4. Strategies allow for varying the implementation of specific steps without changing the Handler&#039;s logic.




---

### Decision Flow

Decision Flow: Choosing the Right Component

# Decision Flow: Choosing the Right Component

This document expands on the Decision Flow Diagram, helping you understand when to use each architectural component in Derafu Backbone.

{.w-75 .mx-auto}
![Decision Flow](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-decision-flow.svg)

## Understanding the Decision Process

When implementing a new piece of functionality in Derafu Backbone, one of the most important decisions is determining which architectural component should handle that functionality. The diagram provides a decision tree to guide this process, but let&#039;s explore the reasoning and implications of each choice.

### When to Use Jobs

Jobs are the workhorses of Backbone architecture. You should choose a Job when:

- The operation performs a **single, well-defined task**
- The operation has **clear inputs and outputs**
- The operation doesn&#039;t need to manage complex flows or state
- The operation could potentially be reused in different contexts

Jobs are particularly valuable when you need to implement operations that might be used by multiple handlers or directly by workers. They represent atomic units of work that should follow the Single Responsibility Principle.

**Example scenarios for Jobs**:
- Validating input data
- Sending a notification
- Storing an entity in a repository
- Transforming data from one format to another
- Performing a calculation

### When to Use Handlers

Handlers should be your choice when:

- The operation involves **multiple steps** or jobs
- You need to **orchestrate a complex workflow**
- There&#039;s **conditional logic** determining the flow
- The operation manages **transactional boundaries**
- You need to **coordinate between different components**

Handlers encapsulate complex business processes and provide a higher level of abstraction. They know how to execute a complete business use case by coordinating the execution of multiple jobs and selecting appropriate strategies.

**Example scenarios for Handlers**:
- Processing a payment (validating, charging, recording transaction, sending receipt)
- Generating a report (gathering data, applying business rules, formatting, delivering)
- User registration flow (validating, creating account, sending welcome email, initializing user settings)

### When to Use Strategies

Strategies are the right choice when:

- You need **multiple implementations** of the same operation
- The implementation should be **selectable at runtime**
- The implementation choice depends on **context or configuration**
- You want to **eliminate conditional logic** from your handlers and jobs

Strategies allow your system to adapt to different circumstances without changing the orchestration logic. They encapsulate the &quot;how&quot; of an operation, while jobs and handlers focus on the &quot;what&quot;.

**Example scenarios for Strategies**:
- Different export formats (PDF, CSV, Excel)
- Multiple payment processors (Stripe, PayPal, Bank Transfer)
- Various notification channels (Email, SMS, Push Notification)
- Different storage backends (File System, S3, Database)

### When to Use Direct Worker Implementation

In some cases, you might implement functionality directly in the Worker:

- For very simple operations that don&#039;t warrant a separate Job
- For operations that are specific to a particular Worker and won&#039;t be reused
- When acting as a simple facade over third-party libraries
- For convenience methods that combine calls to Jobs or Handlers

## Practical Applications

### Component Relationships

Notice how Jobs and Strategies are often used by Handlers. This relationship is key to understanding the architecture:

- **Jobs** provide the atomic operations
- **Handlers** orchestrate these operations
- **Strategies** provide implementation variants

### Benefits of Following This Decision Flow

By correctly choosing the appropriate architectural component:

1. **Improved Testability**: Jobs and Strategies are easier to test in isolation.
2. **Enhanced Maintainability**: Clear separation of concerns makes the code more maintainable.
3. **Greater Flexibility**: Strategies enable system adaptability without widespread changes.
4. **Better Reusability**: Jobs can be reused across multiple contexts.
5. **Clearer Intent**: The architecture communicates the intent of each piece of code.

### Edge Cases and Considerations

- **Size and Complexity**: Very simple applications might not need the full hierarchy. Start with Jobs and add Handlers and Strategies as complexity grows.
- **Performance**: While this architecture promotes good design, be mindful of potential overhead from excessive layering in performance-critical paths.
- **Boundaries**: Consider your domain boundaries carefully when organizing packages and components.

By following this decision flow, you can create a clean, maintainable, and adaptable codebase that effectively leverages the full potential of Derafu Backbone&#039;s architecture.




---

### Component Interaction

Component Interaction Sequence

# Component Interaction Sequence

This document explores how the various components of Derafu Backbone interact during typical operations, providing insight into the architectural flow and communication patterns.

{.w-100 .mx-auto}
![Sequence Diagram](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-component-interaction.svg)

## The Flow of Control

The sequence diagram illustrates the typical flow of control when using Derafu Backbone for a business operation. Understanding this flow is crucial for effectively working with the architecture and designing new components.

### 1. Entry Point: The Registry

All interaction with a Backbone-based system typically begins with the Package Registry. This is the service locator that provides access to the domain packages in your application. The Package Registry serves as the entry point into your domain architecture.

```php
// Obtaining the package registry (typically from a kernel or container).
$packageRegistry = $container-&gt;get(PackageRegistryInterface::class);
```

The Package Registry is responsible for:

- Maintaining references to all registered packages.
- Providing type-safe access to specific packages.
- Acting as the gateway to your domain architecture.

### 2. Accessing a Package

After obtaining the registry, the client accesses a specific domain package:

```php
// Get a specific package by name.
$billingPackage = $packageRegistry-&gt;getPackage(&#039;billing&#039;);
// Or using a type-safe helper method.
$billingPackage = $packageRegistry-&gt;getBillingPackage();
```

Packages represent complete domains or subdomains in your application. They contain all components related to a specific business area. The clean organization of packages helps maintain separation between different domains.

### 3. Accessing a Component

From a package, the client accesses a specific component:

```php
// Access a component within the package.
$invoiceComponent = $billingPackage-&gt;getComponent(&#039;invoice&#039;);
// Or using a type-safe helper method.
$invoiceComponent = $billingPackage-&gt;getInvoiceComponent();
```

Components group related functionality within a domain. They represent functional areas within your domain and contain workers that handle specific aspects of that functionality.

### 4. Accessing a Worker

From a component, the client accesses a specific worker:

```php
// Access a worker within the component.
$generatorWorker = $invoiceComponent-&gt;getWorker(&#039;generator&#039;);
// Or using a type-safe helper method.
$generatorWorker = $invoiceComponent-&gt;getGeneratorWorker();
```

Workers are the coordinators that expose domain operations through public methods. They manage jobs and handlers to perform specific tasks within a component.

### 5. Using Jobs or Handlers

Finally, the client accesses and executes a job or handler:

```php
// Using a job.
$invoice = $generatorWorker-&gt;getCreateJob()-&gt;execute($data);

// Or using a handler.
$result = $generatorWorker-&gt;getProcessHandler()-&gt;handle($invoice, $options);
```

Jobs and handlers do the actual work in the system. Jobs perform atomic operations, while handlers orchestrate complex processes that might involve multiple jobs and strategies.

## Direct Access vs. Hierarchical Access

While the diagram shows a step-by-step hierarchical flow, it&#039;s important to note that once a client has obtained the registry, it can access any level directly:

```php
// Hierarchical access (step by step).
$invoice = $packageRegistry
    -&gt;getBillingPackage()
    -&gt;getInvoiceComponent()
    -&gt;getGeneratorWorker()
    -&gt;getCreateJob()
    -&gt;execute($data);

// Direct access (if you already have a reference to the worker).
$invoice = $generatorWorker-&gt;getCreateJob()-&gt;execute($data);
```

Both approaches are valid, but the hierarchical access pattern is generally recommended for application code as it:

- Makes the dependency hierarchy explicit.
- Follows a natural discovery pattern.
- Makes the code more readable and self-documenting.

The direct access pattern can be useful in:

- Unit tests where you want to focus on testing a specific component.
- Performance-critical paths where you can cache references to frequently used services.
- Situations where you already have a reference to a higher-level component.

## Alternative Path: Using Handlers

For more complex operations, handlers provide an alternative path:

```php
// Using a handler for complex operations.
$result = $generatorWorker-&gt;getProcessHandler()-&gt;handle($invoice, $options);
```

Handlers orchestrate complex workflows and can:

- Execute multiple jobs in sequence.
- Apply conditional logic.
- Select appropriate strategies based on input or configuration.
- Manage transactional boundaries.
- Handle cross-cutting concerns like logging or error handling.

## Communication Patterns

The diagram illustrates several important communication patterns in Backbone:

1. **Hierarchical Organization**: Components are organized in a clear hierarchy that reflects your domain structure.
2. **Dependency Injection**: Components receive their dependencies through constructor injection.
3. **Lazy Loading**: Services are typically loaded lazily, meaning they&#039;re only instantiated when needed.
4. **Interface-Based Programming**: Communication happens through well-defined interfaces.

## Performance Considerations

The hierarchical structure of Backbone might raise concerns about performance overhead. However, several aspects mitigate this:

1. **Lazy Loading**: Services are loaded only when needed.
2. **Cached References**: In performance-critical paths, you can cache references to frequently used services.
3. **DI Container Optimization**: Modern DI containers can optimize service instantiation.
4. **Proxy Generation**: Backbone can use proxies to further optimize lazy loading.

By understanding these interaction patterns, you can effectively design, implement, and use components within Derafu Backbone to create maintainable and extensible applications.




---

### Configuration Layers

Configuration Layers

# Configuration Layers

This document explores how configuration cascades through the architectural layers in Derafu Backbone, providing a powerful and flexible way to configure your application.

{.w-75 .mx-auto}
![Configuration Layers](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-configuration-layers.svg)

## Configuration Hierarchy

Derafu Backbone implements a hierarchical configuration system that follows the same structure as the architectural components. This cascade system allows for powerful customization while maintaining sensible defaults.

### Package Configuration

At the top of the hierarchy is the Package configuration, which provides the foundation for all configuration within a domain:

```yaml
# Example package configuration.
example_package:
  options:
    debug: false
    cache_enabled: true
  components:
    example_component:
      # Component-specific configuration.
    another_component:
      # Another component&#039;s configuration.
```

Package configuration is ideal for:

- Domain-wide settings that affect all components.
- Default values that components can override.
- Shared service configuration.
- Environment-specific settings for an entire domain.
- Feature flags that affect the entire package.

The Package configuration establishes the baseline for all contained components.

### Component Configuration

The Component configuration provides more specific settings for a functional area within a package:

```yaml
# Component configuration within a package.
example_package:
  components:
    example_component:
      options:
        timeout: 30
        max_retries: 3
      workers:
        example_worker:
          # Worker-specific configuration.
        another_worker:
          # Another worker&#039;s configuration.
```

Component configuration is appropriate for:

- Feature-specific settings.
- Overriding package defaults for a specific component.
- API keys or endpoints for external services used by the component.
- Component-specific validation rules or constraints.
- Logging or monitoring settings for a component.

Component configuration inherits from the package level but can override any setting as needed.

### Worker Configuration

Worker configuration provides even more specific settings for individual workers within a component:

```yaml
# Worker configuration within a component
example_package:
  components:
    example_component:
      workers:
        example_worker:
          options:
            batch_size: 100
            processing_mode: &quot;async&quot;
          jobs:
            # Job-specific configuration
          handlers:
            # Handler-specific configuration
```

Worker configuration is useful for:

- Task-specific settings.
- Overriding component defaults for a specific worker.
- Performance tuning parameters.
- Feature flags for specific functionality.
- Worker-specific thresholds or limits.

Worker configuration inherits from both the package and component levels.

### Job/Handler/Strategy Configuration

At the lowest level, individual jobs, handlers, and strategies can have their own configuration:

```yaml
# Job configuration within a worker
example_package:
  components:
    example_component:
      workers:
        example_worker:
          jobs:
            example_job:
              options:
                validation_level: &quot;strict&quot;
                timeout: 10
```

This level of configuration is ideal for:

- Operation-specific settings.
- Fine-grained control over individual operations.
- Feature flags for specific functionality.
- Operation-specific thresholds or limits.

## Accessing Configuration

Derafu Backbone provides a consistent way to access configuration at any level through the `ConfigurableInterface` and the `ConfigurableTrait`:

```php
// Inside a package, component, or worker.
$config = $this-&gt;getConfiguration();

// Accessing specific options.
$debug = $config-&gt;get(&#039;options.debug&#039;, false); // With default value.
$timeout = $config-&gt;get(&#039;options.timeout&#039;); // Without default.

// Accessing nested configuration.
$workerConfig = $config-&gt;get(&#039;workers.example_worker&#039;);
```

The configuration system is designed to be:

- **Type-safe**: Configurations can be defined with schemas for validation.
- **Environment-aware**: Different configurations can be loaded based on the environment.
- **Hierarchical**: Configurations cascade from more general to more specific.
- **Defaulted**: Sensible defaults can be provided at any level.

## Configuration Inheritance Mechanism

The key benefit of Backbone&#039;s layered configuration is inheritance:

1. **Default Values**: Each layer can provide default values that are used if not overridden.
2. **Selective Overrides**: Lower levels can override specific settings without affecting others.
3. **Aggregation**: Configuration is aggregated from all levels before being used.

For example, if you have these configurations:

```yaml
# Package level.
example_package:
  options:
    debug: false
    cache_enabled: true
    timeout: 30

# Component level (in the same file).
example_package:
  components:
    example_component:
      options:
        timeout: 60
```

Then, when accessing configuration from within `ExampleComponent`:

```php
$config = $this-&gt;getConfiguration();
$debug = $config-&gt;get(&#039;options.debug&#039;); // false (inherited from package)
$cacheEnabled = $config-&gt;get(&#039;options.cache_enabled&#039;); // true (inherited from package)
$timeout = $config-&gt;get(&#039;options.timeout&#039;); // 60 (overridden at component level)
```

## Best Practices for Configuration Management

When working with Backbone&#039;s configuration layers:

1. **Define defaults at the highest appropriate level**: Place configuration that applies broadly at the package level.
2. **Override only what&#039;s necessary**: Only override configuration when you need to change it.
3. **Be explicit about types and validation**: Use configuration schemas to validate configuration.
4. **Use environment-specific configuration**: Leverage environment variables and profiles for environment-specific settings.
5. **Document your configuration**: Clearly document what configuration options are available and what they do.
6. **Keep sensitive information separate**: Use environment variables or dedicated configuration stores for secrets.

By understanding and effectively using Backbone&#039;s configuration layers, you can create flexible, configurable applications that are easy to adapt to different environments and use cases.




---

### Service Lifecycle

Service Lifecycle

# Service Lifecycle

This document explains the lifecycle of services within Derafu Backbone, from definition to usage, providing insights into how services are discovered, configured, and instantiated.

{.w-75 .mx-auto}
![Service Lifecycle](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-service-lifecycle.svg)

## Three-Phase Lifecycle

Derafu Backbone services go through three distinct phases during their lifecycle:

1. **Definition Phase**: How services are defined in code.
2. **Discovery Phase**: How services are discovered and registered.
3. **Usage Phase**: How services are instantiated and used.

Understanding this lifecycle is crucial for developing with Backbone and troubleshooting any issues that may arise.

## 1. Definition Phase

The definition phase is where you define your Backbone services in code.

### Class Definition with PHP 8 Attributes

The cornerstone of Backbone&#039;s service definition is PHP 8 attributes. These attributes provide metadata about your services and eliminate the need for verbose configuration:

```php
#[Package(name: &#039;billing&#039;, description: &#039;Handles billing operations&#039;)]
class BillingPackage extends AbstractPackage implements PackageInterface
{
    // Package implementation.
}

#[Component(name: &#039;document&#039;, package: &#039;billing&#039;)]
class DocumentComponent extends AbstractComponent implements ComponentInterface
{
    // Component implementation.
}

#[Worker(name: &#039;renderer&#039;, component: &#039;document&#039;, package: &#039;billing&#039;)]
class RendererWorker extends AbstractWorker implements WorkerInterface
{
    // Worker implementation
}
```

The attributes provide several key pieces of information:
- **Service type**: Package, Component, Worker, Job, Handler, or Strategy
- **Service name**: The identifier used to reference the service
- **Service hierarchy**: The parent services (package, component, worker)
- **Description**: Optional description for better documentation

### Abstract Base Classes and Interfaces

All Backbone services extend abstract base classes and implement interfaces:

- **Abstract classes** provide common functionality like configuration handling, ID generation, and standardized constructors.
- **Interfaces** define the contract that each service must fulfill.

This combination ensures consistent behavior and makes services interchangeable and testable.

### Dependency Injection

Services declare their dependencies through constructor injection:

```php
#[Component(name: &#039;document&#039;, package: &#039;billing&#039;)]
class DocumentComponent extends AbstractComponent implements ComponentInterface
{
    public function __construct(
        private readonly BuilderWorker $builderWorker,
        private readonly RendererWorker $rendererWorker
    ) {
    }

    public function getWorkers(): array
    {
        return [
            &#039;builder&#039; =&gt; $this-&gt;builderWorker,
            &#039;renderer&#039; =&gt; $this-&gt;rendererWorker,
        ];
    }

    // Other component implementation.
    // It&#039;s recommended to create getters for the dependencies. This will
    // provide safe type hinting and autocompletion.
}
```

This explicit declaration of dependencies:

- Makes dependencies clear and traceable.
- Facilitates testing through mocking.
- Allows the DI container to resolve dependencies automatically.
- Ensures services are properly initialized.

## 2. Discovery Phase

The discovery phase is where defined services are found, processed, and registered in the dependency injection container.

### Compiler Passes

Backbone uses Symfony&#039;s compiler passes to discover and process services:

#### ServiceProcessingCompilerPass

This compiler pass:

1. Scans all service definitions in the container.
2. Identifies classes with Backbone attributes.
3. Extracts metadata from attributes.
4. Creates and registers service definitions.
5. Adds appropriate tags for later reference.
6. Mark all services as lazy services.
7. Mark only packages as public services.

The result is that all Backbone services are properly registered in the container, with appropriate tags and metadata.

#### ServiceConfigurationCompilerPass

This compiler pass:
1. Finds services that implement `ConfigurableInterface`.
2. Matches them with configuration from parameters.
3. Adds method calls to set configuration when the service is created.

This ensures that services receive their configuration during instantiation.

### Registration and Aliasing

During the discovery phase, services are registered in the container and aliased for easier access:

```php
// Original service ID (class name).
App\Billing\DocumentComponent

// Aliased as (from attribute metadata).
billing.document
```

These aliases make it easier to reference services by their logical names rather than class names.

## 3. Usage Phase

The usage phase is where services are actually instantiated and used at runtime.

### Lazy Loading

By default, Backbone services are registered as lazy services. This means:

- They&#039;re only instantiated when actually needed.
- A proxy is created that loads the real service on first method call.
- This improves performance by avoiding unnecessary service instantiation.

### Accessing Services

Services can be accessed through the Package Registry:

```php
// Get a package from the registry.
$billingPackage = $packageRegistry-&gt;getPackage(&#039;billing&#039;);

// Get a component from the package.
$documentComponent = $billingPackage-&gt;getComponent(&#039;document&#039;);

// Get a worker from the component.
$rendererWorker = $documentComponent-&gt;getWorker(&#039;renderer&#039;);

// Use the worker.
$pdf = $rendererWorker-&gt;render($data);
```

Each level in the hierarchy provides access to the level below it, creating a discoverable API.

### Configuration Application

When a service is instantiated:

1. The container resolves its dependencies.
2. Dependencies are injected via the constructor.
3. Configuration is applied via a method call (if the service is configurable).
4. The service is ready to use.

This ensures that services are fully initialized before use.

## Understanding the Flow

The complete flow from definition to usage:

1. **Define** services using PHP 8 attributes.
2. **Compile** the container, triggering discovery and registration.
3. **Build** the container for runtime use.
4. **Access** services through the registry.
5. **Use** services to perform domain operations.

## Benefits of This Lifecycle

This lifecycle approach provides several benefits:

1. **Discoverability**: Services are easily discoverable through attributes.
2. **Configuration**: Services receive appropriate configuration automatically.
3. **Lazy Loading**: Only services that are actually used are instantiated.
4. **Type Safety**: The full hierarchy is type-safe through interfaces.
5. **Testability**: Dependencies are explicit and can be mocked for testing.

## Troubleshooting the Lifecycle

Common issues and how to address them:

1. **Service not found**: Ensure attributes are correctly defined and the compiler pass is registered.
2. **Configuration not applied**: Check that your configuration keys match the expected structure.
3. **Dependencies not resolved**: Verify that all dependencies are correctly registered in the container.
4. **Performance issues**: Consider caching the compiled container in production.

By understanding the complete service lifecycle in Derafu Backbone, you can effectively develop, configure, and troubleshoot your application.




---

### Design Patterns

Backbone and Design Patterns

# Backbone and Design Patterns

This document explores how Derafu Backbone implements and leverages established design patterns to create a robust, maintainable architecture for PHP applications.

{.w-50 .mx-auto}
![Design Patterns](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-design-patterns.svg)

## Design Patterns in Backbone

Derafu Backbone draws inspiration from several well-established design patterns. Understanding these patterns and their implementation in Backbone helps developers use the framework effectively and extend it appropriately.

## Jobs and the Command Pattern

Jobs in Backbone are a direct implementation of the Command Pattern.

### Command Pattern Overview

The Command Pattern encapsulates a request as an object, allowing:

- Parameterization of clients with different requests.
- Queueing of requests.
- Logging of requests.
- Support for undoable operations.

### How Jobs Implement Command

Jobs in Backbone embody these principles:

- Each job is a class that encapsulates a single operation.
- Jobs have a standardized execution method (`execute()`).
- Jobs can be parameterized through their execute method.
- Jobs are self-contained and can be invoked by various clients.

```php
#[Job(name: &#039;create&#039;, worker: &#039;generator&#039;, component: &#039;invoice&#039;, package: &#039;billing&#039;)]
class CreateInvoiceJob extends AbstractJob implements JobInterface
{
    public function execute(array $data): Invoice
    {
        // Validate input.
        $this-&gt;validateInput($data);

        // Create invoice
        $invoice = new Invoice();
        $invoice-&gt;setCustomer($data[&#039;customer&#039;]);
        $invoice-&gt;setItems($data[&#039;items&#039;]);

        // Return result.
        return $invoice;
    }

    private function validateInput(array $data): void
    {
        // Validation logic.
    }
}
```

### Benefits of the Command Pattern in Jobs

This implementation provides several advantages:

- **Single Responsibility**: Each job has a clear, specific purpose.
- **Reusability**: Jobs can be reused in different contexts.
- **Testability**: Jobs are easy to test in isolation.
- **Extensibility**: New jobs can be added without modifying existing code.
- **Queueability**: Jobs can be serialized and queued for asynchronous processing.

## Handlers and the Mediator/Chain of Responsibility Patterns

Handlers in Backbone combine aspects of both the Mediator and Chain of Responsibility patterns.

### Mediator Pattern Overview

The Mediator Pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other explicitly.

### Chain of Responsibility Overview

The Chain of Responsibility Pattern passes a request along a chain of handlers, with each handler deciding either to process the request or pass it to the next handler.

### How Handlers Implement These Patterns

Handlers in Backbone combine these concepts:

- They coordinate interactions between multiple Jobs (Mediator).
- They orchestrate a sequence of operations (Chain of Responsibility).
- They encapsulate complex workflows.

```php
#[Handler(name: &#039;process&#039;, worker: &#039;processor&#039;, component: &#039;invoice&#039;, package: &#039;billing&#039;)]
class ProcessInvoiceHandler extends AbstractHandler implements HandlerInterface
{
    public function handle(Invoice $invoice, array $options = []): Result
    {
        // Validate the invoice.
        $validationResult = $this-&gt;getJob(&#039;validate&#039;)-&gt;execute($invoice);
        if (!$validationResult-&gt;isValid()) {
            return Result::failure($validationResult-&gt;getErrors());
        }

        // Determine processing strategy.
        $strategyName = $options[&#039;strategy&#039;] ?? &#039;default&#039;;
        $strategy = $this-&gt;getStrategy($strategyName);

        // Process the invoice.
        $processingResult = $strategy-&gt;process($invoice);
        if (!$processingResult-&gt;isSuccessful()) {
            return Result::failure($processingResult-&gt;getErrors());
        }

        // Send notifications.
        $this-&gt;getJob(&#039;notify&#039;)-&gt;execute($invoice, $options[&#039;notifications&#039;] ?? []);

        // Return success.
        return Result::success([&#039;invoice&#039; =&gt; $invoice, &#039;processed&#039; =&gt; true]);
    }
}
```

### Benefits of These Patterns in Handlers

This implementation provides several advantages:

- **Decoupling**: Components don&#039;t need to know about each other.
- **Centralized Control**: Complex workflows are managed in a single place.
- **Flexibility**: Processing steps can be changed without affecting clients.
- **Transactional Boundaries**: Handlers can manage transactions across multiple operations.
- **Error Handling**: Centralized error handling for multi-step processes.

## Strategies and the Strategy Pattern

Strategies in Backbone are a direct implementation of the Strategy Pattern.

### Strategy Pattern Overview

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

### How Strategies Implement the Pattern

Strategies in Backbone embody these principles:

- They provide alternative implementations of an algorithm.
- They share a common interface.
- They can be selected and switched at runtime.

```php
#[Strategy(name: &#039;pdf&#039;, worker: &#039;renderer&#039;, component: &#039;document&#039;, package: &#039;billing&#039;)]
class PdfRenderStrategy extends AbstractStrategy implements RenderStrategyInterface
{
    public function render(Document $document): string
    {
        // PDF rendering implementation.
        return $this-&gt;pdfRenderer-&gt;renderDocument($document);
    }
}

#[Strategy(name: &#039;html&#039;, worker: &#039;renderer&#039;, component: &#039;document&#039;, package: &#039;billing&#039;)]
class HtmlRenderStrategy extends AbstractStrategy implements RenderStrategyInterface
{
    public function render(Document $document): string
    {
        // HTML rendering implementation.
        return $this-&gt;htmlRenderer-&gt;renderDocument($document);
    }
}
```

### Benefits of the Strategy Pattern

This implementation provides several advantages:

- **Encapsulation**: Different algorithms are encapsulated in separate classes.
- **Interchangeability**: Strategies can be swapped without changing client code.
- **Elimination of Conditionals**: Complex conditional logic is replaced with polymorphism.
- **Runtime Selection**: Algorithms can be selected based on runtime conditions.
- **Testability**: Each strategy can be tested independently.

## Workers and the Facade Pattern

Workers in Backbone implement the Facade Pattern.

### Facade Pattern Overview

The Facade Pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use.

### How Workers Implement the Facade

Workers in Backbone act as facades:

- They provide a simplified interface to complex subsystems.
- They handle the complexity of coordinating jobs, handlers, and strategies.
- They expose domain operations through a clean API.

```php
#[Worker(name: &#039;processor&#039;, component: &#039;invoice&#039;, package: &#039;billing&#039;)]
class InvoiceProcessorWorker extends AbstractWorker implements WorkerInterface
{
    // This method is part of the public API.
    public function processInvoice(Invoice $invoice, array $options = []): Result
    {
        // Delegate to the appropriate handler.
        return $this-&gt;getHandler(&#039;process&#039;)-&gt;handle($invoice, $options);
    }

    // Another public API method.
    public function validateInvoice(Invoice $invoice): ValidationResult
    {
        // Delegate to a job.
        return $this-&gt;getJob(&#039;validate&#039;)-&gt;execute($invoice);
    }
}
```

### Benefits of the Facade Pattern in Workers

This implementation provides several advantages:

- **Simplified Interface**: Clients interact with a clean, focused API.
- **Reduced Coupling**: Clients don&#039;t need to know about the subsystem&#039;s components.
- **Unified Entry Point**: Workers provide a single entry point to related functionality.
- **Abstraction**: Implementation details are hidden behind the facade.

## Hexagonal Architecture Influence

The overall architecture of Backbone is inspired by Hexagonal Architecture (also known as Ports and Adapters).

### Hexagonal Architecture Overview

Hexagonal Architecture aims to create loosely coupled application components that can be easily connected to their software environment by means of ports and adapters.

### How Backbone Implements Hexagonal Concepts

Backbone incorporates these principles:

- **Domain-Centric**: The architecture focuses on domain logic.
- **Ports**: Interfaces define how components interact.
- **Adapters**: Implementations connect the domain to external systems.
- **Inversion of Control**: Dependencies point inward toward the domain.

The Package-Component-Worker structure creates clear boundaries within the domain, while Strategies often serve as adapters to external systems.

## Practical Application

When applying these design patterns in your Backbone applications:

1. **Identify the Pattern**: Recognize which pattern applies to your situation.
2. **Follow the Template**: Use the appropriate Backbone component.
3. **Respect the Boundaries**: Maintain separation between different components.
4. **Leverage Polymorphism**: Use strategies for variant implementations.
5. **Focus on Composition**: Prefer composition over inheritance.

By understanding and applying these design patterns within Backbone, you can create well-structured, maintainable applications that are flexible enough to adapt to changing requirements.




---

### File Structure

Recommended File Structure for Projects

# Recommended File Structure for Projects

This document provides guidelines for organizing your code in Derafu Backbone projects, explaining the rationale behind the recommended structure and best practices for maintaining a clean, maintainable codebase.

{.w-75 .mx-auto}
![File Structure](https://www.derafu.dev/img/diagrams/content/docs/core/backbone/derafu-backbone-file-structure.svg)

## Domain-First Organization

Derafu Backbone encourages a domain-first approach to organizing your codebase. This means that the primary organizing principle is the business domain, not technical concerns.

### Root Structure

A typical Backbone project is organized as follows:

```
src/
├── Domain1/
├── Domain2/
├── Domain3/
└── Registry.php
config/
└── services.yaml
```

This structure puts domains at the forefront, making it immediately clear what business capabilities your application provides.

## Domain Structure

Each domain (represented by a Package) follows a consistent internal structure:

```
Domain/
├── DomainPackage.php
├── Component/
│   ├── Component1.php
│   └── Component2.php
├── Model/
│   ├── Model1.php
│   └── Model2.php
└── Exception/
    └── DomainException.php
```

### Key Elements

- **DomainPackage.php**: The package class that serves as the entry point to the domain
- **Component/**: Directory containing all components of this domain
- **Model/**: Directory containing domain models (entities, value objects, etc.)
- **Exception/**: Domain-specific exceptions

## Component Structure

Each component has its own structure that houses its workers and related classes:

```
Component/
├── Component.php
└── Worker/
    ├── Worker1.php
    ├── Worker2.php
    ├── Job/
    │   ├── Job1.php
    │   └── Job2.php
    ├── Handler/
    │   ├── Handler1.php
    │   └── Handler2.php
    └── Strategy/
        ├── Strategy1.php
        └── Strategy2.php
```

### Key Elements

- **Component.php**: The component class that serves as the entry point to this functional area
- **Worker/**: Directory containing workers and their related classes
- **Job/**: Directory containing jobs used by workers
- **Handler/**: Directory containing handlers used by workers
- **Strategy/**: Directory containing strategies used by handlers and jobs

## Namespacing

The file structure directly corresponds to the namespace structure, following PSR-4 autoloading standards:

```php
// DomainPackage.php
namespace App\Domain;

// Component.php
namespace App\Domain\Component;

// Worker.php
namespace App\Domain\Component\Worker;

// Job.php
namespace App\Domain\Component\Worker\Job;
```

This clear correspondence between namespaces and directories makes it easy to locate files and understand their role in the architecture.

## The Benefits of This Structure

### 1. Domain Discovery

The domain-first structure makes it easy to discover what domains your application handles. New team members can quickly understand the application&#039;s purpose by examining the top-level directories.

### 2. Component Cohesion

By grouping related components within a domain, the structure promotes cohesion. Classes that work together are located near each other, making it easier to understand and modify related functionality.

### 3. Clear Dependencies

The hierarchical structure reflects the dependency hierarchy in Backbone, making it clear how components relate to each other.

### 4. Consistent Navigation

Once familiar with the structure, developers can quickly navigate to any part of the codebase, even in unfamiliar domains, because the pattern is consistent.

### 5. Scalable Organization

The structure scales well from small applications to large enterprise systems. As your application grows, you can add new domains without restructuring existing code.

## Best Practices

### Naming Conventions

Adopting consistent naming conventions enhances the clarity of your codebase:

- **Packages**: Use singular nouns (e.g., `Billing`, not `Bills`).
- **Components**: Use singular nouns that describe their functionality (e.g., `Document`, `Exchange`).
- **Workers**: Use a noun followed by &quot;Worker&quot; (e.g., `BuilderWorker`, `RendererWorker`).
- **Jobs**: Use a verb in the imperative followed by a noun (e.g., `CreateInvoice`, `SendNotification`).
- **Handlers**: Use a verb in the imperative followed by a noun and &quot;Handler&quot; (e.g., `ProcessPaymentHandler`).
- **Strategies**: Use a descriptive adjective or noun followed by the purpose and &quot;Strategy&quot; (e.g., `PdfRenderStrategy`, `StripePaymentStrategy`).

### File Organization Tips

1. **Group Related Files**: Keep files that are likely to change together in the same directory.
2. **Domain Boundaries**: Be careful about cross-domain dependencies. If components in different domains need to communicate, consider defining interfaces.
3. **Package Size**: If a package grows too large (more than 7-10 components), consider splitting it into multiple packages.
4. **Shared Code**: Place shared code that&#039;s used across multiple domains in a separate `Shared` or `Common` package.
5. **Infrastructure Code**: Place infrastructure concerns (like database access, HTTP clients, etc.) in appropriate domains rather than in a separate &quot;infrastructure&quot; layer.

### Exception Hierarchy

Match your exception hierarchy to your package/component hierarchy:

```
Exception/
├── DomainException.php (base exception for the domain).
├── ComponentException.php (base exception for a component).
├── SpecificException1.php (specific exception type).
└── SpecificException2.php (specific exception type).
```

This makes error handling more consistent and helps identify the source of exceptions.

## Real-World Adaptations

While the recommended structure provides a solid foundation, you may need to adapt it to your specific needs.

### Microservice Adaptations

In a microservice architecture, each service might represent a single domain or even a single component:

```
services/
├── billing-service/
│   └── src/
│       └── Billing/
└── customer-service/
    └── src/
        └── Customer/
```

### Legacy Integration Adaptations

When integrating with legacy systems, you might need a different structure for adapter code:

```
src/
├── Domain/
│   └── ...
└── Legacy/
    └── Adapter/
        └── ...
```

### Infrastructure Concerns

For complex applications, you might introduce additional directories for infrastructure concerns:

```
src/
├── Domain/
├── Infrastructure/
│   ├── Database/
│   ├── Queue/
│   └── Cache/
└── Registry.php
```

However, try to keep these separate from your domain logic and limit dependencies on them from your domain code.

## Conclusion

The recommended file structure for Derafu Backbone projects emphasizes domain-driven organization, consistent patterns, and clear separation of concerns. By following these guidelines, you can create codebases that are easy to navigate, maintain, and extend, regardless of the size or complexity of your application.

Remember that the structure should serve your team and your application&#039;s needs. While consistency is important, don&#039;t be afraid to adapt the recommended structure when necessary to better suit your specific context.




---

### Operations

Marking Real Operations: `#[Operation]`

# Marking Real Operations: `#[Operation]`

A `Worker`&#039;s public methods are ordinary PHP methods — but not all of them are business logic. Trait helpers (`JobsAwareTrait`/`HandlersAwareTrait`/`OptionsAwareTrait`) and `ServiceInterface` itself (`getId()`, `getName()`, `getDescription()`) contribute public methods too, and reflection alone cannot tell those apart from a worker&#039;s genuine capabilities. `#[Operation]` is the explicit signal that draws that line: tag a method with it, and any consumer — an allow-list policy, generated documentation, anything else that needs to enumerate what a worker can really do — has a reliable answer instead of a guess based on visibility alone.

```php
use Derafu\Backbone\Attribute\Operation;

class InvoiceBuilderWorker extends AbstractWorker implements WorkerInterface
{
    #[Operation]
    public function build(string $number, int $amount): array
    {
        // ...
    }
}
```

That&#039;s the entire required usage: no arguments, just the tag. Everything else this attribute offers is optional.

## Not a Service Attribute

`#[Package]`/`#[Component]`/`#[Worker]`/`#[Job]`/`#[Handler]`/`#[Strategy]` (see [Service Lifecycle](https://www.derafu.dev/docs/core/backbone/service-lifecycle)) all mark a *class* as a registrable service with its own identity, hierarchy and lifecycle — `TARGET_CLASS`, extending `AbstractServiceMetadata`. `#[Operation]` is deliberately different: `TARGET_METHOD`, and it does **not** extend `AbstractServiceMetadata` or implement `ServiceMetadataInterface`. An operation isn&#039;t a service of its own — it&#039;s a capability of a `Worker` that already exists, with no `id` or parent reference to manage, because the containing class&#039;s own service attribute already provides that context.

## What It Can Add on Top of Reflection

Every property exists only to say something reflection or a PHPDoc block cannot say on its own — none of it is required, and the normal case is to set none of it at all:

```php
#[Operation(
    name: &#039;Create a draft invoice&#039;,
    description: &#039;Builds a draft from the given data, without emitting it.&#039;,
    parameters: [
        &#039;number&#039; =&gt; [&#039;example&#039; =&gt; &#039;F-001&#039;],
        &#039;amount&#039; =&gt; [&#039;example&#039; =&gt; 15000, &#039;description&#039; =&gt; &#039;Amount in the smallest currency unit.&#039;],
    ],
    results: [
        &#039;success&#039; =&gt; [&#039;description&#039; =&gt; &#039;The created draft.&#039;, &#039;example&#039; =&gt; [&#039;id&#039; =&gt; &#039;DR-001&#039;]],
        MissingParameterException::class =&gt; [&#039;description&#039; =&gt; &#039;A required parameter was not provided.&#039;],
    ],
)]
public function build(string $number, int $amount): array
```

- **`name`/`description`** override the method&#039;s own PHPDoc summary/description. Leave both `null` (the default, and the expected normal case) to keep using PHPDoc — this is not a place to duplicate what a good docblock already says; it exists for the rarer case where the text written for PHP maintainers isn&#039;t the text an external consumer should see.
- **`parameters`** overrides or extends what reflection already knows about each parameter, keyed by parameter name. Only the keys given are applied — reflection&#039;s own `type`/`required`/`default` stay exactly as reflected unless a key explicitly overrides them:
  - `&#039;example&#039;` — a realistic sample value. Reflection has no way to produce one on its own.
  - `&#039;type&#039;`/`&#039;description&#039;` — for when reflection&#039;s own type isn&#039;t precise enough (a union type collapsed to a plain string, a bare `array` that actually has a real shape).
- **`results`** documents outcomes, keyed however the consumer identifies each one — `&#039;success&#039;`, or the fully-qualified class name of an exception the operation can throw, with whatever data that consumer finds useful under each key. This attribute does not define what a key means or what it&#039;s for; it just holds what&#039;s given. A key is never anything transport-specific like an HTTP status code — resolving a scenario to something transport-specific (a status code, for instance) is a decision for whichever consumer needs that, not for this attribute.

## Who Reads It

`derafu/backbone` defines `#[Operation]` and stops there — it has no idea what, if anything, ever reads it. Nothing in this package depends on `derafu/backbone-dispatcher`, on purpose: a business library can tag its workers&#039; operations without pulling in anything about how they&#039;ll eventually be dispatched.

The two real consumers today both live in [`derafu/backbone-dispatcher`](https://www.derafu.dev/docs/core/backbone-dispatcher):

- **[`TaggedOperationPolicy`](https://www.derafu.dev/docs/core/backbone-dispatcher#controlling-which-operations-can-be-dispatched-operationpolicyinterface)** — the recommended policy for anything reachable from outside PHP — only allows dispatching what&#039;s tagged, closing off every trait helper and `ServiceInterface` method this attribute exists to distinguish from.
- **`Inspector`**, and through it [`#[Operation]` in Backbone Dispatcher](/docs/core/backbone-dispatcher#documenting-an-operation-operation) and [`derafu/backbone-api`&#039;s `Documenter`](https://www.derafu.dev/docs/core/backbone-api#autodiscovery) — merge the `name`/`description`/`parameters`/`results` given here on top of what reflection and PHPDoc already produced, to build generated documentation (an OpenAPI spec, for instance) that says more than reflection alone ever could.




---

## Translation Project

Derafu Translation

# Derafu Translation




---

### Introduction

Translation Library with Exception Support

# Translation Library with Exception Support

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/translation/main)
![CI Workflow](https://github.com/derafu/translation/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/translation)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/translation)
![Total Downloads](https://poser.pugx.org/derafu/translation/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/translation/d/monthly)

A small library that adds two things on top of `symfony/translation`:

1. **Translatable exceptions**: exceptions that carry a message, ICU
   parameters and a translation domain, and can render themselves in
   English (or any locale) with zero setup, or be translated later by
   handing them a real translator.
2. **A resource discovery layer**: `symfony/translation` doesn&#039;t provide a
   way to discover translation files in a directory on its own (that&#039;s
   normally wired by a full framework). This library adds that missing
   piece as a small, standalone component.

Everything else — ICU message formatting, locale fallback, file loaders
(YAML, JSON, PHP, XLIFF, PO, MO, CSV, INI) — comes directly from
`symfony/translation` itself. This library does not reimplement a
translation engine.

## Features

- 🔄 **Translatable Exceptions**: A full hierarchy of exceptions (mirroring
  PHP&#039;s SPL exceptions) that support translation with minimal setup.
- 🌍 **ICU Support**: Powered by PHP&#039;s `intl` extension via
  `symfony/translation`&#039;s own ICU formatter. Works even without a
  translator configured.
- 📁 **Multi-Directory Resource Discovery**: Register N directories (or a
  tagged collection of providers, if you use a DI container) and have every
  translation file inside them loaded automatically.
- ⛓️ **Locale Fallback**: Configurable fallback locale chain, courtesy of
  `symfony/translation`.
- 🪶 **Lightweight**: The only hard dependencies are `symfony/translation`,
  `symfony/translation-contracts`, `symfony/yaml`, and the `intl` PHP
  extension. No framework, no `symfony/http-kernel`.

## Installation

```bash
composer require derafu/translation
```

## Basic Usage

### Making Exceptions Translatable

```php
// Before: your existing exception.
class ValidationException extends Exception
{
}

// After: extend one of the translatable base exceptions instead.
use Derafu\Translation\Exception\Core\TranslatableException;

class ValidationException extends TranslatableException
{
    // No further changes needed.
}
```

### Using Translatable Exceptions

```php
// 1. A plain string: used as both the message and the translation id.
throw new ValidationException(&#039;Email is invalid.&#039;);

// 2. An array: first element is the message/id, the rest are ICU
//    parameters keyed by name.
throw new ValidationException([
    &#039;The value {value} is not a valid email.&#039;,
    &#039;value&#039; =&gt; &#039;test@example&#039;,
]);

// 3. A TranslatableInterface instance, built directly.
use Derafu\Translation\TranslatableMessage;

throw new ValidationException(new TranslatableMessage(
    &#039;The value {value} is not a valid email.&#039;,
    [&#039;value&#039; =&gt; &#039;test@example&#039;],
));
```

Without ever touching a translator, `getMessage()` already returns the
fully ICU-formatted English text — `TranslatableExceptionTrait` formats it
eagerly at construction time. Handing the exception a real translator later
(via `trans()`) is entirely optional, and is what actually produces a
translated string in another locale.

```php
try {
    // ...
} catch (ValidationException $e) {
    // Untranslated (eager ICU formatting only).
    echo $e-&gt;getMessage();

    // Translated, given a real translator.
    echo $e-&gt;trans($translator, &#039;es&#039;);
}
```

### Translation Files

Files follow `symfony/translation`&#039;s own naming convention, flat inside a
directory: `{domain}(+intl-icu)?.{locale}.{format}`.

```php
// translations/errors+intl-icu.en.php
return [
    &#039;The value {value} is not a valid email.&#039; =&gt; &#039;The value {value} is not a valid email.&#039;,
];
```

```yaml
# translations/errors+intl-icu.es.yaml
&#039;The value {value} is not a valid email.&#039;: &#039;El valor {value} no es un correo electrónico válido.&#039;
```

The `+intl-icu` suffix in the domain activates ICU MessageFormat parsing
for that file (`{value}`-style placeholders, plural/select patterns). See
[ICU Formatting](icu-formatting) for details.

### Building a Translator and Registering Resources

```php
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;

$translator = TranslatorFactory::create(
    defaultLocale: &#039;es&#039;,
    fallbackLocales: [&#039;es&#039;, &#039;en&#039;],
);

// Discover and register every translation file in a directory.
$registrar = new TranslationResourceRegistrar($translator);
$registrar-&gt;registerDirectory(__DIR__ . &#039;/translations&#039;);

echo $translator-&gt;trans(&#039;The value {value} is not a valid email.&#039;, [
    &#039;value&#039; =&gt; &#039;test@example&#039;,
], &#039;errors+intl-icu&#039;, &#039;es&#039;);
```

## Key Benefits

1. **Zero Compromise**: Existing exception-handling code (`getMessage()`,
   `catch` blocks) keeps working unchanged.
2. **ICU Power**: Full ICU message formatting, with or without a
   translator.
3. **Built on `symfony/translation`**: No custom translation engine to
   maintain or learn — the real Symfony component, with all its loaders.
4. **Multi-Package Friendly**: Register directories from N sources (each
   package can ship its own translations) with a single registrar.

## When to Use This Library

- You want translatable exceptions without rewriting how you throw or
  catch them.
- You want ICU message formatting without depending on a full framework.
- You want a small, standalone way to discover translation files across
  multiple directories.




---

### Quick Start

Quick Start Guide

# Quick Start Guide

Get started with `derafu/translation` in a few minutes.

## Installation

```bash
composer require derafu/translation
```

That&#039;s it — there are no optional dependencies to add separately.
`symfony/translation`, `symfony/translation-contracts`, `symfony/yaml` and
the `intl` PHP extension all come as hard requirements, so every built-in
file format (YAML, JSON, PHP, XLIFF, PO, MO, CSV, INI) and ICU formatting
work out of the box.

## Basic Setup

### 1. Make an Exception Translatable

Extend one of the built-in translatable exceptions:

```php
use Derafu\Translation\Exception\Core\TranslatableException;

class ValidationException extends TranslatableException
{
    // No additional code needed.
}
```

If you can&#039;t change the parent class (you already extend something else),
use the trait instead:

```php
use Derafu\Translation\Contract\TranslatableInterface;
use Derafu\Translation\Trait\TranslatableExceptionTrait;
use DomainException;

class ValidationException extends DomainException implements TranslatableInterface
{
    use TranslatableExceptionTrait;
}
```

See [Exceptions](exceptions) for the full hierarchy.

### 2. Throw It

```php
// 1. A plain string: used as-is, and as the translation id.
throw new ValidationException(&#039;Email is required.&#039;);

// 2. An array: first element is the message/id, the rest are named ICU
//    parameters.
throw new ValidationException([
    &#039;The value {value} must be between {min} and {max}.&#039;,
    &#039;value&#039; =&gt; 42,
    &#039;min&#039; =&gt; 1,
    &#039;max&#039; =&gt; 10,
]);
```

`getMessage()` already returns the fully formatted English text at this
point — no translator needed yet:

```php
try {
    // ...
} catch (ValidationException $e) {
    echo $e-&gt;getMessage();
    // &quot;The value 42 must be between 1 and 10.&quot;
}
```

### 3. Create Translation Files

Files are flat inside a directory and follow the naming convention
`{domain}(+intl-icu)?.{locale}.{format}`. `TranslatableExceptionTrait`
defaults to the `errors` domain, so add `+intl-icu` to activate ICU
formatting for it:

```php
// translations/errors+intl-icu.en.php
return [
    &#039;The value {value} must be between {min} and {max}.&#039; =&gt;
        &#039;The value {value} must be between {min} and {max}.&#039;,
];
```

```php
// translations/errors+intl-icu.es.php
return [
    &#039;The value {value} must be between {min} and {max}.&#039; =&gt;
        &#039;El valor {value} debe estar entre {min} y {max}.&#039;,
];
```

YAML and JSON work exactly the same way:

```yaml
# translations/errors+intl-icu.es.yaml
&#039;The value {value} must be between {min} and {max}.&#039;: &#039;El valor {value} debe estar entre {min} y {max}.&#039;
```

```json
// translations/errors+intl-icu.es.json
{
    &quot;The value {value} must be between {min} and {max}.&quot;: &quot;El valor {value} debe estar entre {min} y {max}.&quot;
}
```

### 4. Build a Translator and Register the Files

```php
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;

// Builds a Symfony Translator with every supported loader already wired.
$translator = TranslatorFactory::create(
    defaultLocale: &#039;es&#039;,
    fallbackLocales: [&#039;es&#039;, &#039;en&#039;],
);

// Discovers every translation file in the directory and registers it.
$registrar = new TranslationResourceRegistrar($translator);
$registrar-&gt;registerDirectory(__DIR__ . &#039;/translations&#039;);
```

### 5. Translate

```php
try {
    // ...
} catch (ValidationException $e) {
    // Untranslated (English, formatted eagerly at construction time).
    echo $e-&gt;getMessage();

    // Translated to the translator&#039;s current locale.
    echo $e-&gt;trans($translator);

    // Translated to a specific locale.
    echo $e-&gt;trans($translator, &#039;es&#039;);
}
```

## Common Use Cases

### Form-Style Validation

```php
class UserValidator
{
    public function validateEmail(string $email): void
    {
        if (empty($email)) {
            throw new ValidationException(&#039;The field {field} is required.&#039;, [&#039;field&#039; =&gt; &#039;email&#039;]);
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new ValidationException([
                &#039;The value {value} is not a valid email.&#039;,
                &#039;value&#039; =&gt; $email,
            ]);
        }
    }
}
```

### API Error Responses

```php
use Derafu\Translation\Contract\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;

class ApiErrorHandler
{
    public function __construct(
        private readonly TranslatorInterface $translator,
    ) {
    }

    public function handle(Throwable $e): array
    {
        $message = $e instanceof TranslatableInterface
            ? $e-&gt;trans($this-&gt;translator)
            : $e-&gt;getMessage();

        return [&#039;error&#039; =&gt; [&#039;message&#039; =&gt; $message]];
    }
}
```

### Complex Messages (Pluralization, Gender)

```php
throw new ValidationException(
    &#039;{count, plural, =0{No files uploaded} one{1 file uploaded} other{# files uploaded}}&#039;,
    [&#039;count&#039; =&gt; $fileCount],
);

throw new ValidationException(
    &#039;{gender, select, female{She} male{He} other{They}} uploaded {count} files.&#039;,
    [&#039;gender&#039; =&gt; $user-&gt;getGender(), &#039;count&#039; =&gt; $fileCount],
);
```

## Next Steps

1. Read about [ICU Message Format](icu-formatting) for pluralization,
   gender selection and more.
2. Learn about [Resource Providers](custom-providers) for multi-directory
   and multi-package setups.
3. Check the [API Reference](api-reference) for every class and interface.
4. See [Advanced Usage](advanced-usage) for composition and DI wiring.

## Common Pitfalls

1. Always include the `other` case in plural/select patterns:

    ```php
    // Wrong.
    &#039;{gender, select, female{She} male{He}}&#039;

    // Correct.
    &#039;{gender, select, female{She} male{He} other{They}}&#039;
    ```

2. Provide every parameter the message uses:

    ```php
    // Missing &#039;field&#039; — the {field} placeholder is left unexpanded.
    throw new ValidationException(&#039;The field {field} is required.&#039;);
    // getMessage(): &quot;The field {field} is required.&quot;

    // Correct.
    throw new ValidationException(&#039;The field {field} is required.&#039;, [&#039;field&#039; =&gt; &#039;email&#039;]);
    // getMessage(): &quot;The field email is required.&quot;
    ```

3. Remember the `+intl-icu` suffix on the domain if your messages use ICU
   syntax (`{name}`, plural, select). Without it, `symfony/translation`
   uses plain `%name%`-style substitution instead, and `{name}` is left
   untouched. See [ICU Formatting](icu-formatting).

## Tips &amp; Tricks

1. Add static factory methods to your exceptions for common cases:

    ```php
    class ValidationException extends TranslatableException
    {
        public static function required(string $field): self
        {
            return new self([&#039;The field {field} is required.&#039;, &#039;field&#039; =&gt; $field]);
        }
    }
    ```

2. Use type hints and PHPDoc for better IDE support:

    ```php
    /**
     * @throws ValidationException When the email is invalid.
     */
    public function validateEmail(string $email): void
    {
        // ...
    }
    ```




---

### API Reference

API Reference

# API Reference

Complete reference for every class and interface `derafu/translation`
ships.

## Interfaces

### TranslatableInterface

`Derafu\Translation\Contract\TranslatableInterface`. Extends Symfony&#039;s own
`Symfony\Contracts\Translation\TranslatableInterface` and adds
`Stringable`, so a translatable value can always be cast to a string (the
untranslated, eagerly ICU-formatted version) even without a translator.

```php
interface TranslatableInterface extends
    \Symfony\Contracts\Translation\TranslatableInterface,
    Stringable
{
    public function __toString(): string;
}
```

`Symfony\Contracts\Translation\TranslatableInterface` itself requires:

```php
public function trans(TranslatorInterface $translator, ?string $locale = null): string;
```

### TranslationResourceProviderInterface

`Derafu\Translation\Contract\TranslationResourceProviderInterface`.
Declares directories containing translation files, meant to be collected
(e.g. via a dependency-injection tagged iterator) so
`TranslationResourceRegistrar` can register every directory without a
central place having to know about each source in advance.

```php
interface TranslationResourceProviderInterface
{
    /**
     * @return iterable&lt;string&gt; Absolute paths to directories with files
     * named `domain(+intl-icu)?.locale.format`.
     */
    public function getDirectories(): iterable;
}
```

## Classes

### TranslatableMessage

`Derafu\Translation\TranslatableMessage`. A message with ICU placeholders
that can be translated using a translator, or eagerly ICU-formatted (as-is)
when no translator is available.

```php
final class TranslatableMessage implements TranslatableInterface
{
    /**
     * @param string $message ICU MessageFormat string, used as the
     * translation id and as the fallback text.
     * @param array&lt;string, mixed&gt; $parameters ICU placeholder values.
     * @param string|null $domain Translation domain, null for Symfony&#039;s
     * default (&#039;messages&#039;).
     * @param string|null $defaultLocale Locale used for eager ICU
     * formatting in __toString() when no translator is available, and as
     * the fallback locale in trans() when none is given.
     */
    public function __construct(
        string $message,
        array $parameters = [],
        ?string $domain = null,
        ?string $defaultLocale = null,
    );

    public function trans(TranslatorInterface $translator, ?string $locale = null): string;

    /**
     * Formats the raw message via ICU, without a translator. Returns the
     * raw message unchanged if the pattern is invalid.
     */
    public function __toString(): string;
}
```

### TranslationResourceRegistrar

`Derafu\Translation\TranslationResourceRegistrar`. Discovers translation
files in directories and registers them as resources of a
`Symfony\Component\Translation\Translator`, using the `LoaderInterface`s
registered on it. This is the piece `symfony/translation` doesn&#039;t provide
standalone (directory discovery is normally wired by a full framework).

```php
final class TranslationResourceRegistrar
{
    public function __construct(private readonly Translator $translator);

    /**
     * Registers every file directly inside a directory.
     *
     * @throws InvalidArgumentException If the directory doesn&#039;t exist, a
     * file&#039;s name doesn&#039;t follow the `domain.locale.format` convention, or
     * its extension isn&#039;t supported.
     */
    public function registerDirectory(string $directory): void;

    /**
     * @param iterable&lt;string&gt; $directories
     */
    public function registerDirectories(iterable $directories): void;

    /**
     * @param iterable&lt;TranslationResourceProviderInterface&gt; $providers
     */
    public function registerFromProviders(iterable $providers): void;

    /** @return array&lt;string&gt; Every locale registered so far. */
    public function getRegisteredLocales(): array;

    /** @return array&lt;string&gt; Every domain registered so far. */
    public function getRegisteredDomains(): array;
}
```

Registration order is precedence order: for the same key in the same
domain/locale, the **last** directory registered wins.

Supported file extensions: `yaml`, `yml`, `json`, `php`, `xlf`, `xliff`,
`po`, `mo`, `csv`, `ini`.

### SimpleTranslationResourceProvider

`Derafu\Translation\SimpleTranslationResourceProvider`. Default
implementation of `TranslationResourceProviderInterface` that wraps a
fixed list of directories.

```php
final class SimpleTranslationResourceProvider implements TranslationResourceProviderInterface
{
    /** @param iterable&lt;string&gt; $directories */
    public function __construct(private readonly iterable $directories);

    public function getDirectories(): iterable;
}
```

### TranslatorFactory

`Derafu\Translation\TranslatorFactory`. Builds a
`Symfony\Component\Translation\Translator` with every file loader
`TranslationResourceRegistrar` supports already registered, plus the given
resource providers, plus fallback locales.

```php
final class TranslatorFactory
{
    /**
     * @param array&lt;string&gt; $fallbackLocales
     * @param iterable&lt;TranslationResourceProviderInterface&gt; $resourceProviders
     * Registered immediately, inside this call, so registration always
     * happens whenever a Translator is built — regardless of whether
     * anything else references a registrar directly (a dependency-injected
     * service nothing depends on is never instantiated by the container).
     */
    public static function create(
        string $defaultLocale,
        array $fallbackLocales = [],
        iterable $resourceProviders = [],
    ): Translator;
}
```

## Trait

### TranslatableExceptionTrait

`Derafu\Translation\Trait\TranslatableExceptionTrait`. Adds translation
support to any exception. Used internally by every exception class listed
below — apply it directly only if you can&#039;t extend one of them (see
[Exceptions](exceptions)).

```php
trait TranslatableExceptionTrait
{
    protected string $defaultDomain = &#039;errors&#039;;
    protected string $defaultLocale = &#039;en&#039;;

    /**
     * @param string|array|TranslatableInterface $message
     *   - string: used as both message and translation id.
     *   - array: first element is the message/id, the rest are named ICU
     *     parameters.
     *   - TranslatableInterface: used directly.
     * @throws InvalidArgumentException When an empty array is given, or
     * its first element isn&#039;t a string.
     */
    public function __construct(
        string|array|TranslatableInterface $message,
        int $code = 0,
        ?Throwable $previous = null,
    );

    public function trans(TranslatorInterface $translator, ?string $locale = null): string;

    /** @return array&lt;string, mixed&gt; */
    public function __serialize(): array;

    /** @param array&lt;string, mixed&gt; $data */
    public function __unserialize(array $data): void;
}
```

`__serialize()`/`__unserialize()` intentionally drop the stack trace (it
may contain non-serializable values such as closures or resources), so
these exceptions can safely be stored in sessions (e.g. for flash
messages). After unserializing, `getTrace()` no longer reflects the
exception&#039;s original call stack.

## Exception Hierarchy

Every class below implements `TranslatableInterface` via
`TranslatableExceptionTrait`, and mirrors the equivalent native PHP SPL
exception:

| Class | Extends |
| --- | --- |
| `Exception\Core\TranslatableException` | `Exception` |
| `Exception\Core\TranslatableLogicException` | `LogicException` |
| `Exception\Core\TranslatableRuntimeException` | `RuntimeException` |
| `Exception\Logic\TranslatableDomainException` | `DomainException` |
| `Exception\Logic\TranslatableInvalidArgumentException` | `InvalidArgumentException` |
| `Exception\Logic\TranslatableLengthException` | `LengthException` |
| `Exception\Logic\TranslatableOutOfRangeException` | `OutOfRangeException` |
| `Exception\Runtime\TranslatableOutOfBoundsException` | `OutOfBoundsException` |
| `Exception\Runtime\TranslatableOverflowException` | `OverflowException` |
| `Exception\Runtime\TranslatableRangeException` | `RangeException` |
| `Exception\Runtime\TranslatableUnderflowException` | `UnderflowException` |
| `Exception\Runtime\TranslatableUnexpectedValueException` | `UnexpectedValueException` |

All under the `Derafu\Translation\Exception\` namespace.

## Dependencies

`derafu/translation` requires:

- `php: ^8.5`
- `ext-intl` (PHP&#039;s ICU bindings, for `\MessageFormatter`)
- `symfony/translation-contracts: ^3.7`
- `symfony/yaml: ^8.1`
- `symfony/translation: ^8.1`

No dependency on `symfony/framework-bundle`, `symfony/http-kernel`, or
`symfony/dependency-injection` — the dependency-injection recipe described
in [Advanced Usage](advanced-usage) is entirely optional wiring that lives
in the consuming application.




---

### Exceptions

Working with Translatable Exceptions

# Working with Translatable Exceptions

This guide covers the translatable exception hierarchy shipped by
`derafu/translation`.

## A Note on Translation Ids

Every example in this guide uses **the real English message text as the
translation id** (e.g. `&#039;The field {field} is required.&#039;`), not an
abstract key like `validation.required`. This is deliberate: Symfony&#039;s own
translation documentation recommends real text for shared libraries
specifically (as opposed to end-user applications, where abstract keys
make more sense), so that code stays readable and still produces a sane
message even when no translator — or no matching translation — is
available. `TranslatableExceptionTrait` is built around this idea: the
message you throw with is always what `getMessage()` returns when
untranslated.

## Available Exceptions

The library mirrors PHP&#039;s own SPL exception hierarchy with translatable
equivalents.

### Core Exceptions

- `Derafu\Translation\Exception\Core\TranslatableException` — extends
  `Exception`.
- `Derafu\Translation\Exception\Core\TranslatableLogicException` — extends
  `LogicException`.
- `Derafu\Translation\Exception\Core\TranslatableRuntimeException` —
  extends `RuntimeException`.

### Logic Exceptions

- `Derafu\Translation\Exception\Logic\TranslatableDomainException` —
  extends `DomainException`.
- `Derafu\Translation\Exception\Logic\TranslatableInvalidArgumentException`
  — extends `InvalidArgumentException`.
- `Derafu\Translation\Exception\Logic\TranslatableLengthException` —
  extends `LengthException`.
- `Derafu\Translation\Exception\Logic\TranslatableOutOfRangeException` —
  extends `OutOfRangeException`.

### Runtime Exceptions

- `Derafu\Translation\Exception\Runtime\TranslatableOutOfBoundsException` —
  extends `OutOfBoundsException`.
- `Derafu\Translation\Exception\Runtime\TranslatableOverflowException` —
  extends `OverflowException`.
- `Derafu\Translation\Exception\Runtime\TranslatableRangeException` —
  extends `RangeException`.
- `Derafu\Translation\Exception\Runtime\TranslatableUnderflowException` —
  extends `UnderflowException`.
- `Derafu\Translation\Exception\Runtime\TranslatableUnexpectedValueException`
  — extends `UnexpectedValueException`.

Every class above implements `TranslatableInterface` and uses
`TranslatableExceptionTrait` internally, so they all share the same
constructor and `trans()` method described below.

## Using the Trait Directly

### TranslatableExceptionTrait

If you can&#039;t (or don&#039;t want to) extend one of the exceptions above — for
example, because you already extend something else — use the trait
instead:

```php
use Derafu\Translation\Contract\TranslatableInterface;
use Derafu\Translation\Trait\TranslatableExceptionTrait;
use DomainException;
use Throwable;

class OrderException extends DomainException implements TranslatableInterface
{
    use TranslatableExceptionTrait;

    // $defaultDomain is a *typed* property on the trait, so PHP does not
    // allow overriding its default value by simply redeclaring it here
    // (that&#039;s a fatal &quot;incompatible property definition&quot; error). Set it in
    // the constructor instead, before calling normalizeMessage().
    public function __construct(
        string|array|TranslatableInterface $message,
        int $code = 0,
        ?Throwable $previous = null,
    ) {
        $this-&gt;defaultDomain = &#039;orders&#039;;
        parent::__construct($this-&gt;normalizeMessage($message), $code, $previous);
    }

    public static function insufficientStock(string $product): self
    {
        return new self([&#039;Not enough stock for &quot;{product}&quot;.&#039;, &#039;product&#039; =&gt; $product]);
    }
}
```

### When to Use the Trait vs. Extending a Base Exception

Use the trait when:

- You already extend another exception class.
- You need custom behavior beyond translation.

Use one of the base exceptions when:

- You don&#039;t need custom behavior.
- You want the simplest possible implementation:

    ```php
    class ValidationException extends TranslatableDomainException
    {
        // Inherits everything from the trait, with no extra code.
    }
    ```

## The Constructor

All translatable exceptions accept the same three argument shapes:

```php
public function __construct(
    string|array|TranslatableInterface $message,
    int $code = 0,
    ?Throwable $previous = null,
)
```

- **`string`**: used as both the exception message and the translation id.
- **`array`**: the first element is the message/id, the remaining elements
  are named ICU parameters (`[&#039;The field {field} is required.&#039;, &#039;field&#039; =&gt; &#039;email&#039;]`).
- **`TranslatableInterface`**: a `TranslatableMessage` (or any other
  implementation) built directly — useful when you want to reuse the same
  translatable message in more than one place.

## Default Domain and Locale

`TranslatableExceptionTrait` defines:

```php
protected string $defaultDomain = &#039;errors&#039;;
protected string $defaultLocale = &#039;en&#039;;
```

Override `$defaultDomain` in your own exception class to group its
messages under a different translation domain. This works fine through
normal class inheritance, when you extend one of the built-in exceptions
(they already apply the trait for you):

```php
class OrderException extends TranslatableDomainException
{
    protected string $defaultDomain = &#039;orders&#039;;
}
```

If you `use TranslatableExceptionTrait` directly in your own class instead
(see above), redeclaring `$defaultDomain` in that *same* class fails with a
fatal &quot;incompatible property definition&quot; error — PHP does not allow a
class to override the default value of a trait&#039;s own *typed* property by
redeclaring it in the class that uses the trait. Set it in the constructor
instead in that case, as shown in the `OrderException` example above.

## Best Practices

1. **Choose the most specific exception type available.** It carries
   semantic meaning for callers doing `catch (SomeSplException $e)`, even
   before translation comes into play.
2. **Use the real message text as the id**, per the note above — don&#039;t
   introduce abstract keys unless you have a specific reason to (e.g. a
   dedicated translation-management workflow that needs stable ids
   independent of wording).
3. **Provide every ICU parameter the message references.** A missing
   parameter is left unexpanded in the output rather than throwing.
4. **Group related exceptions under a domain-specific base class:**

    ```php
    abstract class OrderException extends TranslatableDomainException
    {
        protected string $defaultDomain = &#039;orders&#039;;
    }

    class OrderNotFoundException extends OrderException {}
    class OrderValidationException extends OrderException {}
    ```




---

### ICU Formatting

ICU Message Formatting Guide

# ICU Message Formatting Guide

`derafu/translation` doesn&#039;t implement its own message formatter. ICU
formatting comes directly from PHP&#039;s `intl` extension (`\MessageFormatter`),
used in two places:

1. **`TranslatableMessage::__toString()`** — formats the raw message
   eagerly, without a translator. This is what makes `getMessage()` on a
   translatable exception return a fully formatted string with zero setup.
2. **`symfony/translation`&#039;s own ICU formatter** — used when translating
   through a real `Translator`, for any domain whose name ends in
   `+intl-icu` (see [Resource Providers](custom-providers) and the
   [API Reference](api-reference)).

Both paths accept the exact same ICU MessageFormat syntax described below.

## Basic Placeholders

The simplest form uses named placeholders:

```php
throw new ValidationException(
    &#039;The field {field} is required.&#039;,
    [&#039;field&#039; =&gt; &#039;email&#039;],
);
// getMessage(): &quot;The field email is required.&quot;
```

Multiple placeholders are supported:

```php
throw new ValidationException(
    &#039;Value {value} for field {field} is invalid.&#039;,
    [&#039;value&#039; =&gt; &#039;test@&#039;, &#039;field&#039; =&gt; &#039;email&#039;],
);
// getMessage(): &quot;Value test@ for field email is invalid.&quot;
```

## Pluralization

```php
$id = &#039;{count, plural, =0{No messages} one{# message} other{# messages}}&#039;;

new TranslatableMessage($id, [&#039;count&#039; =&gt; 2]); // &quot;2 messages&quot;
new TranslatableMessage($id, [&#039;count&#039; =&gt; 1]); // &quot;1 message&quot;
new TranslatableMessage($id, [&#039;count&#039; =&gt; 0]); // &quot;No messages&quot;
```

Available plural categories:

- `zero`: for languages with a special zero form.
- `one`: singular form.
- `two`: dual form (for languages that have it).
- `few` / `many`: for languages with special handling of small/large
  numbers.
- `other`: default form (**required** — always provide it).
- `=n`: exact number matches (e.g. `=0`).

## Gender / Select

```php
$id = &#039;{gender, select, female {She liked your post} male {He liked your post} other {They liked your post}}&#039;;

(string) new TranslatableMessage($id, [&#039;gender&#039; =&gt; &#039;female&#039;]);
// &quot;She liked your post&quot;
```

Nested placeholders work too:

```php
$id = &#039;{gender, select, female {{name} added her comment} male {{name} added his comment} other {{name} added their comment}}&#039;;

(string) new TranslatableMessage($id, [&#039;gender&#039; =&gt; &#039;female&#039;, &#039;name&#039; =&gt; &#039;Alice&#039;]);
// &quot;Alice added her comment&quot;
```

## Number Formatting

```php
&#039;{value, number}&#039;          // Plain number.
&#039;{value, number, percent}&#039; // Percentage.
&#039;{value, number, currency}&#039; // Currency, using the message&#039;s own locale.
```

Currency formatting depends on the locale carrying a specific currency
association (e.g. `en_US`, not just `en`):

```php
new TranslatableMessage(
    &#039;Balance must be greater than {min, number, currency}.&#039;,
    [&#039;min&#039; =&gt; 100],
    null,
    &#039;en_US&#039;,
);
// &quot;Balance must be greater than $100.00.&quot;
```

## Nested Plural + Select

Patterns can be nested for combined scenarios:

```php
$id = &#039;{gender, select,
    female {{count, plural, =0{She has no messages} one{She has # message} other{She has # messages}}}
    male {{count, plural, =0{He has no messages} one{He has # message} other{He has # messages}}}
    other {{count, plural, =0{They have no messages} one{They have # message} other{They have # messages}}}
}&#039;;

(string) new TranslatableMessage($id, [&#039;gender&#039; =&gt; &#039;female&#039;, &#039;count&#039; =&gt; 5]);
// &quot;She has 5 messages&quot;
```

## Common Patterns

```php
// Range validation.
&#039;Value must be between {min} and {max}.&#039;

// List validation.
&#039;{count, plural, =0{List cannot be empty} one{At least one item is required} other{At least # items are required}}&#039;

// Status messages.
&#039;{status, select, pending{Waiting for approval} approved{Approved on {date}} rejected{Rejected: {reason}} other{Unknown status}}&#039;
```

## Troubleshooting

1. **Missing the `other` category** — required for every `select`/`plural`
   pattern. Without it, a value that doesn&#039;t match any listed category
   falls back to the raw, unformatted message (same as an invalid pattern,
   see below):

    ```php
    // Wrong.
    &#039;{gender, select, male{He} female{She}}&#039;

    // Correct.
    &#039;{gender, select, male{He} female{She} other{They}}&#039;
    ```

2. **Unmatched braces:**

    ```php
    // Wrong.
    &#039;Hello {name&#039;

    // Correct.
    &#039;Hello {name}&#039;
    ```

   `TranslatableMessage::__toString()` catches this: an invalid ICU pattern
   returns the raw message unchanged rather than throwing.

3. **Missing parameters** don&#039;t throw either — the placeholder is simply
   left unexpanded:

    ```php
    (string) new TranslatableMessage(&#039;{count} items&#039;); // &quot;{count} items&quot;
    ```

---

For more on ICU MessageFormat syntax:

- [ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
- [PHP `MessageFormatter` manual](https://www.php.net/manual/en/class.messageformatter.php)




---

### Resource Providers

Resource Providers

# Resource Providers

This guide explains `TranslationResourceProviderInterface` — the piece
that lets you (or a dependency injection container) discover translation
directories from multiple sources — and when you&#039;d write a custom
`Symfony\Component\Translation\Loader\LoaderInterface` instead.

## What a Resource Provider Is (and Isn&#039;t)

`TranslationResourceProviderInterface` declares **directories containing
translation files**. It does not read or parse anything itself — that&#039;s
`TranslationResourceRegistrar`&#039;s job (see the
[API Reference](api-reference)), using `symfony/translation`&#039;s own file
loaders (YAML, JSON, PHP, XLIFF, PO, MO, CSV, INI).

```php
interface TranslationResourceProviderInterface
{
    /**
     * @return iterable&lt;string&gt; Absolute paths to directories containing
     * translation files, named `domain(+intl-icu)?.locale.format`.
     */
    public function getDirectories(): iterable;
}
```

If your translations always live in files on disk, this is the interface
to implement. If they come from somewhere else entirely (a database, an
API, Redis), see [Non-File Sources](#non-file-sources) below — that&#039;s a
different extension point.

## The Built-In Implementation

For the common case — &quot;I have N directories and that&#039;s it&quot; —
`SimpleTranslationResourceProvider` already does the job:

```php
use Derafu\Translation\SimpleTranslationResourceProvider;

$provider = new SimpleTranslationResourceProvider([
    __DIR__ . &#039;/translations&#039;,
    __DIR__ . &#039;/vendor/some-package/translations&#039;,
]);
```

## Writing Your Own

A custom provider is useful when the set of directories isn&#039;t a fixed,
hardcoded list — for example, when it needs to be computed:

```php
use Derafu\Translation\Contract\TranslationResourceProviderInterface;

final class PackageTranslationResourceProvider implements TranslationResourceProviderInterface
{
    public function getDirectories(): iterable
    {
        // Every installed vendor package that ships its own translations,
        // discovered instead of hardcoded.
        foreach (glob(__DIR__ . &#039;/../../vendor/*/*/resources/translations&#039;, GLOB_ONLYDIR) as $dir) {
            yield $dir;
        }
    }
}
```

This is exactly the pattern used to expose a library&#039;s own bundled
translations to whatever application consumes it: the library ships a
small provider class pointing at its own `resources/translations`
directory, and the consuming application registers it (directly, or via a
tagged dependency-injection collection — see
[Advanced Usage](advanced-usage)).

```php
use Derafu\Translation\Contract\TranslationResourceProviderInterface;

final class MyLibraryTranslationResourceProvider implements TranslationResourceProviderInterface
{
    public function getDirectories(): iterable
    {
        return [__DIR__ . &#039;/../../resources/translations&#039;];
    }
}
```

## Registering Providers

`TranslationResourceRegistrar::registerFromProviders()` accepts any
`iterable` of `TranslationResourceProviderInterface` instances:

```php
use Derafu\Translation\TranslationResourceRegistrar;

$registrar = new TranslationResourceRegistrar($translator);
$registrar-&gt;registerFromProviders([
    new SimpleTranslationResourceProvider([__DIR__ . &#039;/translations&#039;]),
    new PackageTranslationResourceProvider(),
]);
```

Or pass them straight to `TranslatorFactory::create()`, which registers
them as part of building the translator (see the
[API Reference](api-reference) for why that matters):

```php
use Derafu\Translation\TranslatorFactory;

$translator = TranslatorFactory::create(
    defaultLocale: &#039;es&#039;,
    resourceProviders: [
        new SimpleTranslationResourceProvider([__DIR__ . &#039;/translations&#039;]),
        new PackageTranslationResourceProvider(),
    ],
);
```

Registration order is precedence order: when two directories define the
same key for the same domain/locale, the **last one registered wins**. If
you want your own application&#039;s translations to be able to override a
dependency&#039;s, register the dependency&#039;s provider(s) first and your own
last.

## Non-File Sources

If your translations genuinely don&#039;t live in files (a database table, a
remote API, Redis), `TranslationResourceProviderInterface` isn&#039;t the right
extension point — it only ever produces *directories*. Instead, write a
`Symfony\Component\Translation\Loader\LoaderInterface` and register it
directly on the `Translator`:

```php
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;

final class DatabaseLoader implements LoaderInterface
{
    public function __construct(private readonly PDO $db)
    {
    }

    public function load(mixed $resource, string $locale, string $domain = &#039;messages&#039;): MessageCatalogue
    {
        $stmt = $this-&gt;db-&gt;prepare(
            &#039;SELECT message_key, message_text FROM translations WHERE locale = ? AND domain = ?&#039;
        );
        $stmt-&gt;execute([$locale, $domain]);

        $catalogue = new MessageCatalogue($locale);
        $catalogue-&gt;add($stmt-&gt;fetchAll(PDO::FETCH_KEY_PAIR), $domain);

        return $catalogue;
    }
}
```

```php
$translator-&gt;addLoader(&#039;database&#039;, new DatabaseLoader($pdo));
$translator-&gt;addResource(&#039;database&#039;, &#039;irrelevant-for-this-loader&#039;, &#039;es&#039;, &#039;errors+intl-icu&#039;);
```

This bypasses `TranslationResourceRegistrar` entirely — it&#039;s a direct use
of `symfony/translation`&#039;s own extension point, which this library builds
on rather than replaces.




---

### Advanced Usage

Advanced Usage Guide

# Advanced Usage Guide

This guide covers less common, but still fully supported, patterns.

## Reusable Message Templates

`TranslatableMessage` instances are plain value objects — build them once
and reuse them wherever a `TranslatableInterface` is expected (including
as an exception&#039;s message):

```php
use Derafu\Translation\Contract\TranslatableInterface;
use Derafu\Translation\TranslatableMessage;

final class Messages
{
    public static function required(string $field): TranslatableInterface
    {
        return new TranslatableMessage(&#039;The field {field} is required.&#039;, [&#039;field&#039; =&gt; $field]);
    }

    public static function invalid(string $field, mixed $value): TranslatableInterface
    {
        return new TranslatableMessage(
            &#039;The value {value} for field {field} is invalid.&#039;,
            [&#039;field&#039; =&gt; $field, &#039;value&#039; =&gt; (string) $value],
        );
    }
}

throw new ValidationException(Messages::required(&#039;email&#039;));
```

## Multiple Directories and Precedence

`TranslationResourceRegistrar` accepts a single directory, a list of
directories, or a collection of
[resource providers](custom-providers) — and registration order defines
precedence:

```php
use Derafu\Translation\TranslationResourceRegistrar;

$registrar = new TranslationResourceRegistrar($translator);
$registrar-&gt;registerDirectories([
    __DIR__ . &#039;/vendor/some-dependency/translations&#039;,
    __DIR__ . &#039;/translations&#039;, // Your own, registered last: it can override the dependency&#039;s keys.
]);

$registrar-&gt;getRegisteredLocales(); // e.g. [&#039;en&#039;, &#039;es&#039;]
$registrar-&gt;getRegisteredDomains(); // e.g. [&#039;errors+intl-icu&#039;]
```

Prefer feeding providers straight into `TranslatorFactory::create()`
instead of instantiating `TranslationResourceRegistrar` yourself whenever
you can — see the [API Reference](api-reference) for why registration
needs to happen there rather than through a separately fetched registrar
object.

## Dependency Injection (`symfony/dependency-injection`)

`derafu/translation` doesn&#039;t depend on `symfony/dependency-injection` (or
any framework) — this is entirely optional wiring for applications that
choose to use it. The package ships
`resources/config/translation-services.yaml` with one thing: an alias from
the translation-contracts interface to the concrete `Translator` service,
so that code type-hinting the interface (as it should) still resolves
correctly:

```yaml
services:
    Symfony\Contracts\Translation\TranslatorInterface: &#039;@Symfony\Component\Translation\Translator&#039;
```

Import it, then register your own `Translator` and any
`TranslationResourceProviderInterface` implementations, tagged so they can
be collected automatically:

```yaml
imports:
    - { resource: &#039;../vendor/derafu/translation/resources/config/translation-services.yaml&#039; }

services:
    App\Translation\MyTranslationResourceProvider:
        tags: [&#039;derafu_translation.resource_provider&#039;]

    Symfony\Component\Translation\Translator:
        factory: [&#039;Derafu\Translation\TranslatorFactory&#039;, &#039;create&#039;]
        arguments:
            $defaultLocale: &#039;es&#039;
            $fallbackLocales: [&#039;es&#039;, &#039;en&#039;]
            $resourceProviders: !tagged_iterator derafu_translation.resource_provider
```

Note the tag is applied explicitly on each provider service, rather than
relying on Symfony&#039;s `_instanceof` autoconfiguration: `_instanceof` (like
`_defaults`) only applies to services declared in the *same* YAML file that
declares it — it does not propagate to services declared in a different
file, even one that imports it.

## Decorating the Translator

Since `derafu/translation` builds directly on
`Symfony\Contracts\Translation\TranslatorInterface`, any standard decorator
pattern for that interface works unchanged — for example, logging every
translation lookup:

```php
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

final class LoggingTranslator implements TranslatorInterface
{
    public function __construct(
        private readonly TranslatorInterface $translator,
        private readonly LoggerInterface $logger,
    ) {
    }

    public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
    {
        $result = $this-&gt;translator-&gt;trans($id, $parameters, $domain, $locale);

        $this-&gt;logger-&gt;debug(&#039;Translation performed.&#039;, [
            &#039;id&#039; =&gt; $id,
            &#039;domain&#039; =&gt; $domain,
            &#039;locale&#039; =&gt; $locale,
            &#039;result&#039; =&gt; $result,
        ]);

        return $result;
    }

    public function getLocale(): string
    {
        return $this-&gt;translator-&gt;getLocale();
    }
}
```

Wrap the `Translator` built by `TranslatorFactory::create()` with your
decorator, and pass the decorator — not the raw translator — to `trans()`
calls and to anything else that needs a `TranslatorInterface`:

```php
$translator = new LoggingTranslator(TranslatorFactory::create(&#039;en&#039;), $logger);

echo $translator-&gt;trans(&#039;Hello {name}&#039;, [&#039;name&#039; =&gt; &#039;John&#039;], &#039;messages+intl-icu&#039;);
// Logs the lookup, then: &quot;Hello John&quot;
```

Remember the `+intl-icu` domain suffix here too — the decorator forwards
whatever domain it&#039;s given straight to the wrapped translator, so the same
[ICU rules](icu-formatting) apply.




---

### Real World

Real World Examples

# Real World Examples

Practical, framework-agnostic examples of using `derafu/translation`.

## API Error Responses

```php
use Derafu\Translation\Contract\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;

final class ApiErrorHandler
{
    public function __construct(
        private readonly TranslatorInterface $translator,
    ) {
    }

    /**
     * @return array{error: array{message: string, code: int}}
     */
    public function handle(Throwable $e, ?string $locale = null): array
    {
        $message = $e instanceof TranslatableInterface
            ? $e-&gt;trans($this-&gt;translator, $locale)
            : $e-&gt;getMessage();

        return [
            &#039;error&#039; =&gt; [
                &#039;message&#039; =&gt; $message,
                &#039;code&#039; =&gt; $e-&gt;getCode(),
            ],
        ];
    }
}
```

## Field-by-Field Validation Errors

A common pattern: collect one translatable exception per invalid field,
then translate them all at once when building the response. Remember to
register a `errors+intl-icu.*.*` resource (`ValidationException` uses the
`errors` domain by default) — without one, `trans()` falls back to
`symfony/translation`&#039;s plain `%name%`-style substitution instead of ICU,
which does not understand `{value}`-style placeholders. See
[ICU Formatting](icu-formatting) for the full explanation.

```php
use Derafu\Translation\Exception\Core\TranslatableException;
use Symfony\Contracts\Translation\TranslatorInterface;

final class ValidationException extends TranslatableException
{
}

final class Validator
{
    /**
     * @return array&lt;string, ValidationException&gt; Keyed by field name.
     */
    public function validate(array $data): array
    {
        $errors = [];

        if (empty($data[&#039;email&#039;])) {
            $errors[&#039;email&#039;] = new ValidationException([
                &#039;The field {field} is required.&#039;,
                &#039;field&#039; =&gt; &#039;email&#039;,
            ]);
        } elseif (!filter_var($data[&#039;email&#039;], FILTER_VALIDATE_EMAIL)) {
            $errors[&#039;email&#039;] = new ValidationException([
                &#039;The value {value} is not a valid email.&#039;,
                &#039;value&#039; =&gt; $data[&#039;email&#039;],
            ]);
        }

        return $errors;
    }
}

function formatErrors(array $errors, TranslatorInterface $translator, string $locale): array
{
    $formatted = [];
    foreach ($errors as $field =&gt; $exception) {
        $formatted[$field] = $exception-&gt;trans($translator, $locale);
    }
    return $formatted;
}
```

```php
// translations/errors+intl-icu.es.php
return [
    &#039;The field {field} is required.&#039; =&gt; &#039;El campo {field} es obligatorio.&#039;,
    &#039;The value {value} is not a valid email.&#039; =&gt; &#039;El valor {value} no es un correo electrónico válido.&#039;,
];
```

## Domain-Specific Exception Hierarchies

Group related exceptions under a shared base class with its own
translation domain (see [Exceptions](exceptions) for why `$defaultDomain`
must be overridden this way):

```php
use Derafu\Translation\Exception\Logic\TranslatableDomainException;

abstract class OrderException extends TranslatableDomainException
{
    protected string $defaultDomain = &#039;orders&#039;;
}

final class InsufficientStockException extends OrderException
{
    public static function forProduct(string $product, int $requested, int $available): self
    {
        return new self([
            &#039;Not enough stock for &quot;{product}&quot;: requested {requested}, available {available}.&#039;,
            &#039;product&#039; =&gt; $product,
            &#039;requested&#039; =&gt; $requested,
            &#039;available&#039; =&gt; $available,
        ]);
    }
}

final class InvalidStatusTransitionException extends OrderException
{
    public static function fromTo(string $from, string $to): self
    {
        return new self([
            &#039;Cannot transition order from &quot;{from}&quot; to &quot;{to}&quot;.&#039;,
            &#039;from&#039; =&gt; $from,
            &#039;to&#039; =&gt; $to,
        ]);
    }
}
```

```php
// translations/orders+intl-icu.es.php
return [
    &#039;Not enough stock for &quot;{product}&quot;: requested {requested}, available {available}.&#039; =&gt;
        &#039;No hay stock suficiente de &quot;{product}&quot;: se pidieron {requested}, hay {available} disponibles.&#039;,
    &#039;Cannot transition order from &quot;{from}&quot; to &quot;{to}&quot;.&#039; =&gt;
        &#039;No se puede pasar el pedido de &quot;{from}&quot; a &quot;{to}&quot;.&#039;,
];
```

## CLI Tools

Translatable exceptions work just as well outside of HTTP contexts —
useful for CLI tools that need to report errors in the operator&#039;s
language:

```php
use Derafu\Translation\TranslatorFactory;
use Derafu\Translation\TranslationResourceRegistrar;

$locale = getenv(&#039;APP_LOCALE&#039;) ?: &#039;en&#039;;

$translator = TranslatorFactory::create($locale, [&#039;en&#039;]);
(new TranslationResourceRegistrar($translator))-&gt;registerDirectory(__DIR__ . &#039;/translations&#039;);

try {
    runCommand();
} catch (Throwable $e) {
    $message = $e instanceof \Derafu\Translation\Contract\TranslatableInterface
        ? $e-&gt;trans($translator)
        : $e-&gt;getMessage();

    fwrite(STDERR, $message . PHP_EOL);
    exit(1);
}
```




---

### Symfony Integration

Symfony Integration

# Symfony Integration

`derafu/translation` doesn&#039;t *integrate with* `symfony/translation` the way
a bridge or an adapter would — it&#039;s built directly **on top of** it.
`TranslatorFactory::create()` returns a real
`Symfony\Component\Translation\Translator` instance, not a custom
wrapper. Everything this library adds (translatable exceptions, resource
discovery across directories) sits alongside that instance, not between
your code and it.

## No Framework Dependency

The only hard dependencies are `symfony/translation`,
`symfony/translation-contracts`, `symfony/yaml`, and the `intl` PHP
extension. There is no dependency on `symfony/framework-bundle` or
`symfony/http-kernel`, and there will not be one — this library works in
any PHP application, framework or not.

## Using the Real `Translator` Directly

Because `TranslatorFactory::create()` gives you the actual Symfony class,
every native method is available — this library doesn&#039;t hide or wrap it:

```php
use Derafu\Translation\TranslatorFactory;

$translator = TranslatorFactory::create(&#039;es&#039;, [&#039;es&#039;, &#039;en&#039;]);

// Native Symfony\Component\Translation\Translator methods, all available.
$translator-&gt;setLocale(&#039;en&#039;);
$translator-&gt;getFallbackLocales();
$translator-&gt;getCatalogue(&#039;es&#039;)-&gt;all(&#039;errors+intl-icu&#039;);
```

If you need a loader this library doesn&#039;t pre-wire (for example,
`IcuResFileLoader` or `IcuDatFileLoader`, or your own custom loader — see
[Resource Providers](custom-providers)), add it the normal Symfony way:

```php
$translator-&gt;addLoader(&#039;my_format&#039;, new MyCustomLoader());
```

## Locale-Aware Contracts

`Symfony\Component\Translation\Translator` also implements
`Symfony\Component\Translation\TranslatorBagInterface` and
`Symfony\Contracts\Translation\LocaleAwareInterface`, so it can be used
anywhere those contracts are expected, not just where
`Symfony\Contracts\Translation\TranslatorInterface` is required — which is
the interface `TranslatableExceptionTrait::trans()` itself expects.

## Dependency Injection

If your application uses `symfony/dependency-injection` (with or without
the rest of the Symfony Framework), see [Advanced Usage](advanced-usage)
for the wiring recipe this package ships.




---

### Testing

Testing Guide

# Testing Guide

How to test code that uses `derafu/translation` — translatable exceptions,
ICU formatting, and resource registration.

## Testing Untranslated Behavior

Since `getMessage()` is fully formatted at construction time, most tests
don&#039;t need a translator at all:

```php
use PHPUnit\Framework\TestCase;

final class ValidationExceptionTest extends TestCase
{
    public function testPlainStringMessage(): void
    {
        $exception = new ValidationException(&#039;Email is invalid.&#039;);

        $this-&gt;assertSame(&#039;Email is invalid.&#039;, $exception-&gt;getMessage());
    }

    public function testIcuParametersAreSubstitutedEagerly(): void
    {
        $exception = new ValidationException([
            &#039;The value {value} is not a valid email.&#039;,
            &#039;value&#039; =&gt; &#039;test@example&#039;,
        ]);

        $this-&gt;assertSame(
            &#039;The value test@example is not a valid email.&#039;,
            $exception-&gt;getMessage(),
        );
    }
}
```

## Testing with a Stub Translator

For unit tests, a `TranslatorInterface` stub configured with
`willReturnCallback()` is usually enough — it doesn&#039;t need to verify how
many times it was called, just what it returns:

```php
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;

final class OrderExceptionTest extends TestCase
{
    public function testTranslation(): void
    {
        $translator = $this-&gt;createStub(TranslatorInterface::class);
        $translator-&gt;method(&#039;trans&#039;)-&gt;willReturnCallback(
            fn (string $id, array $parameters = []): string =&gt; match ($id) {
                &#039;Not enough stock for &quot;{product}&quot;.&#039; =&gt; sprintf(
                    &#039;No hay stock de &quot;%s&quot;.&#039;,
                    $parameters[&#039;product&#039;],
                ),
                default =&gt; $id,
            },
        );

        $exception = OrderException::insufficientStock(&#039;Widget&#039;);

        $this-&gt;assertSame(
            &#039;No hay stock de &quot;Widget&quot;.&#039;,
            $exception-&gt;trans($translator, &#039;es&#039;),
        );
    }
}
```

Use `createMock()` instead of `createStub()` only when you actually need to
assert on the number of calls (e.g. `expects($this-&gt;once())`) — PHPUnit
flags `createMock()` used purely for return-value stubbing as unnecessary.

## Testing with a Real Translator

For integration tests, use the real `TranslatorFactory` +
`TranslationResourceRegistrar` against fixture translation files —
verifying the actual behavior end to end, ICU formatting included.

Fixtures for the examples below (`tests/fixtures/translations/`):

```php
// errors+intl-icu.es.php
return [
    &#039;The value {value} is not a valid email.&#039; =&gt; &#039;El valor {value} no es un correo electrónico válido.&#039;,
];
```

```php
// messages+intl-icu.en.php
return [
    &#039;welcome&#039; =&gt; &#039;Welcome!&#039;,
];
```

And an extra directory used only by the precedence test below,
`tests/fixtures/translations-override/`:

```php
// messages+intl-icu.en.php
return [
    &#039;welcome&#039; =&gt; &#039;Overridden message.&#039;,
];
```

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;
use Symfony\Component\Translation\Translator;

final class TranslationIntegrationTest extends TestCase
{
    private Translator $translator;

    protected function setUp(): void
    {
        $this-&gt;translator = TranslatorFactory::create(&#039;en&#039;, [&#039;en&#039;, &#039;es&#039;]);

        $registrar = new TranslationResourceRegistrar($this-&gt;translator);
        $registrar-&gt;registerDirectory(__DIR__ . &#039;/../fixtures/translations&#039;);
    }

    public function testValidationErrorIsTranslated(): void
    {
        $exception = new ValidationException([
            &#039;The value {value} is not a valid email.&#039;,
            &#039;value&#039; =&gt; &#039;test@example&#039;,
        ]);

        $this-&gt;assertSame(
            &#039;El valor test@example no es un correo electrónico válido.&#039;,
            $exception-&gt;trans($this-&gt;translator, &#039;es&#039;),
        );
    }
}
```

## Testing Resource Registration

Test `TranslationResourceRegistrar` directly against a fixtures directory
to verify discovery and precedence, without needing any specific exception
class:

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;

final class TranslationResourceRegistrarTest extends TestCase
{
    public function testDiscoversDomainsAndLocales(): void
    {
        $translator = TranslatorFactory::create(&#039;en&#039;);
        $registrar = new TranslationResourceRegistrar($translator);

        $registrar-&gt;registerDirectory(__DIR__ . &#039;/../fixtures/translations&#039;);

        sort($domains = $registrar-&gt;getRegisteredDomains());
        sort($locales = $registrar-&gt;getRegisteredLocales());

        $this-&gt;assertSame([&#039;errors+intl-icu&#039;, &#039;messages+intl-icu&#039;], $domains);
        $this-&gt;assertSame([&#039;en&#039;, &#039;es&#039;], $locales);
    }

    public function testLaterDirectoriesOverridePreviousOnesForTheSameKey(): void
    {
        $translator = TranslatorFactory::create(&#039;en&#039;);
        $registrar = new TranslationResourceRegistrar($translator);

        $registrar-&gt;registerDirectories([
            __DIR__ . &#039;/../fixtures/translations&#039;,
            __DIR__ . &#039;/../fixtures/translations-override&#039;,
        ]);

        $this-&gt;assertSame(
            &#039;Overridden message.&#039;,
            $translator-&gt;trans(&#039;welcome&#039;, [], &#039;messages+intl-icu&#039;),
        );
    }
}
```

## Testing Custom Resource Providers

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\Contract\TranslationResourceProviderInterface;

final class PackageTranslationResourceProviderTest extends TestCase
{
    public function testReturnsExpectedDirectories(): void
    {
        $provider = new PackageTranslationResourceProvider();

        $directories = iterator_to_array($provider-&gt;getDirectories());

        $this-&gt;assertNotEmpty($directories);
        foreach ($directories as $directory) {
            $this-&gt;assertDirectoryExists($directory);
        }
    }
}
```

## Testing ICU Edge Cases

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\TranslatableMessage;

final class IcuFormattingTest extends TestCase
{
    public function testMissingOtherCategoryFallsBackToRawMessage(): void
    {
        $message = new TranslatableMessage(
            &#039;{gender, select, male{He} female{She}}&#039;,
            [&#039;gender&#039; =&gt; &#039;unknown&#039;],
        );

        $this-&gt;assertSame(
            &#039;{gender, select, male{He} female{She}}&#039;,
            (string) $message,
        );
    }

    public function testUnmatchedBraceFallsBackToRawMessage(): void
    {
        $message = new TranslatableMessage(&#039;Hello {name&#039;, [&#039;name&#039; =&gt; &#039;John&#039;]);

        $this-&gt;assertSame(&#039;Hello {name&#039;, (string) $message);
    }

    public function testMissingParameterLeavesPlaceholderUnexpanded(): void
    {
        $message = new TranslatableMessage(&#039;{count} items&#039;);

        $this-&gt;assertSame(&#039;{count} items&#039;, (string) $message);
    }
}
```

---

Remember:

- Test both the untranslated (`getMessage()`) and translated (`trans()`)
  paths.
- Prefer `createStub()` over `createMock()` for a `TranslatorInterface`
  double unless you&#039;re actually asserting call counts.
- Use real fixture files and a real `Translator` for integration-level
  coverage of ICU formatting and resource discovery — it&#039;s fast and
  catches naming-convention mistakes that a stub translator can&#039;t.
- Always test with the `other` case missing and with missing parameters:
  both fail *silently*, falling back to the raw message, not by throwing.




---

## Support Project

Essential PHP Utilities

# Essential PHP Utilities

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/support/main)
![CI Workflow](https://github.com/derafu/support/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/support)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/support)
![Total Downloads](https://poser.pugx.org/derafu/support/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/support/d/monthly)

A collection of essential PHP utility classes that provide common functionality for string manipulation, array handling, file operations, date management, and more.

## Features

{.list-unstyled}
- 🔡 String manipulation utilities.
- 📚 Array handling helpers.
- 📅 Date and time management.
- 📂 File system operations.
- 📝 CSV file handling.
- 🔧 Object manipulation tools.
- 🐛 Debugging utilities.
- 🏭 Object factory and hydration.
- 🔄 Data serialization helpers.
- 🧪 Comprehensive test coverage.

## Why Derafu\Support?

This package focuses on solving specific business and data processing needs that are often overlooked by standard PHP utilities:

- **Business Date Handling**: Working days calculation, fiscal period management, and date ranges that understand holidays and weekends.
- **Robust CSV Processing**: Consistent handling of different encodings, separators, and quote styles across systems.
- **Practical Data Transformations**: Convert between different data structures (trees, tables, lists) while preserving data integrity.
- **Real-world File Operations**: Safe file handling with proper error management and automatic MIME type detection.

If your application deals with business dates, data processing, or file management, these utilities can save you from reinventing common solutions.

## Installation

Install via Composer:

```bash
composer require derafu/support
```

## Usage Examples

### String Manipulation (Str)

```php
use Derafu\Support\Str;

// Generate UUID.
$uuid = Str::uuid4();

// Replace placeholders.
$result = Str::format(&#039;Hello {{name}}!&#039;, [&#039;name&#039; =&gt; &#039;John&#039;]);

// Normalize strings for URLs.
$slug = Str::slug(&#039;Hello World!&#039;); // &quot;hello-world&quot;.
```

### Array Handling (Arr)

```php
use Derafu\Support\Arr;

// Auto-cast array values.
$result = Arr::cast($array);

// Convert array to tree structure.
$tree = Arr::toTree($items, &#039;parent_id&#039;, &#039;children&#039;);
```

### Date Management (Date)

```php
use Derafu\Support\Date;

// Add working days.
$newDate = Date::addWorkingDays(&#039;2024-01-15&#039;, 5, $holidays);

// Format date in Spanish.
$formatted = Date::formatSpanish(&#039;2024-01-15&#039;); // &quot;Lunes, 15 de enero del 2024&quot;.

// Calculate periods
$nextPeriod = Date::nextPeriod(202401); // 202402.
```

### File Operations (File)

```php
use Derafu\Support\File;

// Get file MIME type.
$mime = File::mimetype(&#039;document.pdf&#039;);

// Compress directory.
File::compress(&#039;/path/to/dir&#039;);

// Send file through browser.
File::send(&#039;document.pdf&#039;);
```

### CSV Handling (Csv)

```php
use Derafu\Support\Csv;

// Read CSV file.
$data = Csv::read(&#039;file.csv&#039;, &#039;;&#039;);

// Generate CSV content.
$csvString = Csv::generate($data);

// Send CSV as download.
Csv::send($data, &#039;export.csv&#039;);
```

### Object Manipulation (Obj)

```php
use Derafu\Support\Obj;

// Fill object properties.
$object = Obj::fill($instance, $data);

// Get public properties.
$properties = Obj::getPublicProperties($instance);
```

### Object Factory and Hydration

```php
use Derafu\Support\Factory;
use Derafu\Support\Hydrator;

// Create and hydrate objects.
$instance = Factory::create($data, MyClass::class);

// Hydrate existing instance.
$hydrated = Hydrator::hydrate($instance, $data);
```

### Debug Utilities

```php
use Derafu\Support\Debug;

// Inspect variable.
$info = Debug::inspect($var, &#039;myVar&#039;);

// Print debug information.
Debug::print($var);
```




---

## Backbone Dispatcher Project

Safely Invoke Any Backbone Operation, From Anywhere

# Safely Invoke Any Backbone Operation, From Anywhere

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/backbone-dispatcher/main)
![CI Workflow](https://github.com/derafu/backbone-dispatcher/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/backbone-dispatcher)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/backbone-dispatcher)
![Total Downloads](https://poser.pugx.org/derafu/backbone-dispatcher/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/backbone-dispatcher/d/monthly)

Generic invocation and introspection for [Backbone](https://www.derafu.dev/docs/core/backbone) services: turn any worker&#039;s operation into something safely callable by id, with plain array parameters, from outside PHP&#039;s own type system.

## Why

A [Backbone](https://www.derafu.dev/docs/core/backbone) worker&#039;s public methods are ordinary, statically-typed PHP methods — great for an in-process PHP caller, useless for anything else. Backbone Dispatcher adds a generic invocation layer on top: it turns a string operation id (`&quot;package.component.worker::operation&quot;`) plus a plain associative array of parameters into a real, reflection-resolved, type-coerced method call on the right worker, and turns whatever comes back — a return value **or** an uncaught exception — into a plain, serializable shape.

That is what makes it possible for a caller with no concept of PHP reflection, PHP&#039;s type system, or PHP exceptions — Python code across a [`phpy`](https://www.derafu.dev/docs/core/backbone-bridge-python) boundary, an HTTP client through [Backbone API](https://www.derafu.dev/docs/core/backbone-api), or anything else — to invoke any Backbone operation and get back a predictable, serializable answer either way.

## Installation

```bash
composer require derafu/backbone-dispatcher
```

## The Three-Tier Dispatcher

Each tier wraps the one below it and adds exactly one thing — none of them re-implement the others&#039; job.

### `DirectDispatcherInterface`

```php
public function dispatch(
    string $package, string $component, string $worker,
    string $operation, array $params = []
): mixed;
```

Resolves the worker from the package registry, resolves and validates the parameters against the operation&#039;s real method signature, and calls it. Returns exactly what the operation returned, as a real PHP value — no wrapping. Any `Throwable` propagates unaltered. Use this directly only when you&#039;re a PHP-only caller happy to get real domain objects back and to handle exceptions yourself.

### `TypedDispatcherInterface`

```php
public function dispatch(OperationRequestInterface $request): OperationResultInterface;
```

Adapts `DirectDispatcher` to the `OperationRequest` → `OperationResult` shape. Still does **not** catch exceptions — a thrown `Throwable` propagates unaltered, and a successful call always has `isSuccess() === true` (there is no &quot;failure&quot; `OperationResult` coming out of this tier, only a thrown exception). It doesn&#039;t serialize the return value either: a domain object comes back as the real object.

### `SafeDispatcherInterface`

```php
public function dispatch(OperationRequestInterface $request): OperationResultInterface;
```

Same signature as `TypedDispatcher`, but **never throws**: any `Throwable` becomes a failure `OperationResult` carrying a `ProblemDetail`. It&#039;s also the only tier that **serializes** the success value before returning it — because it&#039;s the tier meant to actually cross a process/language boundary, where an uncaught PHP exception is a dead end.

```php
$inspector = new Inspector();

$directDispatcher = new DirectDispatcher(
    $registry,
    $inspector,
    new Resolver(
        $inspector,
        new Caster(new ObjectFactoryRegistry(fallback: new FromArrayDeserializer())),
        new Validator(),
    ),
    // 4th argument, OperationPolicyInterface, defaults to AllowAllOperationPolicy — see below.
);

$safeDispatcher = new SafeDispatcher(
    new TypedDispatcher($directDispatcher),
    new Serializer(),
    environment: &#039;prod&#039;,
    debug: false,
);
```

There is no bundled DI wiring for this chain — wire it however your application already wires services (a `config/services.yaml`, a PHP-DI container, etc.), using the composition above as the reference.

`DirectDispatcher` invokes the resolved operation with a native named-argument call (`$worker-&gt;$operation(...$args)`) rather than a generic invoker library: `Resolver` already produces `$args` as a plain, name-keyed, fully-cast array, so nothing is left for an invoker to resolve that hasn&#039;t already been resolved.

## Controlling Which Operations Can Be Dispatched: `OperationPolicyInterface`

By default, any public method of a worker is dispatchable — the historical
behavior, kept as `AllowAllOperationPolicy`, `DirectDispatcher`&#039;s 4th
constructor argument&#039;s default value. **This default is convenient, not
recommended**: &quot;any public method&quot; includes infrastructure methods a
worker gets from `JobsAwareTrait`/`HandlersAwareTrait`/`OptionsAwareTrait`
(`getJobs()`, `setOptions()`, ...) and from `ServiceInterface` itself
(`getId()`, `getName()`, `getDescription()`) — none of it business logic,
all of it just as dispatchable as a real operation while this policy is
active. `TaggedOperationPolicy` is the recommended choice for
anything reached from outside PHP: it only allows what is explicitly
tagged `#[Operation]`, which is the one real signal for &quot;this is meant
to be exposed&quot; — reflection alone cannot tell &quot;a public method that
happens to exist&quot; apart from &quot;an operation&quot; (see `AllowAllOperationPolicy`&#039;s
own docblock). Two other policies ship with the package, and swapping the
one wired into `DirectDispatcher` is the only change needed — nothing else
in the three-tier chain knows a policy exists:

```php
use Derafu\BackboneDispatcher\Service\Policy\TaggedOperationPolicy;
use Derafu\BackboneDispatcher\Service\Policy\AllowListOperationPolicy;

// Only methods tagged with derafu/backbone&#039;s #[Operation] attribute.
$policy = new TaggedOperationPolicy($registry, $inspector);

// Only operations matching one of these ids. fnmatch() wildcards allowed,
// e.g. &quot;billing.invoice.builder::*&quot; for every operation of that worker,
// or &quot;billing.*&quot; for an entire package.
$policy = new AllowListOperationPolicy([
    &#039;billing.invoice.builder::build&#039;,
    &#039;billing.invoice.builder::cancel&#039;,
]);

$directDispatcher = new DirectDispatcher(
    $registry,
    $inspector,
    $resolver,
    $policy,
);
```

Two guards run in `DirectDispatcher::dispatch()`, in this order, before an
operation is ever resolved or invoked — so every tier built on top of it
enforces both without knowing they exist:

- Does the operation exist at all, as a public method declared on the
  worker? Independent of which policy is configured — throws
  `OperationNotFoundException` otherwise.
- Does the active `OperationPolicyInterface` allow it? Throws
  `OperationNotAllowedException` otherwise.

`Explorer` accepts the same `OperationPolicyInterface` as an optional
constructor argument, so documentation never advertises an operation the
dispatcher would then reject — without one, it lists everything, matching
`AllowAllOperationPolicy`. With one, the pruning goes all the way up the
tree: `getOperations()` drops operations it rejects, and
`getWorkers()`/`getComponents()`/`getPackages()` drop any branch left with
zero visible operations underneath — a worker nobody may call does not
show up at all, not even as an empty entry.

Implementing `OperationPolicyInterface` yourself (a single `isAllowed()`
method) covers any other rule — e.g. combining policies, or checking
against the current user&#039;s permissions.

## Dispatching an Operation

```php
use Derafu\BackboneDispatcher\ValueObject\OperationRequest;

$request = OperationRequest::fromId(
    &#039;billing.invoice.builder::build&#039;,
    [&#039;number&#039; =&gt; &#039;F-001&#039;, &#039;amount&#039; =&gt; 15000],
);

$result = $safeDispatcher-&gt;dispatch($request);

if ($result-&gt;isSuccess()) {
    $invoice = $result-&gt;getValue(); // already serialized: array/scalar, never a raw PHP object.
} else {
    $problem = $result-&gt;getProblem(); // RFC 7807-shaped ProblemDetail.
}
```

`OperationRequest::fromId()` parses `&quot;package.component.worker::operation&quot;` — an id with the wrong shape (missing `::`, wrong number of `.`-segments, any empty piece) throws `InvalidOperationIdException`. `::` (not a single `:`) on purpose: `derafu/backbone` already uses a single `:` for its own `.job:name`/`.handler:name`/`.strategy:name` ids — this identifier is a dispatcher-only concept, unrelated to that family, and `::` keeps it visually distinct.

## Handling Failure: `ProblemDetail`

An RFC 7807-shaped, transport-agnostic problem description — `type`/`title`/`detail`/`instance` at the top level, everything else namespaced under `extensions`:

```php
$problem-&gt;getDetail();               // The exception&#039;s own message.
$problem-&gt;getInstance();              // The failed request&#039;s id: &quot;billing.invoice.builder::build&quot;.
$problem-&gt;getThrowable()-&gt;getClass(); // e.g. &quot;RuntimeException&quot; — only exposed when debug is true.
$problem-&gt;toArray();
```

```php
[
    &#039;type&#039; =&gt; &#039;about:blank&#039;,
    &#039;title&#039; =&gt; &#039;RuntimeException&#039;,
    &#039;detail&#039; =&gt; &#039;Something went wrong while running the operation.&#039;,
    &#039;instance&#039; =&gt; &#039;billing.invoice.builder::build&#039;,
    &#039;extensions&#039; =&gt; [
        &#039;timestamp&#039; =&gt; 1755500000.123456, // Unix epoch, seconds — same instant and representation as `ExecutionMetadata::getTimestamp()`.
        &#039;data_type&#039; =&gt; null,              // Always null on a failure — there is no value to describe.
        &#039;environment&#039; =&gt; &#039;prod&#039;,
        &#039;debug&#039; =&gt; false,
        &#039;context&#039; =&gt; [],
        &#039;throwable&#039; =&gt; null, // hidden outside debug mode.
    ],
]
```

The wrapped `SafeThrowable` actively scrubs sensitive data before it ever gets this far: trace frame `args` are stripped, and absolute file paths are rewritten relative to a project directory (`&quot;project_dir:src/Foo.php&quot;` instead of `/home/user/project/src/Foo.php`) — neither call arguments nor local filesystem layout leak to whatever is on the other side of the boundary.

## Execution Metadata: `ExecutionMetadata`

Every `OperationResultInterface` — success or failure alike — also carries `getMetadata(): ExecutionMetadataInterface`, statistics about the dispatch that produced it. A consumer decides whether, and how, to use these; this package only collects them:

```php
$metadata = $result-&gt;getMetadata();

$metadata-&gt;getStartedAt();       // &quot;2026-01-20T10:00:00+00:00&quot;
$metadata-&gt;getFinishedAt();
$metadata-&gt;getTimestamp();       // The same instant as getFinishedAt(), as a Unix epoch float — flexible to reuse (sorting, arithmetic), unlike the DATE_ATOM string.
$metadata-&gt;getRealTime();        // Seconds, wall-clock — the &quot;real&quot; of `time`.
$metadata-&gt;getUserTime();        // Seconds of CPU in user mode — the &quot;user&quot; of `time`.
$metadata-&gt;getSystemTime();      // Seconds of CPU in kernel mode — the &quot;sys&quot; of `time`.
$metadata-&gt;getMemoryUsed();      // Bytes, a delta — can be negative if the GC freed more than this dispatch allocated.
$metadata-&gt;getPeakMemory();      // Bytes, the whole process&#039;s peak up to this point — stable against that same GC noise.
$metadata-&gt;getPid();
$metadata-&gt;getLoadAverage1Min(); // Plus 5/15-minute variants — tells &quot;slow because of this operation&quot; apart from &quot;slow because the system itself was saturated.&quot;
```

Assumes Linux/macOS: built on `getrusage()` and `sys_getloadavg()`, neither of which exists on Windows — no Windows support is offered.

`TypedDispatcher` and `SafeDispatcher` each measure their own scope independently, never reusing the other&#039;s numbers: `TypedDispatcher`&#039;s metadata covers only resolving parameters and invoking the worker (via `DirectDispatcher`); `SafeDispatcher`&#039;s covers that plus serializing the result on success, or everything up to the moment it caught the exception on failure. `ProblemDetailInterface::getTimestamp()` (above) does reuse `ExecutionMetadata`&#039;s own reading rather than taking a second, independent one, so both always agree on the exact same instant for the same dispatch.

## What Kind of Value Was Returned: `getDataType()`

Alongside `getValue()`, a successful `OperationResultInterface` also carries `getDataType(): ?string` — the type of the value *before* `SafeDispatcher` serializes it (`get_class()` for an object, `gettype()` for a scalar/array), e.g. `&quot;App\Entity\Invoice&quot;` or `&quot;integer&quot;`. `null` on a failure, since there is nothing to describe.

```php
$result-&gt;getDataType(); // e.g. &quot;App\Entity\Invoice&quot; — even though getValue() is already a plain array.
```

This exists because the information is only available at the exact moment of dispatch: once `SafeDispatcher` has serialized a domain object into a plain array, its original class is gone for good — a transport built on top (like [Backbone API](https://www.derafu.dev/docs/core/backbone-api) or [Backbone Console](https://www.derafu.dev/docs/core/backbone-console)) cannot recover it afterward, so `TypedDispatcher` captures it up front and `SafeDispatcher` carries it through unchanged.

## Turning Array Data Into Real Objects

A parameter typed as a class or interface doesn&#039;t have to arrive pre-built — a plain array (or string, for things like base64-encoded certificates) is deserialized on the way in:

- Any class exposing a static `fromArray(array $data): self` works with **zero registration**, via `FromArrayDeserializer` (the conventional fallback).
- A specific class can instead get its own `DeserializerInterface` registered on `ObjectFactoryRegistry`, which takes priority over the fallback — useful when construction isn&#039;t a plain `fromArray()` (loading a certificate from either raw data or a key pair, for example).
- Union-typed parameters (`A|B`) try each candidate class in order.

```php
$resolver = new Resolver(
    new Inspector(),
    new Caster(new ObjectFactoryRegistry(
        deserializers: [Caf::class =&gt; new CafDeserializer()], // explicit, takes priority.
        fallback: new FromArrayDeserializer(),                // used for everything else.
    )),
    new Validator(),
);
```

On the way out, `Serializer` mirrors this: arrays recurse, `JsonSerializable` objects recurse into `jsonSerialize()`, objects with a `toArray()` recurse into that — so a nested domain object graph comes back from `SafeDispatcher` as plain, nested arrays.

## Discovery: `Explorer` and `Inspector`

`Explorer` walks the package registry (packages → components → workers → operations) — no `_links`/HATEOAS shaping, that&#039;s left to transports like [Backbone API](https://www.derafu.dev/docs/core/backbone-api). `Inspector` is the *only* class in the package that imports anything from `Reflection*`: every other collaborator that needs to know something about a class or a method — `Resolver`, `Explorer`, the operation policies — asks `InspectorInterface` for it instead of reflecting directly.

`getPackage()`, `getComponent()` and `getWorker()` include the real `name`/`summary`/`description` of each. `name` is read straight off the instance via `ServiceInterface::getName()` (from `derafu/backbone`, backed by its `#[Package]`/`#[Component]`/`#[Worker]` attributes) — a routing slug, never empty by omission, since it is what builds the discovery id. `summary`/`description` both come from the class&#039; own PHPDoc: `summary` is always the PHPDoc&#039;s, verbatim (there is no `#[Worker(summary: ...)]` to prefer instead), while `description` prefers the same attribute&#039;s `description` argument first, falling back to the PHPDoc (its own description, then its summary) only when that argument was left unset — the same &quot;explicit value, else PHPDoc&quot; precedence `Inspector::getPublicMethods()` already gives an operation&#039;s `#[Operation]` attribute over its method&#039;s PHPDoc (see below), just one level up: a `#[Worker(...)]`-style attribute&#039;s `description` is just as easy to leave unset as an `#[Operation]`&#039;s, and the class right below it almost always already documents itself. `getOperation(string $package, string $component, string $worker, string $operation)` looks up exactly one operation by name — unlike `getOperations()` (a listing, which silently omits what a policy rejects), a direct lookup throws: `OperationNotFoundException` if it does not exist, `OperationNotAllowedException` if the active policy rejects it.

Worth knowing: ids are dot-separated for the hierarchy (`&quot;package&quot;`, `&quot;package.component&quot;`, `&quot;package.component.worker&quot;`) and `::`-separated for an operation (`&quot;package.component.worker::operation&quot;`) — the same shape `OperationRequest`&#039;s invocation id uses. A discovery id with an operation and an invocation id are the same string; `::` never appears anywhere except right before the operation.

`describe(?string $id = null)` is a single entry point over the rest of `ExplorerInterface`: it resolves an id to whichever of `getPackage()`/`getComponent()`/`getWorker()`/`getOperation()` matches, or, for `null` (there is no single &quot;root&quot; resource to look up), `{&#039;summary&#039; =&gt; ..., &#039;description&#039; =&gt; ..., &#039;packages&#039; =&gt; getPackages()}`:

```php
$explorer-&gt;describe();                                              // {&#039;summary&#039; =&gt; ..., &#039;description&#039; =&gt; ..., &#039;packages&#039; =&gt; getPackages()}
$explorer-&gt;describe(&#039;billing&#039;);                                     // getPackage(&#039;billing&#039;)
$explorer-&gt;describe(&#039;billing.invoice&#039;);                              // getComponent(...)
$explorer-&gt;describe(&#039;billing.invoice.builder&#039;);                      // getWorker(...)
$explorer-&gt;describe(&#039;billing.invoice.builder::createDraft&#039;);         // getOperation(...)
```

`summary`/`description` are the package registry&#039;s own PHPDoc — kept alongside `packages` rather than replacing it, the same way every other level keeps its own `summary`/`description` alongside its children (`components`/`workers`/`operations`). `tree(null)` nests the same way, just with each package in `packages` deeply nested instead of the shallow `getPackage()` shape `describe(null)` holds.

More than 3 dot-separated segments before an operation, an empty segment, or an empty operation after `::` all throw `InvalidDiscoveryIdException` — a different exception from `InvalidOperationIdException` on purpose, even though the shape is identical once an operation is present: `describe()` also accepts a *partial* id (just a package, or a package and component) to browse with, something `OperationRequest::fromId()` (always a complete, 4-part invocation) never allows.

`tree(?string $id = null)` resolves the same id the same way, but nests every level&#039;s children instead of stopping at that one node — a worker comes back with its own `operations` nested right in:

```php
$explorer-&gt;tree(&#039;billing.invoice.builder&#039;);
// [
//     &#039;id&#039; =&gt; &#039;billing.invoice.builder&#039;,
//     &#039;name&#039; =&gt; &#039;builder&#039;,
//     &#039;summary&#039; =&gt; &#039;Invoice Builder.&#039;,
//     &#039;description&#039; =&gt; &#039;...&#039;,
//     &#039;operations&#039; =&gt; [
//         [&#039;id&#039; =&gt; &#039;billing.invoice.builder::build&#039;, &#039;name&#039; =&gt; &#039;build&#039;, /* ... */],
//         [&#039;id&#039; =&gt; &#039;billing.invoice.builder::cancel&#039;, &#039;name&#039; =&gt; &#039;cancel&#039;, /* ... */],
//     ],
// ]
```

A package nests its `components`, each of those its own `workers`, each of those its own `operations` — operations are always the leaves, nothing nests under them, so an operation id resolves exactly like `describe()`. The same policy-based pruning as the rest of `Explorer` applies at every level: a worker left with zero visible operations does not appear in its component&#039;s `workers`, and so on up to the root.

For `null`, same as `describe(null)`: `{&#039;summary&#039; =&gt; ..., &#039;description&#039; =&gt; ..., &#039;packages&#039; =&gt; [...]}`, except each package in `packages` is deeply nested here instead of the shallow `getPackage()` shape `describe(null)` holds:

```php
$explorer-&gt;tree();
// [
//     &#039;summary&#039; =&gt; &#039;...&#039;,       // the package registry&#039;s own PHPDoc, first sentence.
//     &#039;description&#039; =&gt; &#039;...&#039;,   // the rest of the same PHPDoc.
//     &#039;packages&#039; =&gt; [
//         [&#039;id&#039; =&gt; &#039;billing&#039;, &#039;name&#039; =&gt; &#039;billing&#039;, &#039;summary&#039; =&gt; &#039;...&#039;, &#039;description&#039; =&gt; &#039;...&#039;, &#039;components&#039; =&gt; [/* ... */]],
//     ],
// ]
```

Every other level in this tree is `{id, name, summary, description, &lt;children&gt;}`; the root is the same shape minus `id`/`name` (a registry is not a `ServiceInterface` with an identity, unlike a package/component/worker) — `summary`/`description` still have the same honest source as everywhere else, the registry class&#039; own PHPDoc, so there is no reason for them to be missing just because `id`/`name` do not apply.

&gt; [!TIP] Operation ≠ Job
&gt;
&gt; An &quot;operation&quot; here is simply a public method of a worker, discovered via reflection. It has nothing to do with Backbone&#039;s own formally-registered `JobInterface`/`#[Job]` concept — a worker&#039;s operation may use zero, one, or several real jobs internally, and the dispatcher neither knows nor cares.

### Documenting an Operation: `#[Operation]`

`derafu/backbone`&#039;s [`#[Operation]` attribute](/docs/core/backbone/operations) (see [Controlling Which Operations Can Be Dispatched](#controlling-which-operations-can-be-dispatched-operationpolicyinterface) above) can also carry documentation reflection/PHPDoc cannot produce on its own:

```php
use Derafu\Backbone\Attribute\Operation;

#[Operation(
    name: &#039;Create a draft invoice&#039;,       // overrides the PHPDoc summary, if given.
    description: &#039;Builds a draft from the given data, without emitting it.&#039;, // overrides the PHPDoc description.
    parameters: [
        &#039;number&#039; =&gt; [&#039;example&#039; =&gt; &#039;F-001&#039;],
        &#039;amount&#039; =&gt; [&#039;example&#039; =&gt; 15000, &#039;description&#039; =&gt; &#039;Amount in the smallest currency unit.&#039;],
    ],
    results: [
        &#039;success&#039; =&gt; [&#039;description&#039; =&gt; &#039;The created draft.&#039;, &#039;example&#039; =&gt; [&#039;id&#039; =&gt; &#039;DR-001&#039;]],
        MissingParameterException::class =&gt; [&#039;description&#039; =&gt; &#039;A required parameter was not provided.&#039;],
    ],
)]
public function build(string $number, int $amount): array
```

`Inspector::getPublicMethods()` merges whatever is given here on top of what reflection and PHPDoc already produced, before any policy or documentation consumer ever sees it — see [Operations](https://www.derafu.dev/docs/core/backbone/operations) for the full attribute reference (every property, what each one means, and why it&#039;s not a service-defining attribute like `#[Job]`/`#[Handler]`/`#[Strategy]`).

### Caching Reflection: `CachedInspector`

`getClassDoc()` and `getPublicMethods()` parse PHPDoc for every method of a class — the same result every time for a given class, until the next deploy. `CachedInspector` decorates any `InspectorInterface` with a PSR-6 `CacheItemPoolInterface`, caching exactly those two calls:

```php
use Derafu\BackboneDispatcher\Service\Reflection\CachedInspector;
use Derafu\Cache\Adapter\PhpFilesCache;

$inspector = new CachedInspector(
    new Inspector(),
    new PhpFilesCache(&#039;backbone_dispatcher&#039;, &#039;/var/cache/backbone_dispatcher&#039;), // any PSR-6 Psr\Cache\CacheItemPoolInterface.
    ttl: 3600,
);
```

There is no default pool: `$cache` must always be given explicitly — `derafu/cache`&#039;s `PhpFilesCache`/`FilesystemCache` (shown above), any other PSR-6 implementation (Redis, APCu, an in-memory one for tests), or `derafu/cache`&#039;s `LocalCacheFactory` if the backend is itself a runtime choice. If caching isn&#039;t wanted at all, the simplest option is not using `CachedInspector` — inject the plain `Inspector` wherever `InspectorInterface` is expected instead, no flag needed anywhere.

Set `ttl: 0` to skip the pool on every call without swapping which `InspectorInterface` is injected — useful when the choice comes from runtime configuration rather than wiring. There is no invalidation logic inside the package on purpose: whether an entry outlives a deploy is entirely up to which pool gets injected — a file-based pool under the system temp directory is exactly as ephemeral as the process using it; a shared backend (Redis, APCu) is the caller&#039;s to flush, or not, as part of their own deploy.

`isOperation()`, `hasOperationAttribute()` and `getOperationParameters()` are deliberately **not** cached, in `CachedInspector` or otherwise: they never parse PHPDoc, they run on every single dispatch regardless of which tier or policy is used, and reading them through a cache backend could easily cost more than the reflection they would replace.

## Never Failing While Exploring: `SafeExplorer`

`Explorer`/`ExplorerInterface` throw — `OperationNotFoundException`, `OperationNotAllowedException`, `InvalidDiscoveryIdException`, or any of `derafu/backbone`&#039;s own &quot;not found&quot; exceptions when a given package/component/worker doesn&#039;t exist. `SafeExplorer` wraps any `ExplorerInterface` the same way `SafeDispatcher` wraps `TypedDispatcher`: no `Throwable` ever crosses back to the caller — every one of `ExplorerInterface`&#039;s 10 public methods instead returns a `DiscoveryResultInterface`:

```php
use Derafu\BackboneDispatcher\Service\Discovery\SafeExplorer;

$safeExplorer = new SafeExplorer($explorer, environment: &#039;prod&#039;, debug: false);

$result = $safeExplorer-&gt;tree(&#039;billing.invoice.builder::build&#039;);

if ($result-&gt;isSuccess()) {
    $operation = $result-&gt;getValue();
} else {
    $problem = $result-&gt;getProblem(); // Same RFC 7807-shaped ProblemDetail as SafeDispatcher&#039;s.
}
```

`DiscoveryResultInterface` has the same `isSuccess()`/`getValue()`/`getProblem()` shape as `OperationResultInterface`, but is a deliberately separate type: one is about dispatching an operation, the other about exploring the package tree, and keeping them independent means either one is free to grow its own data later without dragging the other along. It has no `getMetadata()`: `SafeExplorer` does not measure `ExecutionMetadata`.

## Exceptions

```
InvalidOperationIdException     — malformed &quot;package.component.worker::operation&quot; id.
InvalidDiscoveryIdException     — malformed discovery id passed to Explorer::describe().

ResolverException
├── InvalidParameterTypeException — a scalar parameter has the wrong native type.
└── MissingParameterException     — a required parameter is absent.

ObjectFactoryException
├── ClassNotFoundException            — fromArray() target class doesn&#039;t exist.
├── FromArrayMethodNotFoundException  — target class has no static fromArray().
└── NoDeserializerFoundException      — no registered deserializer, and the fallback failed too.

OperationNotFoundException  — the operation does not exist as a public method of the worker.
OperationNotAllowedException — the operation exists, but the active OperationPolicyInterface rejects it.
```

Every exception exposes a semantic static factory (`InvalidOperationIdException::forId()`, `MissingParameterException::forParameter()`, etc.) instead of a public constructor, and is translatable via [`derafu/translation`](https://www.derafu.dev/docs/core/translation).

## Requirements

PHP 8.5+. Depends on [`derafu/backbone`](https://www.derafu.dev/docs/core/backbone), [`derafu/cache`](https://www.derafu.dev/docs/core/cache) and [`derafu/translation`](https://www.derafu.dev/docs/core/translation).




---

## Backbone API Project

HTTP API With Zero Per-Operation Controllers

# HTTP API With Zero Per-Operation Controllers

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/backbone-api/main)
![CI Workflow](https://github.com/derafu/backbone-api/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/backbone-api)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/backbone-api)
![Total Downloads](https://poser.pugx.org/derafu/backbone-api/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/backbone-api/d/monthly)

Turns every operation of every worker registered in a [Backbone](https://www.derafu.dev/docs/core/backbone) package registry into an HTTP endpoint automatically — no per-operation controller code, self-documenting as HATEOAS resources and an OpenAPI 3.1 spec.

## Why

Instead of writing `POST /invoices/build` → `InvoiceController::build()` by hand for each operation, you write one generic route pointing at one controller, and the URL path itself tells the package which worker method to call. The actual resolution and invocation is delegated entirely to [`derafu/backbone-dispatcher`](https://www.derafu.dev/docs/core/backbone-dispatcher), which is transport-agnostic — this package only deals with the HTTP-specific concerns: parsing the route, listing packages/components/workers as browsable resources, serving OpenAPI documentation, and extracting parameters from the request body.

## Installation

```bash
composer require derafu/backbone-api
```

There is no bundled route table or DI wiring — wiring `Dispatcher` and a controller into your framework&#039;s routes and container is left to your application.

## Routing Convention

`Router::parse()` resolves an incoming request into up to four path segments:

```
/[api/]:package/:component/:worker/:operation
```

The `/api` prefix is optional and stripped if present — `/api/billing/document` and `/billing/document` resolve identically. A path with more than 4 segments throws `InvalidRouteException`. Trailing segments are simply omitted rather than required, which is what drives the dispatch behavior below.

Two special one-segment paths are intercepted before being treated as a package name:

- Empty path or `index` → the root HATEOAS listing.
- `openapi-docs.json` → the generated OpenAPI document.

```php
public function dispatch(ServerRequestInterface $request): mixed
{
    $route = $this-&gt;router-&gt;parse($request);

    return match (true) {
        $route-&gt;getId() === &#039;index&#039; || $route-&gt;getId() === null =&gt; $this-&gt;handleRoot(),
        $route-&gt;getId() === &#039;openapi-docs.json&#039; =&gt; $this-&gt;documenter-&gt;document(),
        $route-&gt;getComponent() === null =&gt; $this-&gt;handlePackage($route-&gt;getPackage()),
        $route-&gt;getWorker() === null =&gt; $this-&gt;handleComponent($route-&gt;getPackage(), $route-&gt;getComponent()),
        $route-&gt;getOperation() === null =&gt; $this-&gt;handleWorker(...),
        default =&gt; $this-&gt;handleOperation($request, ...),
    };
}
```

So `GET /api/billing` lists `billing`&#039;s components, `GET /api/billing/document` lists that component&#039;s workers, and only a full 4-segment path actually invokes an operation.

## Invoking an Operation

Every operation is invoked with the parameters read from the JSON request body&#039;s `parameters` key — nothing else (no query string, no path params beyond routing):

```
POST /api/billing/invoice/builder/build
Content-Type: application/json
Accept: application/json

{ &quot;parameters&quot;: { &quot;number&quot;: &quot;F-001&quot;, &quot;amount&quot;: 15000 } }
```

```php
$requestContent = json_decode($request-&gt;getBody()-&gt;getContents(), true);
$params = $requestContent[&#039;parameters&#039;] ?? [];

$operationRequest = new OperationRequest($package, $component, $worker, $operation, $params);

return $this-&gt;dispatcher-&gt;dispatch($operationRequest); // SafeDispatcherInterface — never throws.
```

`handleOperation()` returns the `OperationResultInterface` itself, unwrapped — `AbstractController` (below) is the single place that turns it into the actual response, since it is also the place that already builds every other response shape this package produces.

## Response Shape

A successful, JSON-wanting request (`Accept: application/json` or `*/*`) gets wrapped in a small envelope — for an operation result, `meta.timestamp`/`meta.data_type` come straight from `OperationResultInterface::getMetadata()-&gt;getTimestamp()`/`::getDataType()` (see [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#what-kind-of-value-was-returned-getdatatype)), not recomputed from the already-serialized value:

```json
{
    &quot;meta&quot;: { &quot;timestamp&quot;: 1755500000.123456, &quot;data_type&quot;: &quot;integer&quot; },
    &quot;data&quot;: 12
}
```

Two results bypass the envelope entirely: a PSR-7 `ResponseInterface` returned by the underlying dispatch (passed through unchanged), and the OpenAPI document itself (detected by its own `openapi` key). If the request doesn&#039;t ask for JSON, the raw result is returned unwrapped for the framework layer to render however it sees fit.

### Failure: a Real Status Code, Never a Silent 200

`SafeDispatcherInterface` never throws, so `AbstractController` is also the one place that decides what a failed operation actually looks like to the client — a real PSR-7 response (built via an injected `Psr\Http\Message\ResponseFactoryInterface`, so this package stays implementation-agnostic about which PSR-17 factory produces it) carrying:

- The resolved HTTP status, from `HttpStatusResolver` (below) — never a default `200` masking a real failure.
- The exact same `ProblemDetailInterface::toArray()` body [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#handling-failure-problemdetail) and [Backbone Console](https://www.derafu.dev/docs/core/backbone-console) already produce — the same failure looks the same across every transport.

```php
$problem = $result-&gt;getProblem();
$status = $this-&gt;httpStatusResolver-&gt;resolve($problem-&gt;getThrowable()-&gt;getClass());

$response = $this-&gt;responseFactory
    -&gt;createResponse($status)
    -&gt;withHeader(&#039;Content-Type&#039;, &#039;application/json&#039;)
;
$response-&gt;getBody()-&gt;write((string) json_encode($problem-&gt;toArray()));
```

### `HttpStatusResolver`: Mapping Exceptions to Status Codes

Shared by two consumers: `AbstractController` above (a real failure&#039;s actual status) and `Documenter` below (what to document for each `#[Operation(results: ...)]` scenario) — the same resolution, kept in one place instead of two copies that could drift apart. Only `derafu/backbone-dispatcher`&#039;s own 7 generic exceptions are mapped (the same 7 [Backbone Console](https://www.derafu.dev/docs/core/backbone-console#exitcoderesolverinterface-mapping-exceptions-to-codes)&#039;s `DefaultExitCodeResolver` maps, mirroring that same split) — anything else, including a business-specific exception, falls back to `500`:

| Status | Exception |
| --- | --- |
| `200` | (`&#039;success&#039;`) |
| `403` | `OperationNotAllowedException` |
| `422` | `MissingParameterException`, `InvalidParameterTypeException`, `NoDeserializerFoundException`, `ClassNotFoundException`, `FromArrayMethodNotFoundException` |
| `500` | anything else, including `OperationNotFoundException` and any business-specific exception |

There is no built-in way to extend this mapping with a project&#039;s own business exceptions yet — unlike `DefaultExitCodeResolver`, `HttpStatusResolver` is not currently designed to be subclassed for that. A project needing its own status codes today would wrap or replace it in its own DI wiring.

## Autodiscovery

One shared criterion for what counts as &quot;visible&quot;, applied consistently across both surfaces — because both are walked from the exact same source:

**`Explorer`** composes over `derafu/backbone-dispatcher`&#039;s own `ExplorerInterface` rather than reimplementing its walk, only adding HATEOAS `_links` on top of whatever it returns — `GET /api/billing/document/builder` returns the worker&#039;s `_links` plus its operations. Every id, name, description and policy-based visibility rule (see [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#controlling-which-operations-can-be-dispatched-operationpolicyinterface)) comes straight from the delegate: with a policy wired into it, a worker nobody may call does not show up here either.

**`Documenter`** generates the OpenAPI 3.1 document served at `GET /api/openapi-docs.json`, walked from `Explorer::tree()` — the exact same nested, policy-pruned structure `Explorer` itself is built on, not a separate traversal of the package registry. This used to not be true: `Documenter` had its own, independent notion of &quot;visible&quot; (any method tagged with Backbone&#039;s `#[Operation]` attribute, optionally narrowed by a *second*, separately-injected policy instance), which could silently drift from what a real `DirectDispatcher` would actually accept — an operation the active `OperationPolicyInterface` allowed, but nobody had gotten around to tagging, was dispatchable and browsable yet absent from the spec, understating the real attack surface. There is no way to configure `Documenter` differently from `Explorer` anymore: whatever `OperationPolicyInterface` was wired into the `ExplorerInterface` `Explorer` composes over is the one and only thing that decides what gets documented.

`#[Operation]` still exists and is still useful — for overriding a parameter&#039;s reflected type/description/example, or the operation&#039;s own name/description, when the reflected PHPDoc isn&#039;t enough (see [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#documenting-an-operation-operation)) — it just no longer gates *whether* something gets documented, only how it looks once it is. If you want a real, non-`#[Operation]`-tagged public method to disappear from both `Explorer` and `Documenter` at once, that is exactly what [`TaggedOperationPolicy`](https://www.derafu.dev/docs/core/backbone-dispatcher#controlling-which-operations-can-be-dispatched-operationpolicyinterface) is for — wired once, where the dispatch chain itself is built, never in `backbone-api`.

Every documented operation is generated as a single OpenAPI `post` entry (there&#039;s no GET/PUT/DELETE distinction), and its request/response schema is built from the reflected parameter types, following the same type-name vocabulary as `backbone-dispatcher`&#039;s `Caster::resolveType()` (`string`, `number`, `integer`, `boolean`, `array`, `object`).

&gt; [!TIP] Operation ≠ Job
&gt;
&gt; Same terminology note as [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#discovery-explorer-and-inspector): an &quot;operation&quot; here is any public method found via reflection, unrelated to Backbone&#039;s own `JobInterface`/`#[Job]` concept.

## Requirements

PHP 8.5+. Depends on [`derafu/backbone`](https://www.derafu.dev/docs/core/backbone), [`derafu/backbone-dispatcher`](https://www.derafu.dev/docs/core/backbone-dispatcher), `psr/http-message`, and `psr/http-factory` (for building the real failure response — a concrete PSR-17 implementation, e.g. `nyholm/psr7`, is the consuming application&#039;s to provide).




---

## Backbone Bridge Python Project

Call Any Backbone-Dispatcher Library From Python

# Call Any Backbone-Dispatcher Library From Python

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/backbone-bridge-python/main)
![CI Workflow](https://github.com/derafu/backbone-bridge-python/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/backbone-bridge-python)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/backbone-bridge-python)
![PyPI version](https://img.shields.io/pypi/v/derafu-backbone-bridge)
![PyPI downloads](https://img.shields.io/pypi/dm/derafu-backbone-bridge)

Generic Python bridge for any [`derafu/backbone-dispatcher`](https://www.derafu.dev/docs/core/backbone-dispatcher)-based PHP library, via [`swoole/phpy`](https://github.com/swoole/phpy) — without a caller ever having to know `phpy` exists.

## Why

`derafu/backbone-dispatcher`&#039;s `SafeDispatcherInterface` already turns any Backbone operation into something that never throws a raw PHP exception and always returns a serializable value — exactly the contract a foreign-language caller needs. `swoole/phpy` lets Python call into a real, embedded PHP interpreter in the same process. This package is the glue between the two: it boots a real `SafeDispatcherInterface` and exposes it as `dispatch(operation_id, **params)`, translating PHP exceptions into typed Python exceptions on the way back.

It knows nothing about any specific library. A specific PHP library&#039;s own bridge subclasses `GenericDispatcher` (and, for exploring a package tree, `GenericExplorer` — see [Exploring the Package Tree](#exploring-the-package-tree-genericexplorer) below), pre-wiring how its own `SafeDispatcherInterface`/`SafeExplorerInterface` gets booted — this is exactly what this package&#039;s own test suite does, against a real, minimal PHP fixture (no mocks):

```python
class ExampleDispatcher(GenericDispatcher):
    _BOOTSTRAP_CLASS = &#039;Derafu\\TestsBackboneBridgePython\\Fixture\\Bootstrap&#039;

    def __init__(self, autoload_path=None):
        super().__init__(self._BOOTSTRAP_CLASS, autoload_path=autoload_path)
```

## Installation

```bash
pip install derafu-backbone-bridge
```

`phpy` itself is **not** a normal pip dependency and never will be resolvable from PyPI: it requires a PHP build with `--enable-embed`, so it must already be present system-wide wherever this package runs — see [Docker with Python and Caddy](https://www.derafu.dev/docs/sysadmin/docker-python-caddy-server) for a ready-made image with an optional `PHPY_ENABLED` build. Install this package into a virtualenv created with `--system-site-packages` so it can see the system `phpy`.

&gt; [!WARNING] PyPI has an unrelated package also named `phpy`
&gt;
&gt; There is a long-abandoned, unrelated package on PyPI called `phpy` (&quot;call legacy PHP functions from Python&quot;, 2013). It is **not** `swoole/phpy`. This package deliberately does not declare `phpy` as a dependency at all, specifically so `pip install` never silently resolves that decoy instead of the real, system-provided one.

## Usage

```python
from derafu_backbone_bridge import GenericDispatcher

dispatcher = GenericDispatcher(
    &#039;Derafu\\TestsBackboneBridgePython\\Fixture\\Bootstrap&#039;,
    autoload_path=&#039;/path/to/backbone-bridge-python/tests/php/vendor/autoload.php&#039;,
)

result = dispatcher.dispatch(
    &#039;example_package.example_component.example_worker::sum&#039;,
    a=5, b=7,
)
result.value      # 12
result.data_type  # &quot;integer&quot; — `gettype()`/`get_class()` of `value`, before PHP serializes it.
result.metadata   # ExecutionMetadata — see below.
```

`operation_id` uses the same `&quot;package.component.worker::operation&quot;` format as `OperationRequest::fromId()` on the PHP side. Every keyword argument becomes a named operation parameter — `b` falls back to the operation&#039;s own PHP default (`10`) when omitted, exactly as it would for a direct PHP caller.

`dispatch()` always returns an `OperationResult` — never the bare value — because the PHP side&#039;s own `OperationResultInterface::getMetadata()` is never optional either: a successful dispatch always has execution statistics attached, so the Python side always has somewhere to put them.

```python
@dataclass(frozen=True)
class OperationResult:
    value: Any
    metadata: ExecutionMetadata
    data_type: str
```

`data_type` mirrors PHP&#039;s `OperationResultInterface::getDataType()` — never `None` here, unlike the PHP interface: a failed dispatch never reaches `OperationResult` at all (it raises instead, see [Handling Errors](#handling-errors)), so there is no failure case for it to be `None` for.

`ExecutionMetadata` mirrors PHP&#039;s `ExecutionMetadataInterface` field for field, just in `snake_case`:

```python
result.metadata.started_at          # &quot;2026-01-20T10:00:00+00:00&quot;
result.metadata.finished_at
result.metadata.timestamp           # 1755500000.123456 — same moment as `finished_at`, as a Unix epoch float.
result.metadata.real_time           # Seconds, wall-clock — the &quot;real&quot; of `time`.
result.metadata.user_time           # Seconds of CPU in user mode — the &quot;user&quot; of `time`.
result.metadata.system_time         # Seconds of CPU in kernel mode — the &quot;sys&quot; of `time`.
result.metadata.memory_used         # Bytes, a delta — can be negative if the GC freed more than this dispatch allocated.
result.metadata.peak_memory         # Bytes, the whole process&#039;s peak up to this point.
result.metadata.pid
result.metadata.load_average_1min   # Plus 5min/15min variants.
```

Assumes Linux/macOS, same as the PHP side: built on `getrusage()`/`sys_getloadavg()`, neither of which exists on Windows — no Windows support is offered.

### Where the PHP autoloader lives

`GenericDispatcher` resolves the PHP autoloader to `phpy.include()` from, in order: an explicit `autoload_path` argument, or the `BACKBONE_DISPATCHER_AUTOLOAD` environment variable. Exactly one variable is used regardless of which library is being bridged, because in practice a given process only ever hosts one bridge:

```bash
export BACKBONE_DISPATCHER_AUTOLOAD=/path/to/backbone-bridge-python/tests/php/vendor/autoload.php
```

```python
dispatcher = ExampleDispatcher()  # No path needed if the env var is set.
```

Neither of those is `phpy`-specific knowledge — they&#039;re just &quot;where does my dependency live,&quot; the same kind of configuration any bridge would need regardless of the underlying interop mechanism.

## Exploring the Package Tree: `GenericExplorer`

`GenericExplorer` mirrors `GenericDispatcher`&#039;s boot mechanism, wrapping a real `SafeExplorerInterface` instead of a `SafeDispatcherInterface`, with the same 10 methods `SafeExplorerInterface` has on the PHP side:

```python
from derafu_backbone_bridge import GenericExplorer

explorer = GenericExplorer(
    &#039;Derafu\\TestsBackboneBridgePython\\Fixture\\Bootstrap&#039;,
    bootstrap_method=&#039;bootExplorer&#039;,
    autoload_path=&#039;/path/to/backbone-bridge-python/tests/php/vendor/autoload.php&#039;,
)

explorer.get_packages()
explorer.get_components(&#039;example_package&#039;)
explorer.get_workers(&#039;example_package&#039;, &#039;example_component&#039;)
explorer.get_operations(&#039;example_package&#039;, &#039;example_component&#039;, &#039;example_worker&#039;)
explorer.get_package(&#039;example_package&#039;, with_components=True)
explorer.get_component(&#039;example_package&#039;, &#039;example_component&#039;, with_workers=True)
explorer.get_worker(&#039;example_package&#039;, &#039;example_component&#039;, &#039;example_worker&#039;, with_operations=True)
explorer.get_operation(&#039;example_package&#039;, &#039;example_component&#039;, &#039;example_worker&#039;, &#039;sum&#039;)
explorer.describe(&#039;example_package.example_component.example_worker&#039;)
explorer.tree(&#039;example_package.example_component.example_worker&#039;)
```

Every method returns a plain `dict`/`list` (or scalar), same shape `derafu/backbone-dispatcher`&#039;s `ExplorerInterface` produces, or raises a `BackboneBridgeError` — see [Handling Errors](#handling-errors) below, `GenericExplorer` uses the exact same mapping. Unlike `GenericDispatcher.dispatch()`, none of these return an `OperationResult`: there is no `ExecutionMetadata` to attach, since `SafeExplorerInterface` does not measure one.

## Handling Errors

A failed operation raises `BackboneBridgeError` or one of its subclasses — never a raw `phpy` call failure, and never a PHP exception object:

```python
from derafu_backbone_bridge import MissingParameterError

try:
    dispatcher.dispatch(&#039;example_package.example_component.example_worker::sum&#039;)
except MissingParameterError as e:
    print(e.php_class)  # &quot;Derafu\BackboneDispatcher\Exception\MissingParameterException&quot;
```

Every `BackboneBridgeError` carries the full `Problem` behind it (in `.problem`), not just a class name and a message:

```python
except MissingParameterError as e:
    e.problem.detail                 # The exception&#039;s own message.
    e.problem.instance                # &quot;example_package.example_component.example_worker::sum&quot;
    e.problem.timestamp               # 1755500000.123456 — Unix epoch float, same as `ExecutionMetadata.timestamp`.
    e.problem.throwable.php_class     # Same as `.php_class` below — a read-only shortcut to this.
    e.problem.throwable.file
    e.problem.throwable.line
    e.problem.throwable.trace
    e.metadata                        # ExecutionMetadata of the failed attempt, or `None` — see below.
```

`Problem`/`SafeThrowable` mirror PHP&#039;s `ProblemDetailInterface`/`SafeThrowableInterface`, in `snake_case`. Unlike PHP&#039;s own `ProblemDetail::toArray()`, `.throwable` here is **always** populated, regardless of whether the underlying `SafeDispatcher` was booted with `debug=True` or `False`: that flag only gates what PHP embeds when *serializing* a problem for an untrusted HTTP consumer, and does not apply to this bridge — `phpy` is an in-process, same-machine, trusted boundary.

`.metadata` is `None` when the failure came from `GenericExplorer` rather than `GenericDispatcher` — `SafeExplorerInterface` does not measure `ExecutionMetadata` (see [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#never-failing-while-exploring-safeexplorer)), so there is honestly none to give in that case; a dispatch failure always has one, and its `.timestamp` is the exact same clock reading as `.problem.timestamp` — not two independent captures.

`.php_class` is a read-only shortcut to `.problem.throwable.php_class` — it exists for backward-compatible ergonomics, not stored data:

```python
from derafu_backbone_bridge import BackboneBridgeError

try:
    dispatcher.dispatch(&#039;example_package.example_component.example_worker::fail&#039;)
except BackboneBridgeError as e:
    print(e.php_class)  # &quot;RuntimeException&quot; — a plain, unmapped PHP exception.
```

The hierarchy mirrors `derafu/backbone` and `derafu/backbone-dispatcher`&#039;s own exceptions one level deep (`ServiceNotFoundError`/`PackageNotFoundError`/`ComponentNotFoundError`/…, `ResolverError`/`InvalidParameterTypeError`/…), so callers can catch broadly or narrowly exactly as they would in PHP. Anything unmapped still raises `BackboneBridgeError`.

A specific library&#039;s own domain exceptions are registered by its own bridge, not by this package:

```python
dispatcher.exceptions.register(&#039;App\\Exception\\SomeDomainException&#039;, SomeDomainError)
```

Separately, a failure while booting PHP itself (a missing autoloader, a broken dependency, a bootstrap class or method that doesn&#039;t exist) raises `BootstrapError` — never a raw `phpy` error either, but also never confused with an operation failure, since booting happens before any `SafeDispatcherInterface` exists to produce a `Problem` from.

## Architecture

`phpy` is only ever imported by `GenericDispatcher`, `GenericExplorer`, and the internal `_phpy_conversions` module they both share for turning a live PHP object into a plain Python one — never by the tests, a specific library&#039;s own bridge, or the application consuming it. Building one specific library&#039;s bridge means subclassing `GenericDispatcher`/`GenericExplorer` with its `bootstrap_class`, and never touching `phpy` directly:

```python
class GenericDispatcher:
    _AUTOLOAD_PATH_ENV = &#039;BACKBONE_DISPATCHER_AUTOLOAD&#039;

    def __init__(
        self,
        bootstrap_class: str,
        bootstrap_method: str = &#039;boot&#039;,
        bootstrap_args: tuple = (),
        exception_registry: ExceptionRegistry | None = None,
        autoload_path: str | None = None,
    ) -&gt; None: ...

    def dispatch(self, operation_id: str, **params) -&gt; OperationResult: ...
```

`ExceptionRegistry` (the PHP-class-name → Python-exception mapping, via `raise_for(problem, metadata=None)`), and the `OperationResult`/`Problem`/`SafeThrowable`/`ExecutionMetadata` dataclasses themselves, are all separate, independent components that never touch `phpy` — plain, immutable data holders that can be built, tested and reasoned about with no PHP interpreter involved at all. `_phpy_conversions` is the one place that bridges the two worlds, turning a live PHP object into one of those.

## Requirements

Python 3.14+. `phpy` itself requires PHP built with `--enable-embed`.




---

## Mail Project

Derafu Mail

# Derafu Mail




---

### Introduction

Elegant orchestration of email communications for PHP

# Elegant orchestration of email communications for PHP

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/mail/main)
![CI Workflow](https://github.com/derafu/mail/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/mail)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/mail)
![Total Downloads](https://poser.pugx.org/derafu/mail/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/mail/d/monthly)

A flexible PHP email library, built on Derafu Backbone architecture, that leverages other libraries and orchestrates the entire sending and receiving process.

## Overview

Derafu Mail provides a robust, extensible framework for sending and receiving emails in PHP applications. Built on the [Derafu Backbone architecture](https://www.derafu.dev/docs/core/backbone), it offers a clean, maintainable structure with clear separation of concerns.

## Features

- **Clean Architecture**: Follows the Derafu Backbone hierarchical structure.
- **Sending Emails**: SMTP support with easy extensibility for other transport methods.
- **Receiving Emails**: IMAP support with customizable search criteria and filtering.
- **Flexible Configuration**: Comprehensive options for both sending and receiving.
- **Robust Error Handling**: Proper exception handling throughout the library.
- **Attachment Management**: Support for both sending and receiving attachments.
- **Strategy Pattern**: Easily swap out sending/receiving implementations.

## Installation

Install the package via Composer:

```bash
composer require derafu/mail
```

## Quick Start

### Sending Emails

```php
use Derafu\Backbone\Contract\PackageRegistryInterface;
use Derafu\Mail\Model\Message;
use Derafu\Mail\Model\Envelope;
use Derafu\Mail\Model\Postman;
use Symfony\Component\Mime\Address;

// Find the package registry using dependency injection and get a $senderWorker
// Also you can inject directly the SenderWorkerInterface wherever you want.
$packageRegistry = $container-&gt;get(PackageRegistryInterface::class);
$mailPackage = $packageRegistry-&gt;getPackage(&#039;mail&#039;);
$exchangeComponent = $mailPackage-&gt;getExchangeComponent();
$senderWorker = $exchangeComponent-&gt;getSenderWorker();

// Create a message.
$message = new Message();
$message-&gt;subject(&#039;Hello World&#039;)
    -&gt;text(&#039;This is a plain text message&#039;)
    -&gt;html(&#039;&lt;h1&gt;Hello World&lt;/h1&gt;&lt;p&gt;This is an HTML message&lt;/p&gt;&#039;)
    -&gt;from(new Address(&#039;sender@example.com&#039;, &#039;Sender Name&#039;))
    -&gt;to(new Address(&#039;recipient@example.com&#039;, &#039;Recipient Name&#039;));

// Create an envelope and add the message.
$envelope = new Envelope(
    new Address(&#039;sender@example.com&#039;, &#039;Sender Name&#039;),
    [new Address(&#039;recipient@example.com&#039;, &#039;Recipient Name&#039;)]
);
$envelope-&gt;addMessage($message);

// Create a postman with SMTP configuration.
$postman = new Postman([
    &#039;strategy&#039; =&gt; &#039;smtp&#039;,
    &#039;transport&#039; =&gt; [
        &#039;host&#039; =&gt; &#039;smtp.example.com&#039;,
        &#039;port&#039; =&gt; 587,
        &#039;encryption&#039; =&gt; &#039;tls&#039;,
        &#039;username&#039; =&gt; &#039;your_username&#039;,
        &#039;password&#039; =&gt; &#039;your_password&#039;,
    ],
]);
$postman-&gt;addEnvelope($envelope);

// Send the email.
$envelopes = $senderWorker-&gt;send($postman);
```

### Receiving Emails

```php
use Derafu\Backbone\Contract\PackageRegistryInterface;
use Derafu\Mail\Model\Postman;

// Find the package registry using dependency injection and get a $receiverWorker
// Also you can inject directly the ReceiverWorkerInterface wherever you want.
$packageRegistry = $container-&gt;get(PackageRegistryInterface::class);
$mailPackage = $packageRegistry-&gt;getPackage(&#039;mail&#039;);
$exchangeComponent = $mailPackage-&gt;getExchangeComponent();
$receiverWorker = $exchangeComponent-&gt;getReceiverWorker();

// Create a postman with IMAP configuration.
$postman = new Postman([
    &#039;strategy&#039; =&gt; &#039;imap&#039;,
    &#039;transport&#039; =&gt; [
        &#039;host&#039; =&gt; &#039;imap.example.com&#039;,
        &#039;port&#039; =&gt; 993,
        &#039;encryption&#039; =&gt; &#039;ssl&#039;,
        &#039;username&#039; =&gt; &#039;your_username&#039;,
        &#039;password&#039; =&gt; &#039;your_password&#039;,
        &#039;mailbox&#039; =&gt; &#039;INBOX&#039;,
        &#039;search&#039; =&gt; [
            &#039;criteria&#039; =&gt; &#039;UNSEEN&#039;,
            &#039;markAsSeen&#039; =&gt; true,
            &#039;attachmentFilters&#039; =&gt; [
                &#039;extension&#039; =&gt; [&#039;pdf&#039;, &#039;doc&#039;, &#039;docx&#039;],
            ],
        ],
    ],
]);

// Receive emails.
$envelopes = $receiverWorker-&gt;receive($postman);

// Process received emails.
foreach ($envelopes as $envelope) {
    foreach ($envelope-&gt;getMessages() as $message) {
        echo &quot;Subject: &quot; . $message-&gt;getSubject() . PHP_EOL;
        echo &quot;From: &quot; . $message-&gt;getFrom()[0]-&gt;getAddress() . PHP_EOL;
        echo &quot;Body: &quot; . $message-&gt;getTextBody() . PHP_EOL;

        // Process attachments.
        foreach ($message-&gt;getAttachments() as $attachment) {
            file_put_contents(&#039;/path/to/save/&#039; . $attachment-&gt;getFilename(), $attachment-&gt;getBody());
        }
    }
}
```

## Extending with New Strategies

The library is designed to be easily extended with new sending or receiving strategies:

1. Create a new strategy class implementing `SenderStrategyInterface` or `ReceiverStrategyInterface`.
2. Tag it with the appropriate attribute.

Example:
```php
use Derafu\Backbone\Attribute\Strategy;
use Derafu\Mail\Component\Exchange\Worker\Sender\Strategy\Abstract\AbstractMailerStrategy;
use Derafu\Mail\Component\Exchange\Worker\Sender\Strategy\Contract\SenderStrategyInterface;

#[Strategy(name: &#039;mailgun&#039;, worker: &#039;sender&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class MailgunStrategy extends AbstractMailerStrategy implements SenderStrategyInterface
{
    // Implementation of SenderStrategyInterface that leverages AbstractMailerStrategy.
}
```

## Architecture

Derafu Mail follows the Derafu Backbone architecture:

- **Package**: MailPackage - The main entry point.
- **Component**: ExchangeComponent - Handles email exchange.
- **Workers**: SenderWorker and ReceiverWorker - Handle sending and receiving.
- **Handlers**: SendHandler and ReceiveHandler - Orchestrate the process.
- **Strategies**: Implement different methods of sending or receiving.




---

### Architecture

Derafu Mail Architecture

# Derafu Mail Architecture

This guide explains the architecture of the Derafu Mail library, which is built on the Derafu Backbone architectural framework. Understanding this architecture will help you effectively use and extend the library.

## Architectural Overview

Derafu Mail follows a hierarchical architecture that separates concerns into distinct layers, each with specific responsibilities:

1. **Package Layer**: The entry point and container for all components.
2. **Component Layer**: Functional modules within the package.
3. **Worker Layer**: Task executors within components.
4. **Handler Layer**: Process orchestrators that coordinate strategies.
5. **Strategy Layer**: Specific implementations of mail operations.
6. **Model Layer**: Domain models representing email-related entities.

![Derafu Mail Architecture](https://www.derafu.dev/img/diagrams/content/docs/core/mail/derafu-mail-architecture.svg)

## Package Layer

The `MailPackage` serves as the entry point for the entire library, following the Package pattern from Derafu Backbone.

```php
#[Package(name: &#039;mail&#039;)]
class MailPackage extends AbstractPackage implements MailPackageInterface
```

**Responsibilities**:

- Provides access to components (currently just `ExchangeComponent`).
- Acts as the root of the dependency tree.
- Maintains registry information for service discovery.

## Component Layer

The `ExchangeComponent` represents a specific functional area within the mail domain, handling both sending and receiving of emails.

```php
#[Component(name: &#039;exchange&#039;, package: &#039;mail&#039;)]
class ExchangeComponent extends AbstractComponent implements ExchangeComponentInterface
```

**Responsibilities**:

- Manages workers for sending and receiving emails.
- Provides access to these workers through getter methods.
- Groups related functionality under a single namespace.

## Worker Layer

Workers expose the public API for specific tasks:

### SenderWorker

```php
#[Worker(name: &#039;sender&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class SenderWorker extends AbstractWorker implements SenderWorkerInterface
```

**Responsibilities**:

- Provides a public `send()` method for sending emails.
- Delegates the actual sending process to handlers.
- Manages any worker-specific resources.

### ReceiverWorker

```php
#[Worker(name: &#039;receiver&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class ReceiverWorker extends AbstractWorker implements ReceiverWorkerInterface
```

**Responsibilities**:

- Provides a public `receive()` method for receiving emails.
- Delegates the receiving process to handlers.
- Manages any worker-specific resources.

## Handler Layer

Handlers orchestrate complex processes, selecting appropriate strategies and managing the workflow:

### SendHandler

```php
class SendHandler extends AbstractHandler
```

**Responsibilities**:

- Selects the appropriate sending strategy based on configuration.
- Orchestrates the email sending process.
- Handles errors in a centralized manner.
- Manages options and configuration.

### ReceiveHandler

```php
class ReceiveHandler extends AbstractHandler
```

**Responsibilities**:

- Selects the appropriate receiving strategy based on configuration.
- Orchestrates the email receiving process.
- Handles errors in a centralized manner.
- Manages options and configuration.

## Strategy Layer

Strategies implement specific methods for sending or receiving emails:

### SmtpStrategy

```php
#[Strategy(name: &#039;smtp&#039;, worker: &#039;sender&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class SmtpStrategy extends AbstractMailerStrategy implements SenderStrategyInterface
```

**Responsibilities**:

- Configures and uses Symfony Mailer for SMTP transport.
- Builds appropriate DSN string based on configuration.
- Handles the actual sending of emails.
- Manages SMTP-specific settings.

### ImapStrategy

```php
#[Strategy(name: &#039;imap&#039;, worker: &#039;receiver&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class ImapStrategy extends AbstractMailboxStrategy implements ReceiverStrategyInterface
```

**Responsibilities**:

- Configures and uses PHP-IMAP for IMAP access.
- Builds appropriate DSN string for IMAP connection.
- Searches for and retrieves emails based on criteria.
- Manages IMAP-specific settings.

## Abstract Base Classes

The library provides abstract base classes that implement common functionality:

### AbstractMailerStrategy

```php
abstract class AbstractMailerStrategy extends AbstractStrategy implements SenderStrategyInterface
```

**Responsibilities**:

- Provides common code for email sending strategies.
- Handles the creation of Symfony Mailer instances.
- Processes envelopes and messages.

### AbstractMailboxStrategy

```php
abstract class AbstractMailboxStrategy extends AbstractStrategy implements ReceiverStrategyInterface
```

**Responsibilities**:

- Provides common code for email receiving strategies.
- Handles the creation of Mailbox instances.
- Processes received emails into envelopes.

## Model Layer

Domain models represent the core entities of the email domain:

### Envelope

```php
class Envelope extends SymfonyEnvelope implements EnvelopeInterface
```

**Responsibilities**:

- Contains sender and recipient information.
- Holds one or more messages.
- Provides a container for email communication.

### Message

```php
class Message extends SymfonyEmail implements MessageInterface
```

**Responsibilities**:

- Represents an individual email message.
- Contains subject, body, attachments, etc.
- Tracks sending/receiving errors.

### Postman

```php
class Postman implements PostmanInterface
```

**Responsibilities**:

- Acts as a transport container for envelopes.
- Holds configuration options for sending/receiving.
- Provides a unified interface for email operations.

### Mailbox

```php
class Mailbox implements MailboxInterface
```

**Responsibilities**:

- Represents an email mailbox (IMAP folder).
- Provides methods for searching and retrieving emails.
- Handles connection to mail servers.

## Data Flow Examples

### Sending an Email

1. Client code creates a Message and adds it to an Envelope.
2. Envelope is added to a Postman with SMTP configuration.
3. Client calls SenderWorker&#039;s send() method with the Postman.
4. SenderWorker delegates to SendHandler.
5. SendHandler selects SmtpStrategy based on configuration.
6. SmtpStrategy uses Symfony Mailer to send the emails.
7. Results are returned through the same chain.

### Receiving Emails

1. Client creates a Postman with IMAP configuration.
2. Client calls ReceiverWorker&#039;s receive() method with the Postman.
3. ReceiverWorker delegates to ReceiveHandler.
4. ReceiveHandler selects ImapStrategy based on configuration.
5. ImapStrategy connects to the mailbox and retrieves emails.
6. Retrieved emails are converted to Envelopes with Messages.
7. Envelopes are added to the Postman and returned.

## Extending the Library

The architecture makes it easy to extend the library with new strategies:

1. **New Sending Strategy**: Create a class that extends AbstractMailerStrategy or implements SenderStrategyInterface.
2. **New Receiving Strategy**: Create a class that extends AbstractMailboxStrategy or implements ReceiverStrategyInterface.
3. **Tag with Attribute**: Use the #[Strategy] attribute for automatic discovery.
4. **Use via Configuration**: Specify your strategy in the Postman options.

## Key Benefits of This Architecture

1. **Separation of Concerns**: Each class has a single, well-defined responsibility.
2. **Extensibility**: Easy to add new strategies without modifying existing code.
3. **Testability**: Clean interfaces make unit testing straightforward.
4. **Configurability**: Comprehensive options at every level.
5. **Maintainability**: Clear structure makes code easy to understand and modify.

## Conclusion

Derafu Mail&#039;s architecture provides a solid foundation for email operations in PHP applications. By leveraging the Derafu Backbone patterns, it achieves a clean separation of concerns while remaining flexible and extensible.

Understanding this architecture will help you effectively use the library and extend it with new capabilities when needed.




---

### Custom Sender Strategy

Creating a Custom Sender Strategy for Derafu Mail

# Creating a Custom Sender Strategy for Derafu Mail

This guide explains how to create custom sender strategies for Derafu Mail. Sender strategies allow you to implement different methods of sending emails beyond the default SMTP implementation.

## Understanding Sender Strategies

A sender strategy in Derafu Mail is responsible for the actual transmission of email messages. The library comes with an SMTP implementation, but you might want to add support for:

- API-based email services (SendGrid, Mailgun, Postmark, etc.).
- Custom internal email systems.
- Database-based email queues.
- Testing/mock implementations.

## Option 1: Extending AbstractMailerStrategy

The simplest approach is to extend the `AbstractMailerStrategy` class, which provides reusable functionality for most email sending scenarios that utilize Symfony Mailer internally.

### When to Use This Approach

- When your email service has a Symfony Transport implementation.
- When you need to reuse the core sending logic.
- When your strategy follows a similar workflow to the SMTP strategy.

### Implementation Steps

1. Create a new class that extends `AbstractMailerStrategy`:

```php
&lt;?php

declare(strict_types=1);

namespace YourNamespace\Strategy;

use Derafu\Backbone\Attribute\Strategy;
use Derafu\Config\Contract\OptionsInterface;
use Derafu\Mail\Component\Exchange\Worker\Sender\Strategy\Abstract\AbstractMailerStrategy;
use Derafu\Mail\Component\Exchange\Worker\Sender\Strategy\Contract\SenderStrategyInterface;

#[Strategy(name: &#039;mailgun&#039;, worker: &#039;sender&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class MailgunStrategy extends AbstractMailerStrategy implements SenderStrategyInterface
{
    /**
     * Schema of the options.
     *
     * @var array&lt;string,array&gt;
     */
    protected array $optionsSchema = [
        &#039;strategy&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;string&#039;,
            &#039;default&#039; =&gt; &#039;mailgun&#039;,
        ],
        &#039;transport&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;array&#039;,
            &#039;schema&#039; =&gt; [
                &#039;api_key&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;domain&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;region&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;default&#039; =&gt; &#039;us&#039;,
                ],
                &#039;dsn&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                ],
                &#039;endpoint&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                ],
            ],
        ],
    ];

    /**
     * {@inheritDoc}
     */
    protected function resolveDsn(OptionsInterface $options): string
    {
        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        if (!empty($transportOptions[&#039;dsn&#039;])) {
            return $transportOptions[&#039;dsn&#039;];
        }

        // Construct the DSN for Mailgun using Symfony&#039;s format.
        $dsn = sprintf(
            &#039;mailgun://%s@%s?region=%s&#039;,
            $transportOptions[&#039;api_key&#039;],
            $transportOptions[&#039;domain&#039;],
            $transportOptions[&#039;region&#039;] ?? &#039;us&#039;
        );

        $options-&gt;set(&#039;transport.dsn&#039;, $dsn);

        return $dsn;
    }

    /**
     * {@inheritDoc}
     */
    protected function resolveEndpoint(OptionsInterface $options): string
    {
        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        if (!empty($transportOptions[&#039;endpoint&#039;])) {
            return $transportOptions[&#039;endpoint&#039;];
        }

        $endpoint = sprintf(
            &#039;mailgun://%s&#039;,
            $transportOptions[&#039;domain&#039;]
        );

        $options-&gt;set(&#039;transport.endpoint&#039;, $endpoint);

        return $endpoint;
    }
}
```

2. Define your options schema to specify the required configuration.

3. Implement the `resolveDsn()` method to build the appropriate DSN string for your service.

4. Implement the `resolveEndpoint()` method to provide a human-readable representation of the endpoint.

### Key Benefits

- Leverages existing functionality from the abstract class.
- Reduces code duplication.
- Ensures consistent behavior across strategies.
- Automatically gets error handling and envelope processing.

## Option 2: Implementing SenderStrategyInterface

For more specialized use cases, you can implement the `SenderStrategyInterface` directly.

### When to Use This Approach

- When your sending mechanism is fundamentally different from Symfony Mailer.
- When you need complete control over the sending process.
- When you want to avoid dependencies on Symfony components.
- For custom integrations with proprietary systems

### Implementation Steps

1. Create a new class that implements `SenderStrategyInterface`:

```php
&lt;?php

declare(strict_types=1);

namespace YourNamespace\Strategy;

use Derafu\Backbone\Abstract\AbstractStrategy;
use Derafu\Backbone\Attribute\Strategy;
use Derafu\Config\Contract\OptionsInterface;
use Derafu\Mail\Component\Exchange\Worker\Sender\Strategy\Contract\SenderStrategyInterface;
use Derafu\Mail\Exception\MailException;
use Derafu\Mail\Model\Contract\PostmanInterface;
use GuzzleHttp\Client;
use Throwable;

#[Strategy(name: &#039;custom-api&#039;, worker: &#039;sender&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class CustomApiStrategy extends AbstractStrategy implements SenderStrategyInterface
{
    /**
     * Schema of the options.
     *
     * @var array&lt;string,array&gt;
     */
    protected array $optionsSchema = [
        &#039;strategy&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;string&#039;,
            &#039;default&#039; =&gt; &#039;custom-api&#039;,
        ],
        &#039;transport&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;array&#039;,
            &#039;schema&#039; =&gt; [
                &#039;api_url&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;api_key&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                // Add any other configuration options needed.
            ],
        ],
    ];

    /**
     * HTTP client for API requests.
     */
    private Client $httpClient;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this-&gt;httpClient = new Client();
    }

    /**
     * {@inheritDoc}
     */
    public function send(PostmanInterface $postman): array
    {
        $options = $this-&gt;resolveOptions($postman-&gt;getOptions());
        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        $apiUrl = $transportOptions[&#039;api_url&#039;];
        $apiKey = $transportOptions[&#039;api_key&#039;];

        foreach ($postman-&gt;getEnvelopes() as $envelope) {
            foreach ($envelope-&gt;getMessages() as $message) {
                try {
                    // Transform the message to your API format.
                    $payload = $this-&gt;transformMessageToApiPayload($message, $envelope);

                    // Send via your custom API.
                    $response = $this-&gt;httpClient-&gt;post($apiUrl, [
                        &#039;headers&#039; =&gt; [
                            &#039;Authorization&#039; =&gt; &#039;Bearer &#039; . $apiKey,
                            &#039;Content-Type&#039; =&gt; &#039;application/json&#039;,
                        ],
                        &#039;json&#039; =&gt; $payload,
                    ]);

                    // Process response if needed.
                    if ($response-&gt;getStatusCode() &gt;= 400) {
                        throw new MailException(&#039;API returned error: &#039; . $response-&gt;getBody());
                    }

                } catch (Throwable $e) {
                    $message-&gt;error($e);
                }
            }
        }

        return $postman-&gt;getEnvelopes();
    }

    /**
     * Transforms a message to the format expected by the API.
     *
     * @param MessageInterface $message
     * @param EnvelopeInterface $envelope
     * @return array
     */
    private function transformMessageToApiPayload($message, $envelope): array
    {
        // Implement the transformation logic for your specific API.
        // This is where you map the Message and Envelope properties
        // to whatever format your API expects.

        return [
            &#039;from&#039; =&gt; $this-&gt;formatAddress($message-&gt;getFrom()[0]),
            &#039;to&#039; =&gt; array_map([$this, &#039;formatAddress&#039;], $message-&gt;getTo()),
            &#039;subject&#039; =&gt; $message-&gt;getSubject(),
            &#039;text&#039; =&gt; $message-&gt;getTextBody(),
            &#039;html&#039; =&gt; $message-&gt;getHtmlBody(),
            // Handle attachments, CC, BCC, etc.
        ];
    }

    /**
     * Formats an email address for the API.
     */
    private function formatAddress($address): array
    {
        return [
            &#039;email&#039; =&gt; $address-&gt;getAddress(),
            &#039;name&#039; =&gt; $address-&gt;getName(),
        ];
    }
}
```

2. Define your options schema to specify the required configuration.

3. Implement the `send()` method to handle the entire sending process.

4. Add any helper methods needed for your specific implementation.

### Key Considerations

When implementing from scratch:

- **Error Handling**: You must handle all exceptions and errors.
- **Message Processing**: You need to transform Derafu Mail messages to your API format.
- **State Management**: Consider how to track message status and handle failures.
- **Testing**: Create test cases for various scenarios and error conditions.

## Usage

Once you&#039;ve created your custom strategy, you can use it by specifying its name in the Postman configuration:

```php
$postman = new Postman([
    &#039;strategy&#039; =&gt; &#039;mailgun&#039;, // Or &#039;custom-api&#039;
    &#039;transport&#039; =&gt; [
        // Strategy-specific configuration options.
        &#039;api_key&#039; =&gt; &#039;your-api-key&#039;,
        &#039;domain&#039; =&gt; &#039;your-domain.com&#039;,
        // Other options...
    ],
]);
```

## Best Practices

1. **Error Handling**: Always catch and properly handle exceptions.
2. **Logging**: Add appropriate logging to help troubleshoot issues.
3. **Configuration Validation**: Use the options schema to validate configuration.
4. **Comprehensive Documentation**: Document your strategy&#039;s requirements.
5. **Unit Testing**: Create tests for various scenarios including error cases.

## Conclusion

Creating custom sender strategies allows you to extend Derafu Mail to work with any email service or system. Whether you extend the abstract class or implement the interface directly depends on your specific needs and how much you want to leverage the existing infrastructure.

For most API-based email services that have Symfony Transport implementations, extending `AbstractMailerStrategy` is recommended. For completely custom implementations, implementing `SenderStrategyInterface` directly gives you maximum flexibility.




---

### Custom Receiver Strategy

Creating a Custom Receiver Strategy for Derafu Mail

# Creating a Custom Receiver Strategy for Derafu Mail

This guide explains how to create custom receiver strategies for Derafu Mail. Receiver strategies allow you to implement different methods of retrieving emails beyond the default IMAP implementation.

## Understanding Receiver Strategies

A receiver strategy in Derafu Mail is responsible for connecting to mail sources and retrieving messages. The library comes with an IMAP implementation, but you might want to add support for:

- API-based email services (Gmail API, Microsoft Graph, etc.).
- Custom email storage systems.
- Database-stored emails.
- Webhook receivers for incoming emails.
- Testing/mock implementations.

## Option 1: Extending AbstractMailboxStrategy

The simplest approach is to extend the `AbstractMailboxStrategy` class, which provides reusable functionality for email retrieval scenarios that use a mailbox-like interface.

### When to Use This Approach

- When your email source follows a mailbox paradigm.
- When you can use the PHP-IMAP library or similar interfaces.
- When you need to reuse common mailbox operations.
- When your strategy follows a similar workflow to the IMAP strategy.

### Implementation Steps

1. Create a new class that extends `AbstractMailboxStrategy`:

```php
&lt;?php

declare(strict_types=1);

namespace YourNamespace\Strategy;

use Derafu\Backbone\Attribute\Strategy;
use Derafu\Config\Contract\OptionsInterface;
use Derafu\Mail\Component\Exchange\Worker\Receiver\Strategy\Abstract\AbstractMailboxStrategy;
use Derafu\Mail\Component\Exchange\Worker\Receiver\Strategy\Contract\ReceiverStrategyInterface;
use Derafu\Mail\Model\Mailbox;
use Derafu\Mail\Model\Contract\MailboxInterface;

#[Strategy(name: &#039;gmail-api&#039;, worker: &#039;receiver&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class GmailApiStrategy extends AbstractMailboxStrategy implements ReceiverStrategyInterface
{
    /**
     * Schema of the options.
     *
     * @var array&lt;string,array&gt;
     */
    protected array $optionsSchema = [
        &#039;strategy&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;string&#039;,
            &#039;default&#039; =&gt; &#039;gmail-api&#039;,
        ],
        &#039;transport&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;array&#039;,
            &#039;schema&#039; =&gt; [
                &#039;client_id&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;client_secret&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;refresh_token&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;user_email&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;label&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;default&#039; =&gt; &#039;INBOX&#039;,
                ],
                &#039;dsn&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                ],
                &#039;endpoint&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                ],
                &#039;search&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;array&#039;,
                    &#039;schema&#039; =&gt; [
                        &#039;query&#039; =&gt; [
                            &#039;types&#039; =&gt; &#039;string&#039;,
                            &#039;default&#039; =&gt; &#039;is:unread&#039;,
                        ],
                        &#039;markAsSeen&#039; =&gt; [
                            &#039;types&#039; =&gt; &#039;bool&#039;,
                            &#039;default&#039; =&gt; false,
                        ],
                        &#039;attachmentFilters&#039; =&gt; [
                            &#039;types&#039; =&gt; &#039;array&#039;,
                            &#039;default&#039; =&gt; [],
                        ],
                    ],
                ],
            ],
        ],
    ];

    /**
     * {@inheritDoc}
     */
    protected function createMailbox(OptionsInterface $options): MailboxInterface
    {
        // Instead of using the standard Mailbox, create a specialized Gmail API mailbox.
        // This could be a custom class that implements MailboxInterface.

        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        // This would be a custom implementation for Gmail API.
        return new GmailApiMailbox(
            $transportOptions[&#039;client_id&#039;],
            $transportOptions[&#039;client_secret&#039;],
            $transportOptions[&#039;refresh_token&#039;],
            $transportOptions[&#039;user_email&#039;],
            $transportOptions[&#039;label&#039;] ?? &#039;INBOX&#039;
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function resolveDsn(OptionsInterface $options): string
    {
        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        if (!empty($transportOptions[&#039;dsn&#039;])) {
            return $transportOptions[&#039;dsn&#039;];
        }

        // Construct a representative DSN for Gmail API.
        $dsn = sprintf(
            &#039;gmail-api://%s&#039;,
            $transportOptions[&#039;user_email&#039;]
        );

        $options-&gt;set(&#039;transport.dsn&#039;, $dsn);

        return $dsn;
    }

    /**
     * {@inheritDoc}
     */
    protected function resolveEndpoint(OptionsInterface $options): string
    {
        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        if (!empty($transportOptions[&#039;endpoint&#039;])) {
            return $transportOptions[&#039;endpoint&#039;];
        }

        $endpoint = sprintf(
            &#039;https://gmail.googleapis.com/gmail/v1/users/%s&#039;,
            $transportOptions[&#039;user_email&#039;]
        );

        $options-&gt;set(&#039;transport.endpoint&#039;, $endpoint);

        return $endpoint;
    }
}

/**
 * Custom Mailbox implementation for Gmail API.
 * This class would need to implement all methods from MailboxInterface
 * but would use the Gmail API instead of IMAP.
 */
class GmailApiMailbox implements MailboxInterface
{
    // Implement all required methods from MailboxInterface.
    // This would use Google API Client or similar to fetch emails.
}
```

2. Define your options schema to specify the required configuration.

3. Override the `createMailbox()` method to return your custom mailbox implementation.

4. Implement the `resolveDsn()` and `resolveEndpoint()` methods.

5. Create a custom mailbox class that implements `MailboxInterface` if needed.

### Key Benefits

- Leverages existing workflow and error handling.
- Preserves compatibility with the rest of the library.
- Reuses attachment filtering and other common functionality.
- Maintains consistent behavior across strategies.

## Option 2: Implementing ReceiverStrategyInterface

For more specialized use cases, you can implement the `ReceiverStrategyInterface` directly.

### When to Use This Approach

- When your receiving mechanism is fundamentally different from a mailbox model.
- When you need complete control over the receiving process.
- When you&#039;re creating a strategy for webhooks or other non-polling mechanisms.
- For custom integrations with proprietary systems.

### Implementation Steps

1. Create a new class that implements `ReceiverStrategyInterface`:

```php
&lt;?php

declare(strict_types=1);

namespace YourNamespace\Strategy;

use Derafu\Backbone\Abstract\AbstractStrategy;
use Derafu\Backbone\Attribute\Strategy;
use Derafu\Mail\Component\Exchange\Worker\Receiver\Strategy\Contract\ReceiverStrategyInterface;
use Derafu\Mail\Exception\MailException;
use Derafu\Mail\Model\Contract\EnvelopeInterface;
use Derafu\Mail\Model\Contract\MessageInterface;
use Derafu\Mail\Model\Contract\PostmanInterface;
use Derafu\Mail\Model\Envelope;
use Derafu\Mail\Model\Message;
use Symfony\Component\Mime\Address;
use Throwable;

#[Strategy(name: &#039;webhook-receiver&#039;, worker: &#039;receiver&#039;, component: &#039;exchange&#039;, package: &#039;mail&#039;)]
class WebhookReceiverStrategy extends AbstractStrategy implements ReceiverStrategyInterface
{
    /**
     * Schema of the options.
     *
     * @var array&lt;string,array&gt;
     */
    protected array $optionsSchema = [
        &#039;strategy&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;string&#039;,
            &#039;default&#039; =&gt; &#039;webhook-receiver&#039;,
        ],
        &#039;transport&#039; =&gt; [
            &#039;types&#039; =&gt; &#039;array&#039;,
            &#039;schema&#039; =&gt; [
                &#039;webhook_data&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;array&#039;,
                    &#039;required&#039; =&gt; true,
                ],
                &#039;secret_key&#039; =&gt; [
                    &#039;types&#039; =&gt; &#039;string&#039;,
                    &#039;default&#039; =&gt; &#039;&#039;,
                ],
                // Add any other configuration options needed.
            ],
        ],
    ];

    /**
     * {@inheritDoc}
     */
    public function receive(PostmanInterface $postman): array
    {
        $options = $this-&gt;resolveOptions($postman-&gt;getOptions());
        $transportOptions = $options-&gt;get(&#039;transport&#039;);

        $webhookData = $transportOptions[&#039;webhook_data&#039;];
        $secretKey = $transportOptions[&#039;secret_key&#039;] ?? &#039;&#039;;

        try {
            // Validate webhook data if a secret is configured.
            if ($secretKey &amp;&amp; !$this-&gt;validateWebhookSignature($webhookData, $secretKey)) {
                throw new MailException(&#039;Invalid webhook signature&#039;);
            }

            // Process the webhook data to extract email information.
            $emails = $this-&gt;processWebhookData($webhookData);

            // Create envelopes and add them to the postman.
            foreach ($emails as $emailData) {
                $envelope = $this-&gt;createEnvelope($emailData);
                $postman-&gt;addEnvelope($envelope);
            }

            // Optionally acknowledge receipt to the webhook source.
            $this-&gt;acknowledgeReceipt($webhookData);

        } catch (Throwable $e) {
            throw new MailException(
                sprintf(
                    &#039;An error occurred while processing webhook data: %s&#039;,
                    $e-&gt;getMessage()
                ),
                0,
                $e
            );
        }

        return $postman-&gt;getEnvelopes();
    }

    /**
     * Validates the webhook signature to ensure authenticity.
     *
     * @param array $webhookData
     * @param string $secretKey
     * @return bool
     */
    private function validateWebhookSignature(array $webhookData, string $secretKey): bool
    {
        // Implement signature validation logic.
        // The exact implementation depends on how your webhook source signs requests.
        return true; // Placeholder.
    }

    /**
     * Processes the webhook data to extract email information.
     *
     * @param array $webhookData
     * @return array
     */
    private function processWebhookData(array $webhookData): array
    {
        // Convert webhook data to a standardized email format.
        // This will depend entirely on the webhook format you&#039;re receiving.

        // Placeholder implementation - extract email data from webhook.
        $emails = [];

        if (isset($webhookData[&#039;emails&#039;]) &amp;&amp; is_array($webhookData[&#039;emails&#039;])) {
            foreach ($webhookData[&#039;emails&#039;] as $email) {
                $emails[] = [
                    &#039;from&#039; =&gt; $email[&#039;sender&#039;] ?? &#039;&#039;,
                    &#039;from_name&#039; =&gt; $email[&#039;sender_name&#039;] ?? &#039;&#039;,
                    &#039;to&#039; =&gt; $email[&#039;recipient&#039;] ?? &#039;&#039;,
                    &#039;to_name&#039; =&gt; $email[&#039;recipient_name&#039;] ?? &#039;&#039;,
                    &#039;subject&#039; =&gt; $email[&#039;subject&#039;] ?? &#039;&#039;,
                    &#039;text_body&#039; =&gt; $email[&#039;plain_text&#039;] ?? &#039;&#039;,
                    &#039;html_body&#039; =&gt; $email[&#039;html&#039;] ?? &#039;&#039;,
                    &#039;attachments&#039; =&gt; $email[&#039;attachments&#039;] ?? [],
                ];
            }
        }

        return $emails;
    }

    /**
     * Creates an envelope from email data.
     *
     * @param array $emailData
     * @return EnvelopeInterface
     */
    private function createEnvelope(array $emailData): EnvelopeInterface
    {
        // Create a sender address.
        $sender = new Address(
            $emailData[&#039;from&#039;],
            $emailData[&#039;from_name&#039;] ?? &#039;&#039;
        );

        // Create recipient addresses.
        $recipients = [
            new Address(
                $emailData[&#039;to&#039;],
                $emailData[&#039;to_name&#039;] ?? &#039;&#039;
            )
        ];

        // Create the envelope.
        $envelope = new Envelope($sender, $recipients);

        // Create and add the message.
        $message = $this-&gt;createMessage($emailData);
        $envelope-&gt;addMessage($message);

        return $envelope;
    }

    /**
     * Creates a message from email data.
     *
     * @param array $emailData
     * @return MessageInterface
     */
    private function createMessage(array $emailData): MessageInterface
    {
        // Create the message.
        $message = new Message();

        // Set basic properties
        $message-&gt;subject($emailData[&#039;subject&#039;] ?? &#039;&#039;);

        if (!empty($emailData[&#039;text_body&#039;])) {
            $message-&gt;text($emailData[&#039;text_body&#039;]);
        }

        if (!empty($emailData[&#039;html_body&#039;])) {
            $message-&gt;html($emailData[&#039;html_body&#039;]);
        }

        $message-&gt;from(new Address(
            $emailData[&#039;from&#039;],
            $emailData[&#039;from_name&#039;] ?? &#039;&#039;
        ));

        $message-&gt;to(new Address(
            $emailData[&#039;to&#039;],
            $emailData[&#039;to_name&#039;] ?? &#039;&#039;
        ));

        // Process attachments if any.
        if (!empty($emailData[&#039;attachments&#039;]) &amp;&amp; is_array($emailData[&#039;attachments&#039;])) {
            foreach ($emailData[&#039;attachments&#039;] as $attachment) {
                if (isset($attachment[&#039;content&#039;], $attachment[&#039;name&#039;], $attachment[&#039;type&#039;])) {
                    $content = base64_decode($attachment[&#039;content&#039;]);
                    $message-&gt;attach(
                        $content,
                        $attachment[&#039;name&#039;],
                        $attachment[&#039;type&#039;]
                    );
                }
            }
        }

        return $message;
    }

    /**
     * Acknowledges receipt to the webhook source if needed.
     *
     * @param array $webhookData
     * @return void
     */
    private function acknowledgeReceipt(array $webhookData): void
    {
        // Some webhook providers require an acknowledgement.
        // Implement if needed for your specific case.
    }
}
```

2. Define your options schema to specify the required configuration.

3. Implement the `receive()` method to handle the entire receiving process.

4. Add helper methods for your specific implementation needs.

### Key Considerations

When implementing from scratch:

- **Error Handling**: Properly catch and handle all exceptions.
- **Data Transformation**: Carefully map external data to Derafu Mail models.
- **Security**: Validate webhook signatures or implement other security measures.
- **Consistency**: Ensure your implementation behaves consistently with other strategies.
- **Testing**: Create comprehensive tests for your implementation.

## Usage

Once you&#039;ve created your custom strategy, you can use it by specifying its name in the Postman configuration:

```php
$postman = new Postman([
    &#039;strategy&#039; =&gt; &#039;gmail-api&#039;, // Or &#039;webhook-receiver&#039;
    &#039;transport&#039; =&gt; [
        // Strategy-specific configuration options.
        &#039;client_id&#039; =&gt; &#039;your-client-id&#039;,
        &#039;client_secret&#039; =&gt; &#039;your-client-secret&#039;,
        &#039;refresh_token&#039; =&gt; &#039;your-refresh-token&#039;,
        &#039;user_email&#039; =&gt; &#039;user@example.com&#039;,
        // Other options...
    ],
]);

$receiverWorker = $exchangeComponent-&gt;getReceiverWorker();
$envelopes = $receiverWorker-&gt;receive($postman);
```

## Best Practices

1. **Error Handling**: Always catch and handle exceptions appropriately.
2. **Logging**: Add detailed logging to help troubleshoot issues.
3. **Configuration Validation**: Use the options schema to validate configuration.
4. **Rate Limiting**: Consider rate limits for API-based strategies.
5. **Pagination**: Implement proper pagination for retrieving large volumes of emails.
6. **Performance**: Be mindful of memory usage when dealing with attachments.
7. **Documentation**: Document your strategy&#039;s specific requirements and limitations.

## Conclusion

Creating custom receiver strategies allows you to extend Derafu Mail to work with any email source or system. Whether you extend the abstract class or implement the interface directly depends on your specific needs and how much you want to leverage the existing infrastructure.

For sources that follow a mailbox-like model, extending `AbstractMailboxStrategy` is recommended. For completely different mechanisms like webhooks, implementing `ReceiverStrategyInterface` directly gives you maximum flexibility.




---

## Logbook Project

PHP Logging Library

# PHP Logging Library

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/log/main)
![CI Workflow](https://github.com/derafu/log/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/log)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/log)
![Total Downloads](https://poser.pugx.org/derafu/log/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/log/d/monthly)

A flexible, PSR-3 compliant PHP logging library that leverages Monolog and add additional features while maintaining a clean and simple API.

## Features

- **PSR-3 Compatible**: Implements the PSR-3 logging interface for standardized usage.
- **In-Memory Log Storage**: Store logs in memory for retrieval during runtime.
- **Caller Information**: Automatically track which class, method, file, and line generated each log message.
- **Custom Formatting**: Enhanced line formatter that includes caller information.
- **Multiple Log Levels**: Support for all standard log levels (debug, info, notice, warning, error, critical, alert, emergency).
- **Context Support**: Add structured data to your log messages.
- **Monolog Integration**: Built on top of Monolog for robust logging capabilities.

## Installation

```bash
composer require derafu/log
```

## Basic Usage

```php
use Derafu\Log\Level;
use Derafu\Log\Service\Logger;
use Derafu\Log\Service\LogService;
use Derafu\Log\Service\LineFormatter;

// Create a logger with a line formatter.
$logger = new Logger(
    formatter: new LineFormatter()
);

// Create the log service.
$logService = new LogService($logger);

// Log messages with different levels.
$logService-&gt;logger()-&gt;debug(&#039;Debug message&#039;);
$logService-&gt;logger()-&gt;info(&#039;Info message&#039;);
$logService-&gt;logger()-&gt;warning(&#039;Warning message&#039;, [&#039;context&#039; =&gt; &#039;value&#039;]);
$logService-&gt;logger()-&gt;error(&#039;Error message&#039;, [&#039;error_code&#039; =&gt; 500]);

// Retrieve all logs.
$allLogs = $logService-&gt;logs();

// Retrieve only error logs.
$errorLogs = $logService-&gt;logs(Level::ERROR);

// Retrieve logs, newest first (default).
$newestFirst = $logService-&gt;logs(null, true);

// Retrieve logs, oldest first.
$oldestFirst = $logService-&gt;logs(null, false);

// Clear logs.
$logService-&gt;clear(); // Clear all logs.
$logService-&gt;clear(Level::ERROR); // Clear only error logs.

// Retrieve logs and clear them in one operation.
$logs = $logService-&gt;flush();
```

## Caller Information

One of the key features of this library is automatic tracking of the caller information. Every log includes details about which class, method, file, and line generated the log message:

```php
$logService-&gt;logger()-&gt;error(&#039;Something went wrong&#039;);

// The log will include caller information automatically.
$logs = $logService-&gt;logs();
$caller = $logs[0]-&gt;getCaller();

echo $caller; // Outputs: in /path/to/file.php on line 42, called by Namespace\Class::method()
```

## Working with Log Records

Each log record provides methods to access its data:

```php
$logs = $logService-&gt;logs();
$log = $logs[0];

// Access log data.
$level = $log-&gt;getCode();
$message = $log-&gt;getMessage();
$context = $log-&gt;getContext();
$caller = $log-&gt;getCaller();
```

## Advanced Configuration

You can customize the logger with additional handlers:

```php
use Monolog\Handler\StreamHandler;
use Monolog\Level;

// Create a logger with additional handlers.
$logger = new Logger(
    configuration: [&#039;channel&#039; =&gt; &#039;app&#039;],
    handlers: [
        new StreamHandler(&#039;path/to/your.log&#039;, Level::Debug),
        // Add more handlers as needed.
    ],
    formatter: new LineFormatter()
);

$logService = new LogService($logger);
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

## License

This package is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).




---

## Auth Project

Derafu Auth

# Derafu Auth




---

### Introduction

Authentication and Authorization

# Derafu: Auth

PSR-15 compliant authentication and authorization library for PHP applications with Keycloak integration.

## Features

- **PSR-15 Middleware**: Standard-compliant middleware.
- **Keycloak Integration**: OAuth2/OpenID Connect.
- **Session Management**: Secure session handling.
- **Token Refresh**: Automatic token refresh.
- **Route Protection**: Flexible route-based auth.
- **CSRF Protection**: State parameter validation.

## Quick Start

### Installation

Install the package:

```bash
composer require derafu/auth
```

### Environment Variables

Configure, at the very least, the following environment variables:

```env
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_CLIENT_ID=your-client-id
KEYCLOAK_CLIENT_SECRET=your-client-secret
KEYCLOAK_REDIRECT_URI=http://localhost/auth/callback
```

### Routes

Import the routes to your `routes.yaml`:

```yaml
imports:
    - { resource: &#039;../vendor/derafu/auth/resources/config/auth-routes.yaml&#039; }
```

### Services

Import the services to your `services.yaml`:

```yaml
imports:
    - { resource: &#039;../vendor/derafu/auth/resources/config/auth-services.yaml&#039; }
```

### Middleware

Add the middleware to your `services.yaml`:

```yaml
Psr\Http\Server\RequestHandlerInterface:
    class: Derafu\Http\Service\RequestHandler
    public: true
    arguments:
        $middlewares:
            - &#039;@Derafu\Auth\Middleware\AuthenticationMiddleware&#039;
```

**Note**: the `AuthenticationMiddleware` needs to be placed before the `DispatcherMiddleware`.

### Access User Information

Directly with the attribute `user` or `access_token`:

```php
$user = $request-&gt;getAttribute(&#039;user&#039;);
$accessToken = $request-&gt;getAttribute(&#039;access_token&#039;);
```

Using the `UserInterface` from the `mezzio/mezzio-authentication` package:

```php
$user = $request-&gt;getAttribute(UserInterface::class);
if ($user) {
    $identity = $user-&gt;getIdentity();
    $roles = iterator_to_array($user-&gt;getRoles());
    $email = $user-&gt;getDetail(&#039;email&#039;);
}
```




---

### Configuration

Configuration options for Derafu Auth

# Configuration

## Environment Variables

| Variable | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `KEYCLOAK_URL` | string | **Yes** | - | Keycloak server URL |
| `KEYCLOAK_CLIENT_ID` | string | **Yes** | - | OAuth2 client ID |
| `KEYCLOAK_CLIENT_SECRET` | string | **Yes** | - | OAuth2 client secret |
| `KEYCLOAK_REDIRECT_URI` | string | **Yes** | - | OAuth2 redirect URI |
| `KEYCLOAK_REALM` | string | No | `master` | Keycloak realm |
| `KEYCLOAK_SCOPES` | json | No | `[&quot;openid&quot;, &quot;profile&quot;, &quot;email&quot;]` | OAuth2 scopes |
| `KEYCLOAK_PROTECTED_ROUTES` | json | No | `[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]` | Protected routes |
| `KEYCLOAK_CALLBACK_ROUTE` | string | No | `/auth/callback` | Callback route |
| `KEYCLOAK_LOGOUT_ROUTE` | string | No | `/auth/logout` | Logout route |
| `KEYCLOAK_SESSION_LIFETIME` | int | No | `3600` | Session lifetime |
| `KEYCLOAK_SECURE_COOKIES` | bool | No | `false` | Secure cookies |
| `KEYCLOAK_HTTP_TIMEOUT` | int | No | `30` | HTTP timeout |
| `KEYCLOAK_HTTP_CONNECT_TIMEOUT` | int | No | `30` | HTTP connect timeout |
| `KEYCLOAK_HTTP_VERIFY` | bool | No | `false` | SSL verification |

## Required Configuration

The following parameters are **mandatory** and must be provided:

```env
# Keycloak server configuration.
KEYCLOAK_URL=https://&lt;YOUR_KEYCLOAK_SERVER&gt;

# OAuth2 client credentials.
KEYCLOAK_CLIENT_ID=your-client-id
KEYCLOAK_CLIENT_SECRET=your-client-secret

# OAuth2 redirect URI.
KEYCLOAK_REDIRECT_URI=https://&lt;YOUR_APP_URL&gt;/auth/callback
```

## Routes Configuration

```yaml
auth_callback:
    path: /auth/callback
    handler: Derafu\Auth\Controller\CallbackController::handle

```

## Services Configuration

```yaml
services:

    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    Derafu\Auth\Contract\AuthConfigurationInterface:
        class: Derafu\Auth\Configuration\AuthConfiguration
        arguments:
            $config:
                keycloak_url: &#039;%env(default::string:KEYCLOAK_URL)%&#039;
                realm: &#039;%env(default::string:KEYCLOAK_REALM)%&#039;
                client_id: &#039;%env(default::string:KEYCLOAK_CLIENT_ID)%&#039;
                client_secret: &#039;%env(default::string:KEYCLOAK_CLIENT_SECRET)%&#039;
                redirect_uri: &#039;%env(default::string:KEYCLOAK_REDIRECT_URI)%&#039;
                scopes: &#039;%env(default::json:KEYCLOAK_SCOPES)%&#039;
                protected_routes: &#039;%env(default::json:KEYCLOAK_PROTECTED_ROUTES)%&#039;
                callback_route: &#039;%env(default::string:KEYCLOAK_CALLBACK_ROUTE)%&#039;
                logout_route: &#039;%env(default::string:KEYCLOAK_LOGOUT_ROUTE)%&#039;
                session_lifetime: &#039;%env(default::int:KEYCLOAK_SESSION_LIFETIME)%&#039;
                secure_cookies: &#039;%env(default::bool:KEYCLOAK_SECURE_COOKIES)%&#039;
                http_client_options:
                    timeout: &#039;%env(default::int:KEYCLOAK_HTTP_TIMEOUT)%&#039;
                    connect_timeout: &#039;%env(default::int:KEYCLOAK_HTTP_CONNECT_TIMEOUT)%&#039;
                    verify: &#039;%env(default::bool:KEYCLOAK_HTTP_VERIFY)%&#039;

    Derafu\Auth\Contract\AuthenticationProviderInterface:
        class: Derafu\Auth\Service\KeycloakAuthenticationService

    Derafu\Auth\Contract\SessionManagerInterface:
        class: Derafu\Auth\Service\SessionService

    Derafu\Auth\Contract\RouteValidatorInterface:
        class: Derafu\Auth\Validator\RouteValidator

    Derafu\Auth\Middleware\AuthenticationMiddleware: ~

    Derafu\Auth\Controller\CallbackController:
        public: true

```




---

### Route Protection

Route protection capabilities in Derafu Auth

# Route Protection

The Derafu Auth package provides route protection through a simple array-based configuration system.

## How It Works

The `RouteValidator` class determines which routes require authentication by checking if the requested path starts with any of the configured protected routes.

```php
public function requiresAuth(string $path): bool
{
    foreach ($this-&gt;config-&gt;getProtectedRoutes() as $route) {
        if (str_starts_with($path, $route)) {
            return true;
        }
    }

    return false;
}
```

## Configuration

### Environment Variable

```env
KEYCLOAK_PROTECTED_ROUTES=&#039;[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]&#039;
```

### Default Protected Routes

If not specified, the package uses these default protected routes:

```json
[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]
```

## Route Protection Examples

### Basic Protection

Protect specific routes:

```env
KEYCLOAK_PROTECTED_ROUTES=[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]
```

This configuration will protect:

- `/dashboard` - Exact match.
- `/dashboard/settings` - Starts with `/dashboard`.
- `/profile` - Exact match.
- `/profile/edit` - Starts with `/profile`.
- `/admin` - Exact match.
- `/admin/users` - Starts with `/admin`.

### API Protection

Protect API routes:

```env
KEYCLOAK_PROTECTED_ROUTES=[&quot;/api/v1&quot;, &quot;/api/v2&quot;]
```

This will protect all routes starting with `/api/v1` or `/api/v2`:

- `/api/v1/users`
- `/api/v1/users/123`
- `/api/v2/admin`
- `/api/v2/admin/settings`

### Admin Panel Protection

Protect admin panel:

```env
KEYCLOAK_PROTECTED_ROUTES=[&quot;/admin&quot;, &quot;/moderator&quot;]
```

This protects:

- `/admin` - Admin panel
- `/admin/users` - User management
- `/admin/settings` - Settings
- `/moderator` - Moderator panel
- `/moderator/comments` - Comment moderation

## Limitations

The current implementation has the following limitations:

1. **Prefix Matching Only**: Routes are protected using `str_starts_with()`, so `/admin` will also protect `/admin-panel` and `/administer`.

2. **No Regex Support**: The package does not support regular expressions for route matching.

3. **No Wildcard Support**: There is no support for wildcard patterns like `/admin/*`.

4. **No Negative Patterns**: Cannot exclude specific routes from protection.

5. **No Role-Based Protection**: All protected routes require the same level of authentication. But you can use authorization to protect specific routes based on the user&#039;s roles.

## Special Routes

The package automatically handles these special routes:

- **Callback Route**: Default is `/auth/callback`.
- **Logout Route**: Default is `/auth/logout`.

These routes are automatically excluded from authentication requirements.

## Custom Route Validator

You can create a custom route validator by implementing the `RouteValidatorInterface`:

```php
use Derafu\Auth\Contract\RouteValidatorInterface;

class CustomRouteValidator implements RouteValidatorInterface
{
    public function requiresAuth(string $path): bool
    {
        // Your custom logic here
        return str_starts_with($path, &#039;/protected&#039;);
    }

    public function isCallbackPath(string $path): bool
    {
        return $path === &#039;/auth/callback&#039;;
    }

    public function isLogoutPath(string $path): bool
    {
        return $path === &#039;/auth/logout&#039;;
    }

    public function getProtectedRoutes(): array
    {
        return [&#039;/protected&#039;];
    }

    public function getCallbackRoute(): string
    {
        return &#039;/auth/callback&#039;;
    }

    public function getLogoutRoute(): string
    {
        return &#039;/auth/logout&#039;;
    }
}
```

Then configure it in your services:

```yaml
services:
    Derafu\Auth\Contract\RouteValidatorInterface:
        class: App\Auth\CustomRouteValidator
```




---

### Authorization

Authorization and permissions in Derafu Auth

# Authorization

Derafu Auth handles **authentication** (who you are) and helps with **authorization** (what you can do). The last one depends on the roles and permissions you set in Keycloak.

## What comes from the package?

When a user is authenticated, the package automatically provides:

- **Basic data**: `sub`, `email`, `name`, `preferred_username`, etc.
- **Roles**: Come directly from Keycloak (e.g., `user`, `admin`, `moderator`).

## What you need to implement?

### Option 1: Role-based authorization (Recommended)

**Advantages**:

- Simple to implement.
- Roles already come from Keycloak.
- No additional configuration required.

**How it works**:

1. Keycloak assigns roles to users.
2. Your application maps permissions to roles.
3. You verify if the user has the required role.

**Mapping example**:

- Permission `user:read` → Roles `user`, `admin`, `moderator`.
- Permission `user:write` → Roles `admin`, `moderator`.
- Permission `admin:access` → Only role `admin`.

### Option 2: Permission-based authorization

**Advantages**:

- More granular.
- Specific permissions per action.
- Better access control.

**Disadvantages**:

- Requires additional Keycloak configuration.
- More complex to maintain.

## Keycloak configuration for permissions

### Step 1: Create a custom scope

1. Go to **Clients** → Your Client → **Client Scopes**
2. Create a new scope called `permissions`.
3. Add it to your client.

### Step 2: Configure Token Mapper

1. In the `permissions` scope, go to **Mappers**.
2. Create a new mapper:
   - **Name**: `permissions`
   - **Mapper Type**: `User Attribute`
   - **User Attribute**: `permissions`
   - **Token Claim Name**: `permissions`
   - **Claim JSON Type**: `String`
   - **Full group path**: `false`

### Step 3: Assign permissions to users

1. Go to **Users** → Select a user → **Attributes**
2. Add the attribute:
   - **Key**: `permissions`
   - **Value**: `user:read,user:write,admin:access`

### Step 4: Configure the scope in your application

```env
KEYCLOAK_SCOPES=[&quot;openid&quot;, &quot;profile&quot;, &quot;email&quot;, &quot;permissions&quot;]
```

## How do permissions work?

### Authorization flow:

1. **User authenticates** → Keycloak generates token with permissions.
2. **Token arrives at your app** → Derafu Auth extracts the data.
3. **Your code verifies** → If the user has the required permission.
4. **Access granted/denied** → Based on the verification.

### Recommended permission structure:

```
resource:action
```

**Examples**:

- `user:read` - Read users.
- `user:write` - Create/edit users.
- `user:delete` - Delete users.
- `admin:access` - Access admin panel.
- `report:generate` - Generate reports.

## Key differences

| Aspect            | Roles         | Permissions       |
|-------------------|---------------|-------------------|
| **Configuration** | Automatic     | Requires Keycloak |
| **Granularity**   | Access groups | Specific actions  |
| **Maintenance**   | Easy          | More complex      |
| **Flexibility**   | Limited       | High              |

## Recommendation

**Start with role-based authorization** because:

- It&#039;s simpler to implement.
- No additional Keycloak configuration required.
- Sufficient for most applications.
- You can migrate to permissions later if needed.

## Summary

- **Authentication**: Handled automatically by Derafu Auth.
- **Authorization**: Implementation based on roles or permissions.
- **Roles**: Come automatically from Keycloak.
- **Permissions**: Require additional Keycloak configuration.
- **Recommendation**: Start with roles, migrate to permissions if you need more granularity.




---

### Security

Security best practices for Derafu Auth

# Security

## Environment Variables

Always use environment variables for sensitive configuration:

```env
# ✅ Good
KEYCLOAK_CLIENT_SECRET=your-secret-here

# ❌ Bad
&#039;client_secret&#039; =&gt; &#039;your-secret-here&#039;
```

## HTTPS Configuration

Enable HTTPS in production:

```env
# Development.
KEYCLOAK_SECURE_COOKIES=false
KEYCLOAK_HTTP_VERIFY=false

# Production.
KEYCLOAK_SECURE_COOKIES=true
KEYCLOAK_HTTP_VERIFY=true
```

## Session Security

```php
// Secure session configuration.
$config = new AuthConfiguration([
    &#039;session_lifetime&#039; =&gt; 3600,
    &#039;secure_cookies&#039; =&gt; true,
]);

// Additional session security that can be set.
ini_set(&#039;session.cookie_httponly&#039;, true);
ini_set(&#039;session.cookie_samesite&#039;, &#039;Lax&#039;);
ini_set(&#039;session.use_strict_mode&#039;, true);
```

## CSRF Protection

The package automatically validates the `state` parameter to prevent CSRF attacks.

## Token Security

Never store tokens in client-side storage:

```php
// ✅ Good - Server-side session.
$session-&gt;set(&#039;access_token&#039;, $accessToken);

// ❌ Bad - Never expose to client.
return new JsonResponse([&#039;token&#039; =&gt; $accessToken]);
```

## Authorization

```php
// Validate role format.
private function isValidRole(string $role): bool
{
    return preg_match(&#039;/^[a-zA-Z0-9_]+$/&#039;, $role) === 1;
}

// Check role hierarchy.
private function hasRole(array $userRoles, string $requiredRole): bool
{
    $roleHierarchy = [
        &#039;super_admin&#039; =&gt; [&#039;admin&#039;, &#039;user&#039;],
        &#039;admin&#039; =&gt; [&#039;user&#039;],
    ];

    if (in_array($requiredRole, $userRoles)) {
        return true;
    }

    foreach ($userRoles as $userRole) {
        if (isset($roleHierarchy[$userRole]) &amp;&amp;
            in_array($requiredRole, $roleHierarchy[$userRole])) {
            return true;
        }
    }

    return false;
}
```

## Error Handling

Don&#039;t expose sensitive information in production:

```php
public function handle(ServerRequestInterface $request, Throwable $exception): ResponseInterface
{
    $isProduction = getenv(&#039;APP_ENV&#039;) === &#039;production&#039;;

    if ($exception instanceof AuthenticationException) {
        return new JsonResponse([
            &#039;error&#039; =&gt; &#039;Authentication failed&#039;,
            &#039;code&#039; =&gt; &#039;UNAUTHORIZED&#039;
        ], 401);
    }

    return new JsonResponse([
        &#039;error&#039; =&gt; $isProduction ? &#039;Internal server error&#039; : $exception-&gt;getMessage(),
        &#039;code&#039; =&gt; &#039;INTERNAL_ERROR&#039;
    ], 500);
}
```

## Security Headers

```php
class SecurityHeadersMiddleware
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $response = $handler-&gt;handle($request);

        return $response
            -&gt;withHeader(&#039;X-Content-Type-Options&#039;, &#039;nosniff&#039;)
            -&gt;withHeader(&#039;X-Frame-Options&#039;, &#039;DENY&#039;)
            -&gt;withHeader(&#039;X-XSS-Protection&#039;, &#039;1; mode=block&#039;)
            -&gt;withHeader(&#039;Referrer-Policy&#039;, &#039;strict-origin-when-cross-origin&#039;)
            -&gt;withHeader(&#039;Content-Security-Policy&#039;, &quot;default-src &#039;self&#039;&quot;)
            -&gt;withHeader(&#039;Strict-Transport-Security&#039;, &#039;max-age=31536000; includeSubDomains&#039;);
    }
}
```

## Keycloak Security

### Client Configuration

1. **Access Type**: `confidential`.
2. **Valid Redirect URIs**: Only your application URLs.
3. **Web Origins**: Your application domain.
4. **Client Authentication**: Enabled.

### Realm Settings

1. **Password Policy**: Strong password requirements.
2. **Brute Force Detection**: Enabled.
3. **Session Timeout**: Configured appropriately.
4. **SSL Required**: `external` or `all`.




---

### Examples

Practical examples for Derafu Auth

# Examples

## Minimal Setup

```php
&lt;?php

require_once &#039;vendor/autoload.php&#039;;

use Derafu\Auth\Configuration\AuthConfiguration;
use Derafu\Auth\Service\KeycloakAuthenticationService;
use Derafu\Auth\Service\SessionService;
use Derafu\Auth\Validator\RouteValidator;
use Derafu\Auth\Middleware\AuthenticationMiddleware;

$config = new AuthConfiguration([
    &#039;keycloak_url&#039; =&gt; &#039;http://localhost:8080&#039;,
    &#039;client_id&#039; =&gt; &#039;your-client-id&#039;,
    &#039;client_secret&#039; =&gt; &#039;your-client-secret&#039;,
    &#039;redirect_uri&#039; =&gt; &#039;http://localhost/auth/callback&#039;,
]);

$authService = new KeycloakAuthenticationService($config);
$sessionService = new SessionService($config);
$routeValidator = new RouteValidator($config);

$authMiddleware = new AuthenticationMiddleware(
    $authService,
    $sessionService,
    $routeValidator
);
```

## API Examples

### REST API with Authentication

```php
class UserApiController
{
    public function getUsers(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request-&gt;getAttribute(&#039;user&#039;);

        if (!$user) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Not authenticated&#039;], 401);
        }

        if (!$this-&gt;isAuthorized($user, &#039;user:read&#039;)) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Access denied&#039;], 403);
        }

        $users = $this-&gt;getUserList();

        return new JsonResponse([&#039;users&#039; =&gt; $users]);
    }

    private function isAuthorized(array $user, string $permission): bool
    {
        $userPermissions = $user[&#039;permissions&#039;] ?? [];

        return in_array($permission, $userPermissions);
    }
}
```

### GraphQL with Authentication

```php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;

class AuthenticatedGraphQLSchema
{
    public function createSchema(): Schema
    {
        $userType = new ObjectType([
            &#039;name&#039; =&gt; &#039;User&#039;,
            &#039;fields&#039; =&gt; [
                &#039;id&#039; =&gt; Type::string(),
                &#039;email&#039; =&gt; Type::string(),
                &#039;name&#039; =&gt; Type::string(),
                &#039;roles&#039; =&gt; Type::listOf(Type::string()),
            ],
        ]);

        $queryType = new ObjectType([
            &#039;name&#039; =&gt; &#039;Query&#039;,
            &#039;fields&#039; =&gt; [
                &#039;me&#039; =&gt; [
                    &#039;type&#039; =&gt; $userType,
                    &#039;resolve&#039; =&gt; function ($root, $args, $context) {
                        $user = $context[&#039;user&#039;] ?? null;

                        if (!$user) {
                            throw new \Exception(&#039;Not authenticated&#039;);
                        }

                        return $user;
                    },
                ],
            ],
        ]);

        return new Schema([
            &#039;query&#039; =&gt; $queryType,
        ]);
    }
}
```

## Testing Example

```php
use PHPUnit\Framework\TestCase;
use Derafu\Auth\Middleware\AuthenticationMiddleware;

class AuthenticationMiddlewareTest extends TestCase
{
    public function testProtectedRouteRequiresAuthentication()
    {
        $config = new AuthConfiguration([
            &#039;keycloak_url&#039; =&gt; &#039;http://localhost:8080&#039;,
            &#039;client_id&#039; =&gt; &#039;test-client&#039;,
            &#039;client_secret&#039; =&gt; &#039;test-secret&#039;,
            &#039;redirect_uri&#039; =&gt; &#039;http://localhost/auth/callback&#039;,
            &#039;protected_routes&#039; =&gt; [&#039;/dashboard&#039;],
        ]);

        $authService = $this-&gt;createMock(KeycloakAuthenticationService::class);
        $sessionService = $this-&gt;createMock(SessionService::class);
        $routeValidator = new RouteValidator($config);

        $middleware = new AuthenticationMiddleware(
            $authService,
            $sessionService,
            $routeValidator
        );

        $request = $this-&gt;createMock(ServerRequestInterface::class);
        $request-&gt;method(&#039;getUri&#039;)-&gt;willReturn(new Uri(&#039;/dashboard&#039;));

        $handler = $this-&gt;createMock(RequestHandlerInterface::class);

        $response = $middleware-&gt;process($request, $handler);

        $this-&gt;assertEquals(302, $response-&gt;getStatusCode()); // Redirect to login.
    }
}
```

## Real-World Examples

### E-commerce Application

```php
class EcommerceAuthController
{
    public function handleLogin(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request-&gt;getAttribute(&#039;user&#039;);

        if (!$user) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Not authenticated&#039;], 401);
        }

        if (!$this-&gt;hasPermission($user, &#039;shop:access&#039;)) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Access denied&#039;], 403);
        }

        $cart = $this-&gt;getUserCart($user[&#039;sub&#039;]);

        return new JsonResponse([
            &#039;user&#039; =&gt; $user,
            &#039;cart&#039; =&gt; $cart,
            &#039;permissions&#039; =&gt; $user[&#039;permissions&#039;] ?? [],
        ]);
    }

    private function hasPermission(array $user, string $permission): bool
    {
        $permissions = $user[&#039;permissions&#039;] ?? [];

        return in_array($permission, $permissions);
    }
}
```

### Admin Panel

```php
class AdminPanelController
{
    public function handleDashboard(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request-&gt;getAttribute(&#039;user&#039;);

        if (!$user) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Not authenticated&#039;], 401);
        }

        if (!$this-&gt;isAdmin($user)) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Access denied&#039;], 403);
        }

        $stats = $this-&gt;getAdminStats();

        return new JsonResponse([
            &#039;user&#039; =&gt; $user,
            &#039;stats&#039; =&gt; $stats,
        ]);
    }

    private function isAdmin(array $user): bool
    {
        $roles = $user[&#039;roles&#039;] ?? [];

        return in_array(&#039;admin&#039;, $roles) || in_array(&#039;super_admin&#039;, $roles);
    }
}
```




---

## Cache Project

Consistent PSR-6/PSR-16 Cache Wiring Across Derafu Packages

# Consistent PSR-6/PSR-16 Cache Wiring Across Derafu Packages

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/cache/main)
![CI Workflow](https://github.com/derafu/cache/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/cache)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/cache)
![Total Downloads](https://poser.pugx.org/derafu/cache/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/cache/d/monthly)

A small, opinionated layer over [`symfony/cache`](https://symfony.com/doc/current/components/cache.html) for the *local* backends (no connection, no credentials, no network round trip) — plus a handful of things PSR-6/PSR-16 and `symfony/cache` genuinely don&#039;t provide on their own.

## Why

`symfony/cache`&#039;s own file-based adapters (`PhpFilesAdapter`, `FilesystemAdapter`) accept a `null` directory and silently fall back to one you&#039;d have to read the source to find. A cache backed by the filesystem that nobody can point to exactly is a cache nobody can clear with confidence — `derafu/cache`&#039;s adapters make `$namespace` and `$directory` required instead, so that decision is never made for you.

Past that one guardrail, this package adds a handful of things PSR-6/PSR-16 and `symfony/cache` genuinely don&#039;t provide on their own: a safe way to build cache keys without hand-rolling sanitization, a stampede-safe &quot;compute and remember&quot; helper, and a way to inspect or clear a cache directory without needing to reconstruct whatever wrote to it.

It is **not** a general abstraction over every backend `symfony/cache` supports. PSR-6/PSR-16 already are that abstraction — building another one on top would just be indirection. See [Scope](#scope-local-only) below.

## Installation

```bash
composer require derafu/cache
```

`ext-apcu` (for `LocalCacheBackend::Apcu`) and `symfony/console` (for the bundled commands) are both entirely optional — see [`composer.json`&#039;s `suggest`](https://github.com/derafu/cache/blob/main/composer.json).

## Scope: Local Only

`LocalCacheBackend` and `LocalCacheFactory` cover exactly the backends that need no connection, no credentials, and no network round trip: they live in this process, in shared memory on this server, or on this filesystem.

```php
enum LocalCacheBackend: string
{
    case None = &#039;none&#039;;           // Never caches anything (NullAdapter).
    case Memory = &#039;memory&#039;;       // Dies with the process (ArrayAdapter).
    case Apcu = &#039;apcu&#039;;           // Shared memory, survives requests (ApcuAdapter).
    case Filesystem = &#039;filesystem&#039;; // Serialized files, supports objects (FilesystemAdapter).
    case PhpFiles = &#039;php_files&#039;;  // Plain PHP array files, OPcache-friendly (PhpFilesAdapter).
}
```

Redis, Memcached, Couchbase, PDO, and every other connection-based backend `symfony/cache` supports are deliberately out of scope — not a gap, a boundary. They have a fundamentally different construction shape (a connection or DSN, not a namespace/directory pair) and none of the &quot;silent default&quot; problem this package exists to close off: `RedisAdapter`/`MemcachedAdapter` already force you to pass a real connection, no `null` fallback to guard against. Building one directly from `symfony/cache` isn&#039;t a workaround, it&#039;s simply what those backends need — `$pool-&gt;clear()` (part of PSR-6 itself) already works on them the same way it works on everything else.

## `PhpFilesCache` and `FilesystemCache`

```php
use Derafu\Cache\Adapter\PhpFilesCache;
use Derafu\Cache\Adapter\FilesystemCache;

// OPcache-friendly: values must be var_export()-able (arrays/scalars, no objects).
$pool = new PhpFilesCache(&#039;my_package&#039;, &#039;/var/cache/my_package&#039;);

// Serialized: supports arbitrary values, including objects. No OPcache benefit.
$pool = new FilesystemCache(&#039;my_package&#039;, &#039;/var/cache/my_package&#039;);
```

Both extend the matching `symfony/cache` adapter directly, under the same name, so anyone already familiar with `symfony/cache` recognizes immediately what each one does. `$namespace` and `$directory` are both required — `$defaultLifetime` (3600 by default) is the only parameter allowed to have one, since it isn&#039;t a &quot;where does my data live&quot; decision.

## `LocalCacheFactory`

For the case where the backend itself is a runtime choice — from configuration, the way a per-feature `cache: memory|filesystem` setting would — rather than something you already know at the call site:

```php
use Derafu\Cache\LocalCacheFactory;
use Derafu\Cache\Enum\LocalCacheBackend;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

$pool = LocalCacheFactory::pool(LocalCacheBackend::Filesystem, &#039;my_package&#039;, &#039;/var/cache/my_package&#039;);

// PSR-16 instead of PSR-6 — the same pool, wrapped in Symfony&#039;s Psr16Cache bridge.
$cache = LocalCacheFactory::simple(LocalCacheBackend::Memory, &#039;my_package&#039;);

// Tag-aware — $item-&gt;tag([...]) on save, invalidateTags([...]) to drop a whole group at once.
$cache = LocalCacheFactory::taggedPool(LocalCacheBackend::Apcu, &#039;my_package&#039;);

// Several pools, fastest first, with automatic backfill on a slower-tier hit.
$pool = LocalCacheFactory::layered([new ArrayAdapter(), $filesystemPool]);
```

`$directory` is required for `Filesystem`/`PhpFiles`, ignored for `None`/`Memory`/`Apcu` — passing it for those three is harmless, omitting it for the other two throws.

`LocalCacheBackend::None` (backed by Symfony&#039;s own `NullAdapter`) is how &quot;no caching&quot; is expressed — not a runtime flag a consumer has to check, but a pool like any other. A decorator that always receives a `CacheItemPoolInterface` needs no special case for &quot;caching is off&quot;: it just does nothing useful when this happens to be the pool it got.

## `Memoizer`: Compute-and-Cache Without Reinventing Stampede Protection

```php
use Derafu\Cache\Memoizer;

$memoizer = new Memoizer();

$value = $memoizer-&gt;remember($pool, &#039;some_key&#039;, function () {
    return expensive_computation();
}, ttl: 3600);
```

Every hand-written PSR-6 caching decorator ends up rewriting the same &quot;is it there? no? compute it, store it, return it&quot; dance — `getItem()` → `isHit()` → `set()` → `save()`. That naive version has no protection against a cache stampede: under concurrent access, every process that hits a cold cache computes and saves independently.

Every `symfony/cache` adapter also implements Symfony&#039;s own [`CacheInterface::get()`](https://symfony.com/doc/current/components/cache.html#basic-usage-psr-6) contract, which does the same thing with real locking — only one process computes, the rest wait for it and read the result. `Memoizer::remember()` uses that path automatically whenever the given pool supports it, and falls back to the naive dance only for a PSR-6 implementation that isn&#039;t a Symfony adapter (no locking possible without Symfony&#039;s own contract). `$ttl` means exactly what it means for `expiresAfter()` — `null` leaves the pool&#039;s default lifetime in effect, `0` means the entry never expires, a positive integer is seconds until expiration. Nothing here redefines that; a decorator that also needs a &quot;don&#039;t even try to cache&quot; switch should express it as its own separately-named parameter, or — more simply — accept a `LocalCacheBackend::None` pool.

## `CacheKey`: One Safe Way to Build a Key

```php
use Derafu\Cache\CacheKey;

$cacheKey = new CacheKey();

$key = $cacheKey-&gt;build(&#039;my_package&#039;, &#039;ExampleWorker&#039;, [&#039;api_resource&#039; =&gt; true]);
// &quot;my_package.ExampleWorker.e381541fbf7da0e4036f1cc881592fce&quot;
```

PSR-6 forbids `{}()/\@:` in keys but sanitizes nothing for you — building a safe, collision-resistant key from real context (a class name with backslashes, a filter array) is left entirely to each caller. `CacheKey::build()` normalizes each part: a string gets its forbidden characters *replaced* (never dropped — dropping them could collide `App\FooBar` with `App\Foo\Bar`, both becoming `AppFooBar`), any other scalar is used as-is, and anything else (an array, an object) is hashed. If the assembled key would exceed 250 bytes (Memcached&#039;s real limit, a safe ceiling for any backend), the whole thing collapses to the prefix plus a hash of the full key — long context never silently breaks a backend with an actual key-length limit.

`CacheKey` implements `Derafu\Cache\Contract\CacheKeyInterface` so a consumer that needs key logic driven by what&#039;s actually being cached — not just its class name — can inject its own implementation of that one-method interface instead of subclassing `CacheKey` and reaching into its private sanitizing helpers.

## `CacheDirectory`: Inspecting and Clearing by Path Alone

```php
use Derafu\Cache\CacheDirectory;

$directory = new CacheDirectory(&#039;/var/cache/my_package&#039;);

$directory-&gt;stats(); // [&#039;count&#039; =&gt; 42, &#039;totalSize&#039; =&gt; 1048576, &#039;oldest&#039; =&gt; 1700000000, &#039;newest&#039; =&gt; 1700003600]
$directory-&gt;clear(); // int — how many files were deleted.
```

`stats()` is a real, new capability — neither PSR-6 nor `symfony/cache` expose a way to ask any pool how many entries it holds or how much space they use, for any backend. `clear()` is not: `CacheItemPoolInterface::clear()` is already part of PSR-6 and works fine when you have a live pool instance in hand. `CacheDirectory::clear()` exists for exactly the case where you don&#039;t — an admin command that only knows a path, not which adapter type (namespace, TTL, backend) originally wrote there.

## `CacheWarmer` and `WarmableInterface`: Warming Without a Kernel

```php
use Derafu\Cache\CacheWarmer;
use Derafu\Cache\Contract\WarmableInterface;

final class ExampleWarmable implements WarmableInterface
{
    public function warmup(): void { /* ... */ }
}

$failures = (new CacheWarmer())-&gt;warmup([$warmableA, $warmableB]);
// list&lt;array{warmable: WarmableInterface, exception: Throwable}&gt; — empty if everything succeeded.
```

`symfony/cache` has no warmup concept at all — only `PruneableInterface` (the opposite: removing *expired* entries). Symfony&#039;s real `CacheWarmerInterface` lives in `symfony/http-kernel`, tied to the full framework Kernel&#039;s boot lifecycle: it warms the *framework&#039;s own* caches (compiled container, routing) into a temporary location and atomically swaps them in, specifically because a half-written container cache would take the whole application down.

Warming an application-level PSR-6 cache doesn&#039;t carry that risk — if a warmable fails partway through, the next real read just recomputes that one entry. So `CacheWarmer` doesn&#039;t try to replicate the atomic swap or the Kernel&#039;s boot-order resolution: it runs each warmable in order, keeps going if one throws, and reports every failure at the end instead of stopping at the first one.

There&#039;s deliberately no bundled warmup *command*: a generic one would need to discover which `WarmableInterface` instances exist in a given application and how to construct each one — almost none will have a no-argument constructor, since a real warmable needs whatever it&#039;s warming. That discovery is exactly what a DI container is for, which this package chooses not to depend on (see [Console Commands](#console-commands-optional) below). The intended shape is a small, application-specific command that constructs the warmables it actually has and calls `(new CacheWarmer())-&gt;warmup($warmables)`.

## Console Commands (Optional)

```php
use Derafu\Cache\Console\ClearLocalCacheCommand;
use Derafu\Cache\Console\LocalCacheStatsCommand;

$application-&gt;add(new ClearLocalCacheCommand());
$application-&gt;add(new LocalCacheStatsCommand());
```

```bash
php bin/console derafu:local-cache:clear /var/cache/my_package
php bin/console derafu:local-cache:stats /var/cache/my_package
```

Both wrap `CacheDirectory` and require only a path. Named `derafu:local-cache:*`, not the more obvious `cache:clear`/`cache:stats` — that name is already taken in any real Symfony full-stack app (`symfony/framework-bundle` ships its own `cache:clear`, for the framework&#039;s own internal cache), and a generic name from a library meant to be added into someone else&#039;s `Application` is a collision waiting to happen. Still want a shorter name? Rename after construction: `(new ClearLocalCacheCommand())-&gt;setName(&#039;cache:clear-local&#039;)`.

Neither command is registered automatically — this package has no DI container of its own to register commands into. `symfony/console` is a `suggest`, not a hard requirement: Composer&#039;s autoloading is lazy, so a consumer that never references `Console\*` pays nothing for it even without `symfony/console` installed.

## Requirements

PHP 8.5+. Depends on [`symfony/cache`](https://symfony.com/doc/current/components/cache.html), `psr/cache`, and `psr/simple-cache`.




---

## Console Project

Symfony Console Integration for the Derafu Kernel

# Symfony Console Integration for the Derafu Kernel

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/console/main)
![CI Workflow](https://github.com/derafu/console/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/console)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/console)
![Total Downloads](https://poser.pugx.org/derafu/console/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/console/d/monthly)

Adds [Symfony Console](https://symfony.com/doc/current/components/console.html) command auto-discovery and execution to any [`Derafu\Kernel\MicroKernel`](https://www.derafu.dev/docs/core/kernel)-based Kernel — no per-command registration, no opinion on whether a project reuses its main Kernel or a dedicated console-only one.

## What This Package Provides

- `Derafu\Console\DependencyInjection\CommandCompilerPass`: collects every service tagged `console.command` into a `console.command_ids` container parameter.
- `Derafu\Console\ConsoleKernelTrait`: adds command auto-discovery and a `run(): int` method to any class extending `Derafu\Kernel\MicroKernel` (or providing an equivalent `getContainer(): ContainerInterface`).
- `Derafu\Console\Kernel`: a ready-to-use console Kernel for projects that do not need to compose console support into an existing Kernel.
- `Derafu\Console\Runtime`: resolves the real process environment (`APP_ENV`/`APP_DEBUG`) before any Kernel exists — see below.

This package does not depend on `derafu/http` or any HTTP-related package. It has no opinion on whether a project reuses its main application Kernel, a dedicated console-only subclass of it, or a fresh one — that decision belongs entirely to each project.

## Installation

```bash
composer require derafu/console
```

## `Runtime`: Resolving `APP_ENV`/`APP_DEBUG` Before the Kernel Exists

A Kernel&#039;s own `Derafu\Kernel\Environment::getEnv()` cannot help decide *its own* `$environment`/`$debug` constructor arguments — it&#039;s an instance method, and the instance doesn&#039;t exist yet. Something has to resolve those two values from the real process environment first.

Reading `$_ENV[&#039;APP_ENV&#039;]` directly is not reliable: `$_ENV` is only populated when the `variables_order` php.ini directive includes `E`, which is not every PHP installation&#039;s default — a stock Homebrew PHP on macOS, for example, ships with `GPCS`, without `E`. `$_SERVER` does carry real process environment variables in the CLI SAPI regardless of that setting, so `Runtime::getApplicationContext()` merges both:

```php
use Derafu\Console\Runtime;

$context = Runtime::getApplicationContext();
// [&#039;APP_ENV&#039; =&gt; &#039;prod&#039;, &#039;APP_DEBUG&#039; =&gt; false, ...] — plus whatever else
// was already in $_SERVER/$_ENV.
```

Defaults to `prod`/`false` when neither is set — deliberately the *opposite* of [`derafu/http`&#039;s `Runtime`](https://www.derafu.dev/docs/core/http), whose `dev`/`true` fallback only matters when an HTTP project has no `.env` of its own (`Derafu\Kernel\Environment::loadEnvironmentVariables()` loads one). A console tool installed via Composer and run as `bin/console` has no such file: the values this class resolves *are* the actual default a fresh install runs with, so it has to be the safe one — a stack trace should never be exposed unless `APP_DEBUG` is explicitly set.

`Runtime::run()` resolves the context, builds the Kernel through a factory, and runs it — this is the actual recommended `bin/console` entry point, not `getApplicationContext()` used by hand:

```php
// bin/console
use Derafu\Console\Kernel;
use Derafu\Console\Runtime;

require dirname(__DIR__) . &#039;/vendor/autoload.php&#039;;

exit(Runtime::run(fn (array $context): Kernel =&gt; new Kernel(
    $context[&#039;APP_ENV&#039;],
    (bool) $context[&#039;APP_DEBUG&#039;],
)));
```

## Usage

### A Project With No Other Kernel (Simplest Case)

`Kernel` reads `services.yaml` from the environment&#039;s configuration directory. Any service in it whose class extends `Symfony\Component\Console\Command\Command` is discovered automatically.

```php
// bin/console
use Derafu\Console\Kernel;
use Derafu\Console\Runtime;

require dirname(__DIR__) . &#039;/vendor/autoload.php&#039;;

exit(Runtime::run(fn (array $context): Kernel =&gt; new Kernel(
    $context[&#039;APP_ENV&#039;],
    (bool) $context[&#039;APP_DEBUG&#039;],
)));
```

### A Project That Already Has Its Own Kernel

Use `ConsoleKernelTrait` directly on a dedicated subclass, so the main Kernel is not forced to carry console-specific wiring it may rarely need:

```php
use Derafu\Console\ConsoleKernelTrait;

class ConsoleApplication extends Application // your project&#039;s own Kernel.
{
    use ConsoleKernelTrait;

    protected function configure(
        ContainerConfigurator $configurator,
        ContainerBuilder $container
    ): void {
        parent::configure($configurator, $container);
        $this-&gt;configureConsole($container);
    }
}
```

```php
// bin/console
use App\ConsoleApplication;
use Derafu\Console\Runtime;

require dirname(__DIR__) . &#039;/vendor/autoload.php&#039;;

exit(Runtime::run(fn (array $context): ConsoleApplication =&gt; new ConsoleApplication(
    $context[&#039;APP_ENV&#039;],
    (bool) $context[&#039;APP_DEBUG&#039;],
)));
```

The same applies if the console needs to share the exact same `services.yaml` as, e.g., an HTTP Kernel: extend that Kernel instead of `Derafu\Console\Kernel` and `use ConsoleKernelTrait` the same way — see [Backbone Console](https://www.derafu.dev/docs/core/backbone-console) for a real example of this (`libredte-lib-core-console`&#039;s `ConsoleApplication` extends its own business Kernel, not `Derafu\Console\Kernel`).

### Overriding the Application Built by `run()`

`ConsoleKernelTrait::createConsoleApplication()` builds the underlying `Symfony\Component\Console\Application` — override it to customize the name/version, or to install a `CommandLoaderInterface` (as [Backbone Console](https://www.derafu.dev/docs/core/backbone-console) does):

```php
use Symfony\Component\Console\Application as SymfonyConsoleApplication;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ConsoleApplication extends Application
{
    use ConsoleKernelTrait {
        createConsoleApplication as private buildBaseConsoleApplication;
    }

    protected function createConsoleApplication(
        ContainerInterface $container
    ): SymfonyConsoleApplication {
        $application = $this-&gt;buildBaseConsoleApplication($container);

        // e.g. $application-&gt;setCommandLoader(...);

        return $application;
    }
}
```

The alias (`createConsoleApplication as private buildBaseConsoleApplication`) is required to call the trait&#039;s own implementation: a trait&#039;s methods are mixed directly into the class, not inherited through a parent class, so `parent::createConsoleApplication()` would not resolve.

### Customizing the Application Name and Version

Set these as container parameters (e.g. in `services.yaml`) and they will be picked up automatically:

```yaml
parameters:
    kernel.app_name: &#039;My CLI&#039;
    kernel.app_version: &#039;1.0.0&#039;
```

## Requirements

PHP 8.5+. Depends on [`derafu/kernel`](https://www.derafu.dev/docs/core/kernel) and `symfony/console`.




---

## Backbone Console Project

Turn Any Backbone Operation Into a CLI Command

# Turn Any Backbone Operation Into a CLI Command

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/backbone-console/main)
![CI Workflow](https://github.com/derafu/backbone-console/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/backbone-console)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/backbone-console)
![Total Downloads](https://poser.pugx.org/derafu/backbone-console/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/backbone-console/d/monthly)

Exposes every operation a [`SafeExplorerInterface`](https://www.derafu.dev/docs/core/backbone-dispatcher) discovers as its own auto-discovered [Symfony Console](https://symfony.com/doc/current/components/console.html) command — no per-operation `Command` subclass, ever.

## Why

[Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher) already turns a string operation id and a plain array of parameters into a safe, serializable call. This package is one more transport on top of it — the same idea [Backbone API](https://www.derafu.dev/docs/core/backbone-api) is for HTTP, but for a standalone CLI process, meant to be run by hand or invoked from any other language via `exec()`/`system()`/`subprocess.run()`, not just from PHP.

A business library can expose hundreds of operations. `OperationCommandLoader` builds a `Symfony\Component\Console\Command\Command` for exactly one, on demand, only when a `bin/console` invocation actually needs it — never all of them upfront.

## Installation

```bash
composer require derafu/backbone-console
```

## Usage

Compose a Kernel with [`derafu/console`](https://www.derafu.dev/docs/core/console)&#039;s `ConsoleKernelTrait`, and install `OperationCommandLoader` as the `CommandLoaderInterface`:

```php
use Derafu\BackboneConsole\Service\OperationCommandLoader;
use Derafu\BackboneDispatcher\Contract\SafeDispatcherInterface;
use Derafu\BackboneDispatcher\Contract\SafeExplorerInterface;
use Derafu\Console\ConsoleKernelTrait;
use Symfony\Component\Console\Application as SymfonyConsoleApplication;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ConsoleApplication extends YourOwnKernel // e.g. your business library&#039;s own Kernel.
{
    use ConsoleKernelTrait {
        createConsoleApplication as private buildBaseConsoleApplication;
    }

    protected function createConsoleApplication(
        ContainerInterface $container
    ): SymfonyConsoleApplication {
        $application = $this-&gt;buildBaseConsoleApplication($container);

        $application-&gt;setCommandLoader(new OperationCommandLoader(
            $container-&gt;get(SafeExplorerInterface::class),
            $container-&gt;get(SafeDispatcherInterface::class),
        ));

        return $application;
    }
}
```

```php
// bin/console
use App\ConsoleApplication;
use Derafu\Console\Runtime;

require dirname(__DIR__) . &#039;/vendor/autoload.php&#039;;

exit(Runtime::run(fn (array $context): ConsoleApplication =&gt; new ConsoleApplication(
    $context[&#039;APP_ENV&#039;],
    (bool) $context[&#039;APP_DEBUG&#039;],
)));
```

Extending your own Kernel directly (rather than `Derafu\Console\Kernel`) is deliberate whenever the console needs the exact same `services.yaml`-wired services (like `SafeExplorerInterface`/`SafeDispatcherInterface` above) as the rest of the project — see [Console](https://www.derafu.dev/docs/core/console#a-project-that-already-has-its-own-kernel).

Every discovered operation becomes its own command, named by converting its id&#039;s `.`/`::` separators to Symfony Console&#039;s own `:` convention:

```
billing.invoice.builder::build  →  billing:invoice:builder:build
```

```bash
bin/console list
bin/console help billing:invoice:builder:build
```

## The Request: One File (or STDIN), Any Format

A `Worker`&#039;s operations can each take arbitrarily different parameters — there is no fixed set of CLI flags to define per command, only reflection (via `SafeExplorerInterface`) knows the shape at runtime. So the request is always a single JSON, YAML, or XML document, with the operation&#039;s parameters under a `&quot;parameters&quot;` key — the same shape an HTTP body would use:

```bash
echo &#039;{&quot;parameters&quot;: {&quot;number&quot;: &quot;F-001&quot;, &quot;amount&quot;: 15000}}&#039; \
    | bin/console billing:invoice:builder:build

bin/console billing:invoice:builder:build request.json
bin/console billing:invoice:builder:build request.yaml
bin/console billing:invoice:builder:build -   # &quot;-&quot;: explicit STDIN.
```

The `input` argument is optional — omitted, or given as `-`, reads from STDIN. Format is auto-detected from content: a leading `&lt;` is tried as XML, then JSON, then YAML (YAML is a syntactic superset of JSON, so trying it first would mean JSON is never actually detected as its own format).

Content starting with `{`/`[` is treated as an unambiguous signal that JSON was intended: if it fails to parse as JSON, that is a real, thrown error (`EX_DATAERR`, see below) rather than a silent fallback to YAML. Without this, some malformed JSON parses successfully as something else instead of failing loudly — a trailing comma (`{&quot;a&quot;: 5,}`) is invalid JSON but valid YAML flow-style, so it used to silently reinterpret into a document simply missing whatever came after the comma. The trade-off: a hand-written top-level YAML *flow-style* document (`{a: 5}` written as YAML on purpose, not as a JSON typo) is no longer accepted — YAML&#039;s more common block style (`a: 5` on its own line) is completely unaffected.

## The Response: Same Format as the Request, or Forced With `--output`

Without `--output`, the response is written to STDOUT in the same format the request came in as — a successful one is `{&quot;meta&quot;: {&quot;timestamp&quot;: ..., &quot;data_type&quot;: ...}, &quot;data&quot;: &lt;value&gt;}` (the same envelope [Backbone API](https://www.derafu.dev/docs/core/backbone-api) uses, so a caller does not have to special-case which transport it is talking to); a failed one is `ProblemDetailInterface::toArray()` (see [Backbone Dispatcher](https://www.derafu.dev/docs/core/backbone-dispatcher#handling-failure-problemdetail), `extensions.timestamp`/`extensions.data_type` included there too), written to STDERR instead, alongside a resolved exit code (see below).

`--output &lt;path&gt;` and `--error-output &lt;path&gt;` write to a real file instead of STDOUT/STDERR, independently of each other — `--output` only ever applies on success, `--error-output` only ever on failure (a single invocation only ever hits one of the two branches, so they can even point at the same path without conflict). Piping with shell redirection (`&gt;`/`2&gt;`) already works, but conflates the structured payload with anything else the process might write; these options guarantee only the payload lands in the file:

```bash
bin/console billing:invoice:builder:build request.json --output=result.yaml
bin/console billing:invoice:builder:build request.json --error-output=problem.xml
```

The destination&#039;s own file extension (`.json`/`.yaml`/`.yml`/`.xml`) decides the *response* format, independent of the *request*&#039;s — an unrecognized or missing extension (e.g. `.txt`) falls back to the request&#039;s format, same as omitting the option entirely. `-` is accepted explicitly for &quot;STDOUT&quot;/&quot;STDERR&quot;, the same convention the `input` argument already uses.

A write failure is never silent: a missing destination directory or a permissions error is reported on the real STDERR and returns `GenericOperationCommand::EX_CANTCREAT` — never a false &quot;success&quot; while nothing was actually written.

## Execution Metadata: `-v`

The response payload&#039;s *shape* never depends on where it is written, only on `-v`/`--verbose` — and only for how *much* ends up in it, never whether `meta`/`data_type`/`timestamp` are there at all (they always are). Without `-v`, `meta` has just `timestamp`/`data_type`, as shown above. With it, the rest of [`ExecutionMetadata`](https://www.derafu.dev/docs/core/backbone-dispatcher#execution-metadata-executionmetadata) (`startedAt`, timing, memory, CPU, load average) is merged into that same `meta` on success, or into `extensions` on failure (next to the already-present `debug`/`context`/`throwable`) — never a new top-level key:

```bash
bin/console -v billing:invoice:builder:build request.json
```

```json
{
    &quot;meta&quot;: {
        &quot;timestamp&quot;: 1755500000.123456,
        &quot;data_type&quot;: &quot;App\\Entity\\Invoice&quot;,
        &quot;startedAt&quot;: &quot;2026-01-20T10:00:00+00:00&quot;,
        &quot;finishedAt&quot;: &quot;2026-01-20T10:00:00+00:00&quot;,
        &quot;realTime&quot;: 0.0234,
        &quot;userTime&quot;: 0.0198,
        &quot;systemTime&quot;: 0.0012,
        &quot;memoryUsed&quot;: 131072,
        &quot;peakMemory&quot;: 4194304,
        &quot;pid&quot;: 12345,
        &quot;loadAverage1Min&quot;: 0.52,
        &quot;loadAverage5Min&quot;: 0.61,
        &quot;loadAverage15Min&quot;: 0.58
    },
    &quot;data&quot;: { &quot;id&quot;: &quot;INV-001&quot; }
}
```

## Exit Codes

`bin/console help &lt;command&gt;` always lists every exit code that specific command can return — the fixed ones below, plus whatever the injected `ExitCodeResolverInterface` reports via `describe()` (see next section).

| Code | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | The operation failed, and nothing more specific applies (`DefaultExitCodeResolver`&#039;s fallback). |
| `10`–`16` | One of `derafu/backbone-dispatcher`&#039;s own 7 generic exceptions — see below. |
| `65` (`EX_DATAERR`) | The request could not be parsed as JSON/YAML/XML. |
| `66` (`EX_NOINPUT`) | The input file does not exist or is not readable. |
| `70` (`EX_SOFTWARE`) | An unexpected internal error — a bug, not a usage or business problem. |
| `73` (`EX_CANTCREAT`) | `--output`/`--error-output` could not be created. |

`65`/`66`/`70`/`73` are [`sysexits(3)`](https://man.freebsd.org/cgi/man.cgi?query=sysexits) codes — a real BSD convention (`/usr/include/sysexits.h` on macOS/BSD), chosen because they start at `64` specifically to avoid colliding with whatever small integers other programs already use for their own exit codes. `2` (Symfony Console&#039;s own `Command::INVALID`) is reserved but currently unused by this package.

### `ExitCodeResolverInterface`: Mapping Exceptions to Codes

Every failure not caused by this command itself (a `ProblemDetailInterface`, produced by the dispatch) goes through `ExitCodeResolverInterface::resolve()`. `DefaultExitCodeResolver` — the default — already maps the 7 exceptions generic to *any* Backbone-based project, since they mean the same thing regardless of domain:

| Code | Exception |
| --- | --- |
| `10` | `OperationNotFoundException` |
| `11` | `OperationNotAllowedException` |
| `12` | `MissingParameterException` |
| `13` | `InvalidParameterTypeException` |
| `14` | `ClassNotFoundException` |
| `15` | `FromArrayMethodNotFoundException` |
| `16` | `NoDeserializerFoundException` |

A project that also wants its *own* business exceptions mapped extends `DefaultExitCodeResolver` rather than starting from nothing, layering its mapping on top via `parent::`:

```php
use Derafu\BackboneConsole\Service\DefaultExitCodeResolver;
use Derafu\BackboneDispatcher\Contract\ProblemDetailInterface;

class MyExitCodeResolver extends DefaultExitCodeResolver
{
    public function resolve(ProblemDetailInterface $problem): int
    {
        return match ($problem-&gt;getThrowable()-&gt;getClass()) {
            MyBusinessException::class =&gt; 20,
            default =&gt; parent::resolve($problem),
        };
    }

    public function describe(): array
    {
        return [MyBusinessException::class =&gt; 20] + parent::describe();
    }
}
```

Codes `&gt;= 10` are the convention for a project&#039;s own mapping — clear of Symfony Console&#039;s `0`/`1`/`2`, `DefaultExitCodeResolver`&#039;s own `10`–`16`, and the `64`–`78` `sysexits(3)` range this package uses internally.

## Requirements

PHP 8.5+. Depends on [`derafu/backbone-dispatcher`](https://www.derafu.dev/docs/core/backbone-dispatcher), [`derafu/console`](https://www.derafu.dev/docs/core/console), `derafu/xml`, and `symfony/console`/`symfony/yaml`.





---
Last updated on 09/09/2026

